การกำหนดและใช้งาน BottomSheet ใน Flutter

เขียนเมื่อ 3 ปีก่อน โดย Ninenik Narkdee
draggablescrollablesheet bottomsheet flutter gesturedetector

คำสั่ง การ กำหนด รูปแบบ ตัวอย่าง เทคนิค ลูกเล่น การประยุกต์ การใช้งาน เกี่ยวกับ draggablescrollablesheet bottomsheet flutter gesturedetector

ดูแล้ว 6,112 ครั้ง




เนื้อหาต่อไปนี้จะมาดูเกี่ยวกับการใช้งาน Bottom Sheet ส่วน
ที่ใช้สำหรับแสดงเนื้อหาหรือรายละเอียด หรือปุ่มคำสั่งเพิ่มเติม
โดยจะเป็นส่วนที่เลื่อนมาจากขอบด้านล่างของหน้าจอ มีทั้งแบบ
แสดงถาวรและแบบแสดงแบบ modal ชั่วคราวคล้าย popup ก็ได้
ขึ้นกับการปรับใช้งาน เราจะใช้เนื้อหาต่อเนื่องจากการใช้งาน WebView
ในบทความตอนที่แล้ว เดิมที่เราเพิ่มส่วนของ PopupMenuButton เราจะ
เปลี่ยนมาเป็นปุ่มเมนูเพิ่มเติม แสดงในส่วนของ Bottom Sheet แทน
ทบทวนเนื้อหาตอนที่แล้วได้ที่บทความ
    การกำหนดและใช้งาน PopupMenuButton ใน Flutter http://niik.in/1044
 
    *เนื้อหานี้ใช้เนื้อหาต่อเนื่องจากบทความ http://niik.in/961
 
 
 

การใช้งาน BottomSheet

    ก่อนลงไปในโค้ดรายละเอียด จะขอแนะนำวิธีการใช้งาน ในแต่ละแบบ โดยใช้หน้า profile.dart
ประกอบเนื้อหา  โดยรูปแบบการใช้งาน Bottom Sheet จะมีด้วยกัน 3 รูปแบบ
    - แบบใช้ Scaffold.bottomSheet constructor แสดงแบบถาวร
    - แบบใช้ ScaffoldState.showBottomSheet function แสดงแบบถาวร
    - แบบใช้ showModalBottomSheet function แสดงชั่วคราว สามารถยกเลิกได้
 
 

    แบบใช้ Scaffold.bottomSheet constructor

    ไฟล์ profile.dart
 
import 'package:flutter/material.dart';
 
class Profile extends StatefulWidget {
    static const routeName = '/profile';

    const Profile({Key? key}) : super(key: key);
 
    @override
    State<StatefulWidget> createState() {
        return _ProfileState();
    }
}
 
class _ProfileState extends State<Profile> {  
 
    @override
    Widget build(BuildContext context) {
 
        return Scaffold(
            appBar: AppBar(
                title: Text('Profile'),
            ),
            body: Center(
                child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: <Widget>[
                        Text('Profile Screen'),
                        const SizedBox(height: 20,),
                        ElevatedButton(
                          onPressed: (){},
                          child: const Text('Toggle Bottom Sheet'),
                        ),                            
                    ],
                )
            ),
            bottomSheet:BottomSheet(
              enableDrag: false,
              onClosing: (){}, 
              builder: (BuildContext context){
                return Container(
                  color: Colors.grey,
                  height: 200,
                  child: Center(
                      child: ElevatedButton(
                        onPressed: (){}, 
                        child: Text("Bottom Sheet")
                        )
                  ),
                );
              }
            ),
        );
    }
}
 
    ผลลัพธ์ที่ได้
 


 
 
    ข้างต้นเป็นการใช้งาน BottomSheet แบบใช้ Scaffold.bottomSheet constructor โดยกำหนดเป็น
widget เข้าไปใน property bottomSheet ของ Scaffold เมื่อเปิดหน้านี้ขึ้นมาก็จะแสดงทันทีและค้างอยู่
อย่างนั้นจนกว่าจะมีการกำหนดการทำคำสั่งให้ปิดการแสดงไป ค่าเริ่มต้นของ enableDrag จะเป็น true
ถ้าเราไม่กำหนด enableDrag: false, จะเกิด error ขึ้นได้ หากต้องการใช้งานเป็น true ต้องกำหนดในส่วน
ของการใช้งาน animationController เข้าไปด้วย รวมถึงใช้งาน TickerProviderStateMixin คือ
    เปลี่ยนจาก
 
class _ProfileState extends State<Profile> {  
 
    เป็น
 
class _ProfileState extends State<Profile> with TickerProviderStateMixin {
 
    และกำหนดในส่วนของการใช้งาน BottomSheet โดยเพิ่ม animationController ดังนี้เข้าไปแทน
 
BottomSheet(
  enableDrag: true,
  animationController: BottomSheet.createAnimationController(this),
  onClosing: (){}, 
  builder: (BuildContext context){
    return Container(
      color: Colors.grey,
      height: 200,
      child: ....
      ),
    );
  }
),
 
    ในที่นี้เราจะใช้เป็น enableDrag: false, และไม่กำหนด animationController เพิ่ม ยึดตามโค้ดตัวอย่าง
ด้านบนโค้ดแรก
    จะเห็นว่าการกำหนด BottomSheet ลักษณะนี้ เรายังสามารถใช้งานส่วนของเนื้อหาได้ ถ้ามีการกำหนด
ความสูงของ child ภายใน อย่างข้างต้นกำหนดไว้ที่ 200 ซึ่งถ้าเราไม่กำหนดความสูง ส่วนของ BottomSheet
ก็จะทับอยู่ด้านบนส่วนของ body ของ Scaffold แบบเต็มพื้นที่
    รูปแบบของ BottomSheet ลักษณะนี้เราอาจจะสร้างไว้สำหรับทำเป็นแสดงปุ่มเพิ่มเติมที่ตรึงไว้ขอบล่างของ
หน้าที่ต้องการ แสดงแบบถาวร โดยไม่ต้องปิดก็ได้
 
 
 

    แบบใช้ ScaffoldState.showBottomSheet function

 
    รูปแบบการใช้งานโดยเรียกผ่านฟังก์ชั่น ScaffoldState.showBottomSheet  รูปแบบนี้จะได้ผลลัพธ์เหมือนวิธี
แรก แตกต่างแค่เป็นการเรียกให้แสดงด้วยฟังก์ชั่น และมีการใช้งานผ่าน ScaffoldState ซึ่งถ้าเราเรียกใช้งาน
ภายใน Scaffold เลยก็สามารถใช้เป็นแบบด้านล่างได้เลย แต่ปกติ เราจะต้องแยกสร้างเป็นฟังก์ชั่น เพราะว่า เรา
ต้องสร้างส่วนของเนื้อหา Bottom Sheet เข้าไปอีก 
 
Scaffold.of(context).showBottomSheet()
 
    เมื่อเราแยกเป็นฟังก์ชั่น และเพื่ออ้างอิง ตัว Scaffold ที่กำลังใช้งานอยู่ เราต้องกำหนดค่า key ให้กับ
Scaffold ด้วย โดยกำหนดค่า key ในลักษณะดังนี้
 
// สร้าง key สำหรับ ScaffoldState เหมือนการสร้าง id
final _gKey = GlobalKey<ScaffoldState>();
 
    จากนั้นเรียกใช้ในส่วนของ key property ของ 
 
return Scaffold(
    key: _gKey, // กำหนด key
    appBar: AppBar(
        title: Text('Profile'),
    ),
 
 
    รูปแบบการใช้งานจะเป็นดังนี้
 
 
class _ProfileState extends State<Profile> {  

    // สร้าง key สำหรับ ScaffoldState เหมือนการสร้าง id
    final _gKey = GlobalKey<ScaffoldState>();
 
    @override
    Widget build(BuildContext context) {
 
        return Scaffold(
            key: _gKey, // กำหนด key
            appBar: AppBar(
                title: Text('Profile'),
            ),
            body: Center(
                child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: <Widget>[
                        Text('Profile Screen'),
                        const SizedBox(height: 20,),
                        ElevatedButton(
                          onPressed: _showBottomSheet, // เปิด Bottom Sheet
                          child: const Text('Open Bottom Sheet'),
                        ),                            
                    ],
                )
            ),
        );
    }

    // สร้างฟังก์ชั่นสำหรับเรียกใช้งาน
    void _showBottomSheet(){
      // เรียกใช้ ScaffoldState จาก key แล้วเรียกฟังก์ชั่น showBottomSheet
      _gKey.currentState!.showBottomSheet<void>(
        (BuildContext context) {
          return Container(
            color: Colors.grey,
            height: 200,
            child: Center(
                child: ElevatedButton(
                  onPressed: (){
                    Navigator.of(context).pop(); // ปิด Bottom Sheet
                  }, 
                  child: Text("Close Bottom Sheet")
                ),
            ),
          );
        });   
    }
}
 
    ผลลัพธ์ที่ได้
 


 
 
    เราสร้างฟังก์ชั่น _showBottomSheet() เพื่อเรียกใช้งาน ฟังก์ชั่น showBottomSheet อีกที
เมื่อกดที่ปุ่ม Open ตัว BottomSheet ก็จะเลื่อนขึ้นจากขอบด้านล่าง การเพิ่มเข้าในในลักษณะนี้ เราสามารถ
ใช้ปุ่ม back ปิดตัว BottomSheet ได้ หรือจะใช้คำสั่ง Navigator.of(context).pop() ปิดก็ได้เหมือนกัน
อย่างในตัวอย่าง เมื่อกดปุ่ม close ก็เรียกคำสั่งดังกล่าว เพื่อปิด BottomSheet
    BottomSheet รูปแบบนี้จะคล้ายกับรูปแบบแรก ต่างกันที่ไม่จำเป็นต้องแสดงแบบตรึงถาวรไว้ก็ได้ ใช้
สำหรับแสดงปุ่มหรือเมนูเพิ่มเติม หรือแสดงข้อมูลเพิ่มเติม ชั่วคราวเท่านั้น
 
 

    ประยุกต์แบบใช้ Scaffold.bottomSheet constructor

 
    ก่อนไปที่รูปแบบที่สามรูปแบบสุดท้าย ย้อนมาที่รูปแบบแรกก่อน ในรูปแบบแรก เราสามารถจัดการให้เหมือน
รูปแบบที่สองได้ เช่น เริ่มต้นให้แสดง แต่สามารถปิดได้ เราจะทำในลักษณะนี้
 
    สร้างตัวแปร และฟังก์ชั่น กำหนดการซ่อนหรือแสดง 
 
// สร้างตัวแปรสถานะการซ่อนหรือแสดง
bool _isShowBottomSheet = true;

// สร้างฟังก์ชั่นเปลี่ยนค่า สลับซ่อน / แสดง
void _toggleBottomSheet(value){
  setState(() {
    _isShowBottomSheet = value ? false : true;
  });
}
 
    รูปแบบการใช้งานจะเป็นดังนี้
 
class _ProfileState extends State<Profile> {  

    // สร้าง key สำหรับ ScaffoldState เหมือนการสร้าง id
    final _gKey = GlobalKey<ScaffoldState>();

    // สร้างตัวแปรสถานะการซ่อนหรือแสดง
    bool _isShowBottomSheet = true;

    // สร้างฟังก์ชั่นเปลี่ยนค่า สลับซ่อน / แสดง
    void _toggleBottomSheet(value){
      setState(() {
        _isShowBottomSheet = value ? false : true;
      });
    }
 
    @override
    Widget build(BuildContext context) {
 
        return Scaffold(
            key: _gKey, // กำหนด key
            appBar: AppBar(
                title: Text('Profile'),
            ),
            body: Center(
                child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: <Widget>[
                        Text('Profile Screen'),
                        const SizedBox(height: 20,),
                        ElevatedButton(
                          onPressed: (){
                            _toggleBottomSheet(_isShowBottomSheet);
                          }, // เปิด Bottom Sheet
                          child: const Text('Toggle Bottom Sheet'),
                        ),                            
                    ],
                )
            ),
            bottomSheet: _showBottomSheet(), // เรียกใช้จากฟังก์ชั่น
        );
    }

    // สร้างฟังก์ชั่นสำหรับเรียกใช้งาน คืนค่า Widget? รองรับการส่งค่า null
    Widget? _showBottomSheet(){
        return _isShowBottomSheet // ถ้าเป็น true แสดง
        ? BottomSheet(
          enableDrag: false,
          onClosing: (){}, 
          builder: (BuildContext context){
            return Container(
              color: Colors.grey,
              height: 200,
              child: Center(
                  child: ElevatedButton(
                    onPressed: (){
                      _toggleBottomSheet(_isShowBottomSheet);
                    }, // เปิด Bottom Sheet
                    child: Text("Toggle Bottom Sheet")
                    )
              ),
            );
          }
        )
        : null; // ไม่แสดงส่งค่า null     
    }
}
 
    ผลลัพธ์ที่ได้
 


 
 
    เมื่อเปิดมาครั้งแรก เรากำหนดให้แสดงเป็นค่าเริ่มต้น และเปลี่ยนค่าการซ่อนหรือแสดง โดยการเปลี่ยน
ค่าตัวแปร _isShowBottomSheet  ซึ่งมีผลต่อเงื่อนไขการซ่อนหรือแสดง BottomSheet ในฟังก์ชั่น
_showBottomSheet() ถ้าแสดงก็จะคืนค่าเป็น widget ตามรูปแบบที่กำหนด ถ้าไม่แสดงก็คืนค่าเป็น null
การแสดงลักษณะนี้ จะไม่เหมือนแบบที่สองทุกอย่างเสียทีเดียว เพราะการตรึงในลักษณะนี้ เป็นการซ่อน
หรือแสดงแบบตรึงเท่านั้น ไม่ใช้ชั่วคราว เวลาปิดจึงไม่สามารถใช้คำสั่ง Navigator.of(context).pop() ได้
หากใช้คำสั่งนี้ จะหมายถึงปิดหน้า profile ไปแทนไม่ใช่ปิดเฉพาะส่วนของ BottomSheet  
    การซ่อนหรือปิดในวิธีนี้ จึงใช้การเปลี่ยนแปลงค่าตัวแปร _isShowBottomSheet เป็นเงื่อนไข
 
 

    แบบใช้ showModalBottomSheet function

 
    การใช้งาน BottomSheet แบบสุดท้ายโดยเรียกใช้ผ่านฟังก์ชั่น showModalBottomSheet สังเกตว่ามีคำว่า
Modal เพิ่มเข้ามา นั่นคือรูปแบบการแสดงที่คล้ายกับ dialog หรือ popup  ที่ตัว BottomSheet จะถูกทำให้เด่น
ขึ้นมา โดยมีม่านคลุมสีดำให้เห็นพื้นที่ข้อมูลด้านหลังจางๆ ถ้าเรากดหรือแตะที่พื้นที่นั้น จะเป็นการปิด BottomSheet
อัตโนมัติ หรือเราเรียกพื้นที่คลุมจางนั้นว่า Dismissible
    การใช้งานรูปแบบนี้ เราจะไม่สามารถจัดการกับข้อมูลด้านหลังได้ เว้นแต่จะปิดตัว BottomSheet ไปก่อนเท่านั้น
หากไม่กำหนดความสูงของข้อมูลใน BottomSheet ตัว BottomSheet จะสูงที่ประมาณ 60% ของพื้นที่ เพื่อให้เห็น
ส่วนม่านคลุมจาง ต่างจากรูปแบบที่หนึ่งและสอง หากไม่กำหนดจะแสดงเต็มพืนที่
    BottomSheet รูปแบบนี้จะสามารถปิดได้ด้วยปุ่ม back หรือคำสั่ง Navigator.of(context).pop() หรือแตะที่พื้นที่
Dismissible  ค่าเริ่มต้นของ enableDrag เป็น true เราสามารถลากลงเพื่อปิด โดยไม่ต้องทำการกำหนด 
animationController ได้
 
    รูปแบบการใช้งานจะเป็นดังนี้
 
class _ProfileState extends State<Profile> {  

    // สร้าง key สำหรับ ScaffoldState เหมือนการสร้าง id
    final _gKey = GlobalKey<ScaffoldState>();
 
    @override
    Widget build(BuildContext context) {
 
        return Scaffold(
            key: _gKey, // กำหนด key
            appBar: AppBar(
                title: Text('Profile'),
            ),
            body: Center(
                child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: <Widget>[
                        Text('Profile Screen'),
                        const SizedBox(height: 20,),
                        ElevatedButton(
                          onPressed: _showBottomSheet, // เปิด Bottom Sheet
                          child: const Text('Open Bottom Sheet'),
                        ),                            
                    ],
                )
            ),
        );
    }

    // สร้างฟังก์ชั่นสำหรับเรียกใช้งาน
    Widget? _showBottomSheet(){
      showModalBottomSheet(
        context: context,
        builder: (BuildContext context){
            return Container(
              color: Colors.grey,
              child: Center(
                  child: ElevatedButton(
                    onPressed: (){
                      Navigator.of(context).pop(); // ปิด Bottom Sheet
                    }, 
                    child: Text("Close Bottom Sheet")
                  )
              ),
            );
        }
      );
    }
}
 
    ผลลัพธ์ที่ได้
 


 
 
    เราสามารถกำหนดค่าต่างๆ เพิ่มเติมได้ ดังนี้
 
// enableDrag: false, // ลากปัดขึ้นลง 
// isDismissible: false, // ปิดโดยแตะที่ตัวม่านคลุม
// isScrollControlled: true, // แสดงเต็มพื้นที่ หรือตามความสูงถ้ามีกำหนด
 
 
    ถ้าต้องการให้ BottomSheet รองรับการใช้งาน ListView หรือ GirdView ที่สามารถเลื่อนได้ ให้เรากำหนด
การใช้งาน  DraggableScrollableSheet ซึ่งจะต้องกำหนด isScrollControlled: true, 
    โดยตัว  DraggableScrollableSheet ยังรองรับการกำหนดการใช้งานเพิ่มเติม เช่น กำหนดความสูงของ 
Child widget ใน BottomSheet โดยกำหนดความสูงค่าเริ่มต้น ค่าสูงสุด ค่าต่ำสุด ได้ ดูตัวอย่างการใช้งาน
 
showModalBottomSheet(
  isScrollControlled: true,
  context: context,
  builder: (BuildContext context){
      return DraggableScrollableSheet(
        builder: (BuildContext context, ScrollController scrollController) {
          return Container(
            color: Colors.grey,
            child: Center(
                child: ElevatedButton(
                  onPressed: (){
                    Navigator.of(context).pop(); // ปิด Bottom Sheet
                  }, 
                  child: Text("Close Bottom Sheet")
                )
            ),
          );
        }
      );
  }
);
 
    ผลลัพธ์ที่ได้
 


 
 
    ข้างต้นเราไม่ได้กำหนด property ใดๆ เพิ่มเติมนอกจากใช้งาน builder callback เท่านั้นใน
DraggableScrollableSheet ใช้ค่าเริ่มต้นดังนี้
 
double initialChildSize = 0.5, // ค่าเริ่มต้นเมื่อแสดงครั้งแรก
double minChildSize = 0.25,  // แสดงอย่างน้อยสุด
double maxChildSize = 1.0, // รองรับการแสดงเต็มพื้นที่
 
    ตัวเลขค่าเริ่มต้นข้างต้น เป็นสัดส่วนต่อขนาดความสูง เช่น 1 ก็หมายถึง เต็มจอ 0.5 ก็แสดครึ่งหนึ่ง
    จากผลลัพธ์ เราก็จะเห็นว่าถ้าเรากำหนด  isScrollControlled: true, ให้กับ BottomSheet หากไม่
กำหนดความสูง ตัว child ต่างๆ จะแสดงเต็มพื้นที่ นั่นหมายความว่าตัว Dismissible ไม่มี แล้วพอเรากำหนด
ใช้งาน DraggableScrollableSheet เพิ่มเข้ามา และให้ child ด้านในมีความสูงเริ่มต้นที่ 0.5 หรือ 50% ของ
พื้นที่ จึงเกิดพื้นหลังสีขาวของ BottomSheet ให้เรากำหนดสีพื้นหลังของ BottomSheet เป็นโปร่งใสแทน
ก็จะได้เป็น
 
showModalBottomSheet(
  backgroundColor: Colors.transparent,
  isScrollControlled: true,
  context: context,
  builder: (BuildContext context){
.....
 
    ผลลัพธ์ที่ได้
 


 
 
    ตอนนี้เรามองเห็นส่วนที่เป็นเนื้อหาด้านหลังบ้างแล้ว แต่ว่าส่วนม่านคลุมจุดนี้ ไม่ใช่ตัว Dismissible เวลาเราแตะ
หรือกดจึงไม่ปิดลงไป เพราะเป็นส่วนของ DraggableScrollableSheet  เราสามารถใช้ GestureDetector จำลอง
การทำงานแทน Dismissible โดยจะใช้ GestureDetector สองครั้งคลุมครั้งแรก กำหนดให้ทำงานในส่วนของที่ไม่ใช้
เนื้อหา แล้วซ้อนด้วยที่สั่งไม่ต้องทำงานใดๆ แล้วค่อยซ้อนตัว DraggableScrollableSheet อีกที จะได้เป็นดังนี้
 
showModalBottomSheet(
  backgroundColor: Colors.transparent,
  isScrollControlled: true,
  context: context,
  builder: (BuildContext context){
      return GestureDetector(
        behavior: HitTestBehavior.opaque, // ใช้กับส่วนที่มีการกำหนดการโปร่งใส
        onTap: () => Navigator.of(context).pop(),              
        child: GestureDetector(
          onTap: (){},
          child: DraggableScrollableSheet(
            builder: (BuildContext context, ScrollController scrollController) {
              return Container(
                color: Colors.grey,
                child: Center(
                    child: ElevatedButton(
                      onPressed: (){
                        Navigator.of(context).pop(); // ปิด Bottom Sheet
                      }, 
                      child: Text("Close Bottom Sheet")
                    )
                ),
              );
            }
          ),
        ),
      );
  }
);
 
 
    เราจะจำลองกับ ListView เพื่อใช้งานการเลื่อนขยายขึ้นลงของ DraggableScrollableSheet ดังนี้
 
showModalBottomSheet(
  // enableDrag: false,
  // isDismissible: false,
  backgroundColor: Colors.transparent,
  isScrollControlled: true,
  context: context,
  builder: (BuildContext context){
      return GestureDetector(
        behavior: HitTestBehavior.opaque,
        onTap: () => Navigator.of(context).pop(),              
        child: GestureDetector(
          onTap: (){},
          child: DraggableScrollableSheet(
            initialChildSize: 0.5, // ขนาดแสดงเริ่มต้น 50%
            minChildSize: 0.25, // ปรับขนาดน้อยสุด 25% น้อยกว่านี้จะเป็นการปิด
            maxChildSize: 0.9, // ขยายสูงสุดแค่ 90%
            builder: (BuildContext context, ScrollController scrollController) {
              return Container(
                color: Colors.pink,
                child: ListView( // ใช้งาน ListView
                  controller: scrollController, // ใช้งาน controller
                  children: List.generate(100, (index) { // วนลูปจำลองข้อมูล
                      return Container(
                          padding: const EdgeInsets.all(5.0),
                          height: 75,
                          child: Card(
                            child: Text('Item ${index}'),
                          ),
                      );
                  }),
                )
              );
            }
          ),
        ),
      );
  }
);
 
    ผลลัพธ์ที่ได้
 


 
 
    เมื่อแสดงครั้งขนาดจะอยู่ที่ 50% เมื่อใช้ร่วมกัน ListView ก็จะสามารถขยายขนาดขึ้นลงได้ โดยขยายขึ้น
ได้ไม่เกิน 90% และขยายไปขนาดต่างๆ ได้อยู่ที่ช่วง 25 - 90% ถ้าน้อยกว่า 25% จะเป็นการปิดใช้งาน
 
    เนื้อหาเกี่ยวกับการใช้งาน BottomSheet ค่อนข้างครบพอสมควร เพื่อไม่ให้เสียเวลา จะขอนำโค้ดที่
ปรับใช้กับบทความที่แล้วในไฟล์ artcle.dart เป็นดังนี้
 
    ไฟล์ artcle.dart
 
import 'dart:async';
import 'dart:io';

import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';

class Articles extends StatefulWidget {
    static const routeName = '/articles';
 
    const Articles({Key? key}) : super(key: key);
  
    @override
    State<StatefulWidget> createState() {
        return _ArticlesState();
    }
}
  
class _ArticlesState extends State<Articles> {

    // แก้ไขตัวแปรสำหรับ contrller ใหม่ ให้เป็นชนิดข้อมูล late 
    late final WebViewController _controller;
  /*
   ValueNotifier เป็นชนิดข้อมูลใน Flutter ซึ่งเป็น subclass ของ ChangeNotifier 
   ที่ใช้ในการเก็บข้อมูลและแจ้งเตือนผู้ฟัง (listeners) เมื่อค่าของข้อมูลเปลี่ยนแปลง 
   ชนิดข้อมูลนี้มีประโยชน์ในการจัดการสถานะ (state) อย่างง่ายดาย โดยไม่ต้องใช้ state management 
   library ที่ซับซ้อน เช่น Provider หรือ Bloc
  */
  // กำหนดค่าเริ่มต้นเป็น false    
    final ValueNotifier<bool> _canGoBack = ValueNotifier<bool>(false);
    final ValueNotifier<bool> _canGoForward = ValueNotifier<bool>(false);    
    // ส่วนของตัวแปรจัดการ cookies
    final WebViewCookieManager _cookieManager = WebViewCookieManager();   
    // ส่วนของตัวแปร กำหนดให้ตรวจสอบว่าโหลด url แล้วหรือไม่เพื่อเรียกใช้งานเพียงครั้งเดียวที่เปิดขึ้นมา
    bool _isUrlLoaded = false;

    // สร้างตัวแปรสถานะการซ่อนหรือแสดง
    bool _isBottomSheetShow = false;
 
    // สร้างฟังก์ชั่นเปลี่ยนค่า สลับซ่อน / แสดง
    void _toggleBottomSheet(value){
      setState(() {
        _isBottomSheetShow = value ? false : true;
      });
    }        


    @override
    void initState() {
      super.initState();

      _controller = WebViewController()
      ..setJavaScriptMode(JavaScriptMode.unrestricted)
      ..setBackgroundColor(const Color(0x00000000))
      ..setNavigationDelegate(
        NavigationDelegate(
          onProgress: (int progress) {
            print("WebView is loading (progress : $progress%)");
            // Update loading bar.
          },
          onPageStarted: (String url) async {
            _canGoBack.value = await _controller.canGoBack();
            _canGoForward.value = await _controller.canGoForward();
          },
          onPageFinished: (String url) async {
            _canGoBack.value = await _controller.canGoBack();
            _canGoForward.value = await _controller.canGoForward();
          },          
          onHttpError: (HttpResponseError error) {},
          onWebResourceError: (WebResourceError error) {},
          onNavigationRequest: (NavigationRequest request) {// กำหนดการทำงานเมื่อคลิกลิ้งค์ในเว็บเพจ
            // เช่นการตรวจ url และ block ไม่ให้ใช้้งาน url ที่กำหนด
            if (request.url.startsWith('https://www.youtube.com/')) {
              print('blocking navigation to $request}');
              return NavigationDecision.prevent; // ถ้าเป็นจากลิ้งค์ youtube ให้ block
            }
            print('allowing navigation to $request');
            return NavigationDecision.navigate; // ถ้าเป็นลิ้งค์อื่นๆ เข้าไปปกติ
          },
        ),
      // เพิ่มส่วนนี้เพื่อ สร้าง JavascriptChannel สำหรับรับค่าข้อมูลที่ส่งผ่านทาง JavaScript  
      )..addJavaScriptChannel(
        'Toaster', // กำหนดชื่อสำหรับเรียกใช้งาน
        onMessageReceived: (JavaScriptMessage message) {
          print(message.message);
          // ในที่นี้เมื่อได้ค่ามาแล้ว จะแสดงข้อความด้วย SnackBar
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(content: Text(message.message)),
          );          
        },
      );       

    }


    @override
    Widget build(BuildContext context) {
        // เนื่องจาการใช้งาน PopupMenuButton จะมีการ rebuild widget ทุกครั้งที่กด
        // ดังนั้นเพื่อไม่ให้มีการโหลดหน้าเพจ เมื่อกดที่ปุ่มเมนูนี้ เราต้องกำหนดเงื่อนไขว่า 
        // โหลดหน้าเพจเฉพาะครั้งแรกเท่าานั้น
        if (!_isUrlLoaded) {
        // รับค่า url ที่ส่งมาใน arguments          
          final url = ModalRoute.of(context)!.settings.arguments as String;
          _controller.loadRequest(Uri.parse(url));
          _isUrlLoaded = true;
        }

        return Scaffold(
            appBar: AppBar(
                title: Text('Articles'),
                actions: <Widget>[
                  NavigationControls( // เมนูส่วนของการใช้งาน NavigationControls
                    controller: _controller, 
                    canGoBack: _canGoBack, 
                    canGoForward: 
                    _canGoForward
                  ),          
/*                   SampleMenu( // เมนูส่วนของการใชงาน PopupMenuButton
                    controller: _controller,
                    cookieManager: _cookieManager), */
                  IconButton(
                    onPressed: (){
                      setState(() {
                        _toggleBottomSheet(_isBottomSheetShow);
                      });
                    }, 
                    icon: const Icon(Icons.more_vert),
                  ),                    
                ],                
            ),
            body: WebViewWidget(controller: _controller),
            floatingActionButton: scrollTopButton(), // เรียกใช้ปุ่มจากฟังก์ชั่น
            bottomSheet: _showBottomSheet(),
        );
    }


    // สร้างฟังก์ชั่นสำหรับเรียกใช้งาน
    Widget? _showBottomSheet(){
        return _isBottomSheetShow 
         ? BottomSheet(
          backgroundColor: Colors.pink.withAlpha(100),
          enableDrag: false,
          onClosing: () {},
          builder: (context) {
            return Column(
              mainAxisSize: MainAxisSize.min,
              children: <Widget>[
                ListTile(
                  leading: const Icon(FontAwesomeIcons.share),
                  title: const Text('Share'),
                  onTap: () => _toggleBottomSheet(_isBottomSheetShow),
                ),
                ListTile(
                  leading: const Icon(FontAwesomeIcons.link),
                  title: const Text('Copy link'),
                  onTap: () => _toggleBottomSheet(_isBottomSheetShow),
                ),
                ListTile(
                  leading: const Icon(FontAwesomeIcons.facebookMessenger),
                  title: const Text('Share to Messenger'),
                  onTap: () => _toggleBottomSheet(_isBottomSheetShow),
                ),
                ListTile(
                  leading: const Icon(FontAwesomeIcons.externalLinkAlt),
                  title: const Text('Open in Browser'),
                  onTap: () => _toggleBottomSheet(_isBottomSheetShow),
                ),
              ],
            );
          },
        )
        : null;      
    }

    // สร้างฟังก์ชั่น คืนค่าเป็น widget
    Widget scrollTopButton() {
      return FloatingActionButton( // คืนค่าเป็นปุ่มรูปหัวใจ
          onPressed: () async { // ถ้ากด
              // เรียกคำสั่ง javascript  เลื่อน scroll ไปด้านบนสุด
              await _controller.runJavaScript('window.scrollTo(0, 0);');      
          },
          child: const Icon(Icons.arrow_upward),
        );
    }

}

// สร้าง widget สำหรับทำปุ่มควบคุม เช่น ก่อนหน้า ย้อนหลัง รีเฟรช
class NavigationControls extends StatelessWidget {

  // กำหนด class constructor รับค่าที่จำเป็น
  const NavigationControls({
    required this.controller,
    required this.canGoBack,
    required this.canGoForward,
    Key? key,
  }) : super(key: key);

  // กำหนดตัวแปรที่เกี่ยวข้อง
  /*
   ValueNotifier เป็นชนิดข้อมูลใน Flutter ซึ่งเป็น subclass ของ ChangeNotifier 
   ที่ใช้ในการเก็บข้อมูลและแจ้งเตือนผู้ฟัง (listeners) เมื่อค่าของข้อมูลเปลี่ยนแปลง 
   ชนิดข้อมูลนี้มีประโยชน์ในการจัดการสถานะ (state) อย่างง่ายดาย โดยไม่ต้องใช้ state management 
   library ที่ซับซ้อน เช่น Provider หรือ Bloc
  */
  final WebViewController controller;
  final ValueNotifier<bool> canGoBack;
  final ValueNotifier<bool> canGoForward;

  /*
  ValueListenableBuilder เป็น widget ที่ใช้ในการสร้าง UI ที่ฟังการเปลี่ยนแปลงค่าของ 
  ValueNotifier และทำการ rebuild UI เมื่อค่าของ ValueNotifier มีการเปลี่ยนแปลง
  */
  @override
  Widget build(BuildContext context) {
    return Row(
      children: <Widget>[
        ValueListenableBuilder<bool>(
          valueListenable: canGoBack,
          builder: (context, value, child) {
            return IconButton(
              icon: const Icon(Icons.arrow_back),
              onPressed: value ? () => controller.goBack() : null,
            );
          },
        ),
        ValueListenableBuilder<bool>(
          valueListenable: canGoForward,
          builder: (context, value, child) {
            return IconButton(
              icon: const Icon(Icons.arrow_forward),
              onPressed: value ? () => controller.goForward() : null,
            );
          },
        ),
        IconButton(
          icon: const Icon(Icons.refresh),
          onPressed: () => controller.reload(),
        ),
      ],
    );
  }
}


// กำหนด Enum Type สำหรับเป็นลิสรายการของ PopupMenuButton
enum MenuOptions {
  showUserAgent,
  listCookies,
  clearCookies,
  addToCache,
  listCache,
  clearCache,
}
 
// สร้าง widget สำหรับทำปุ่มควบคุม เพิ่มเติมแบบ PopupMenuButton
class SampleMenu extends StatelessWidget {

  // กำหนด class constructor รับค่าที่จำเป็น
  SampleMenu({
    required this.controller,
    required this.cookieManager,
    Key? key,
  }) : super(key: key);

  final WebViewController controller; // ใช้งาน WebViewController
  final WebViewCookieManager cookieManager; // ใช้งาน CookieManager
 
  @override
  Widget build(BuildContext context) {
        return PopupMenuButton<MenuOptions>(
          onSelected: (MenuOptions value) {
            switch (value) { // ใช้เงื่อนไขค่าที่เลือก ทำฟังก์ชั่นที่ต้องการ
              case MenuOptions.showUserAgent:
                _onShowUserAgent(controller, context);
                break;
              case MenuOptions.listCookies:
                _onListCookies(controller, context);
                break;
              case MenuOptions.clearCookies:
                _onClearCookies(context);
                break;
              case MenuOptions.addToCache:
                _onAddToCache(controller, context);
                break;
              case MenuOptions.listCache:
                _onListCache(controller, context);
                break;
              case MenuOptions.clearCache:
                _onClearCache(controller, context);
                break;
            }
          },
          itemBuilder: (BuildContext context) => <PopupMenuItem<MenuOptions>>[
            PopupMenuItem<MenuOptions>(
              value: MenuOptions.showUserAgent,
              child: const Text('Show user agent'),
              // enabled: controller.!,
            ),
            const PopupMenuItem<MenuOptions>(
              value: MenuOptions.listCookies,
              child: Text('List cookies'),
            ),
            const PopupMenuItem<MenuOptions>(
              value: MenuOptions.clearCookies,
              child: Text('Clear cookies'),
            ),
            const PopupMenuItem<MenuOptions>(
              value: MenuOptions.addToCache,
              child: Text('Add to cache'),
            ),
            const PopupMenuItem<MenuOptions>(
              value: MenuOptions.listCache,
              child: Text('List cache'),
            ),
            const PopupMenuItem<MenuOptions>(
              value: MenuOptions.clearCache,
              child: Text('Clear cache'),
            ),
          ],
        );
  }
 
  // ส่วนของฟังก์ชั่นการทำงานต่างๆ 
 
  // ฟังก์ชั่นแสดง UserAgent ของ WebView
  void _onShowUserAgent(
      WebViewController controller, BuildContext context) async {
    await controller.runJavaScript(
        'Toaster.postMessage("User Agent: " + navigator.userAgent);');
  }
 
  // ฟังก์ชั่นแสดงรายการ cookie
  void _onListCookies(
      WebViewController controller, BuildContext context) async {
      final String cookies = await controller
      .runJavaScriptReturningResult('document.cookie')
      .then((value) => value.toString());
    ScaffoldMessenger.of(context).showSnackBar(SnackBar(
      content: Column(
        mainAxisAlignment: MainAxisAlignment.end,
        mainAxisSize: MainAxisSize.min,
        children: <Widget>[
          const Text('Cookies:'),
          _getCookieList(cookies),
        ],
      ),
    ));
  }
 
  // ฟังก์ชั่นเพิ่มรายการ cache
  void _onAddToCache(WebViewController controller, BuildContext context) async {
    await controller.runJavaScript(
        'caches.open("test_caches_entry"); localStorage["test_localStorage"] = "dummy_entry";');
    ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
      content: Text('Added a test entry to cache.'),
    ));
  }
 
  // ฟังก์ชั่นแสดงรายการ cache
  void _onListCache(WebViewController controller, BuildContext context) async {
    await controller.runJavaScript('caches.keys()'
        '.then((cacheKeys) => JSON.stringify({"cacheKeys" : cacheKeys, "localStorage" : localStorage}))'
        '.then((caches) => Toaster.postMessage(caches))');
  }
 
  // ฟังก์ชั่นล้างค่า cache
  void _onClearCache(WebViewController controller, BuildContext context) async {
    await controller.clearCache();
    ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
      content: Text("Cache cleared."),
    ));
  }
 
  // ฟังก์ชั่นล้างค่า cookie
  void _onClearCookies(BuildContext context) async {
    final bool hadCookies = await cookieManager.clearCookies();
    String message = 'There were cookies. Now, they are gone!';
    if (!hadCookies) {
      message = 'There are no cookies.';
    }
    ScaffoldMessenger.of(context).showSnackBar(SnackBar(
      content: Text(message),
    ));
  }
 
  // ฟังก์ชั่นแสดงรายการ cookie
  Widget _getCookieList(String cookies) {
    if (cookies.isEmpty || cookies == '""') {
      return Container();
    }
    final List<String> cookieList = cookies.split(';');
    final Iterable<Text> cookieWidgets =
        cookieList.map((String cookie) => Text(cookie));
    return Column(
      mainAxisAlignment: MainAxisAlignment.end,
      mainAxisSize: MainAxisSize.min,
      children: cookieWidgets.toList(),
    );
  }
}
 
    ผลลัพธ์ที่ได้
 

 
 
 
    เนื้อหาเกี่ยวกับการใช้งาน BottomSheet ก็จะขอจบเพียงเท่านี้ รวมถึงเนื้อหาของ WebView ด้วย
สำหรับตอนหน้าจะเป็นอะไร รอติดตาม


   เพิ่มเติมเนื้อหา ครั้งที่ 1 วันที่ 29-07-2024


ดาวน์โหลดโค้ดตัวอย่าง สามารถนำไปประยุกต์ หรือ run ทดสอบได้

http://niik.in/download/flutter/demo_019_29072024_source.rar


กด Like หรือ Share เป็นกำลังใจ ให้มีบทความใหม่ๆ เรื่อยๆ น่ะครับ



อ่านต่อที่บทความ



ทบทวนบทความที่แล้ว









เนื้อหาที่เกี่ยวข้อง









URL สำหรับอ้างอิง





คำแนะนำ และการใช้งาน

สมาชิก กรุณา ล็อกอินเข้าระบบ เพื่อตั้งคำถามใหม่ หรือ ตอบคำถาม สมาชิกใหม่ สมัครสมาชิกได้ที่ สมัครสมาชิก


  • ถาม-ตอบ กรุณา ล็อกอินเข้าระบบ
  • เปลี่ยน


    ( หรือ เข้าใช้งานผ่าน Social Login )







เว็บไซต์ของเราให้บริการเนื้อหาบทความสำหรับนักพัฒนา โดยพึ่งพารายได้เล็กน้อยจากการแสดงโฆษณา โปรดสนับสนุนเว็บไซต์ของเราด้วยการปิดการใช้งานตัวปิดกั้นโฆษณา (Disable Ads Blocker) ขอบคุณครับ