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

บทความใหม่ เดือนนี้ โดย Ninenik Narkdee
gesturedetector draggablescrollablesheet flutter bottomsheet

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



เนื้อหาต่อไปนี้จะมาดูเกี่ยวกับการใช้งาน 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 ซึ่งถ้าเราเรียกใช้งาน
ภายใน Scaffo เลยก็สามารถใช้เป็นแบบด้านล่างได้เลย แต่ปกติ เราจะต้องแยกสร้างเป็นฟังก์ชั่น เพราะว่า เรา
ต้องสร้างส่วนของเนื้อหา 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> {
    // กำหนดตัวแปร controler สำหรับควบคุมการทำงาน
    final Completer<WebViewController> _controller =
        Completer<WebViewController>(); 

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

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

    @override
    void initState() {
      super.initState();
      // กำหนดการใช้งาน ที่รับการใช้งาน keyboard สำหรับ android
      if (Platform.isAndroid) WebView.platform = SurfaceAndroidWebView();
    }

    @override
    Widget build(BuildContext context) {
        // รับค่า url ที่ส่งมาใน arguments
        final url = ModalRoute.of(context)!.settings.arguments as String;

        void _toggleShow(value){
          setState(() {
            _isBottomSheetShow = value ? false : true ;
          });
        }

        return Scaffold(
            appBar: AppBar(
                title: Text('Articles'),
                actions: <Widget>[ // สร้างอาเรย์หรือ List ของปุ่มใน action
                  NavigationControls(_controller.future),
                  IconButton(
                    onPressed: (){
                      setState(() {
                        _toggleBottomSheet(_isBottomSheetShow);
                      });
                    }, 
                    icon: const Icon(Icons.more_vert),
                  ),
                ],
            ),
            body: Builder(builder: (BuildContext context) {
              // ใช้งาน WebView กำหนดค่าเบื้องต้น 
              return WebView(
                initialUrl: url, // ใช้ url จากหน้าที่ส่งมา 
                javascriptMode: JavascriptMode.unrestricted, // ใช้งาน JavaScript ได้
                onWebViewCreated: (WebViewController webViewController) { // เมื่อสร้าง webviewเสร็จ
                  _controller.complete(webViewController); // การใช้งาน async เมื่อ controller พร้อมใช้งาน
                },
                javascriptChannels: <JavascriptChannel>{ // กำหนดการใช้งาน javascriptChannels
                  _toasterJavascriptChannel(context),
                },                
                gestureNavigationEnabled: true, // กำหนดให้รองรับ gusture ต่างๆ เช่นการปัด เพื่อเลื่อนไปหน้าต่างอื่น
              );
            }),
            floatingActionButton: scrollTopButton(),
            bottomSheet: _showBottomSheet(),
        );
    }

    // สร้าง JavascriptChannel สำหรับรับค่าข้อมูลที่ส่งผ่านทาง JavaScript
    JavascriptChannel _toasterJavascriptChannel(BuildContext context) {
      return JavascriptChannel(
        name: 'Toaster', // กำหนดชื่อที่จะใช้
        onMessageReceived: (JavascriptMessage message) {
          // ในที่นี้เมื่อได้ค่ามาแล้ว จะแสดงข้อความด้วย SnackBar
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(content: Text(message.message)),
          );
      });
    }    

    // สร้างฟังก์ชั่นสำหรับเรียกใช้งาน
    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 FutureBuilder<WebViewController>(
          future: _controller.future, // ใช้งาน controller future ได้เลย
          builder: (BuildContext context,
              AsyncSnapshot<WebViewController> controller) {
            if (controller.hasData) { // มีข้อมูล
              return FloatingActionButton( // คืนค่าเป็นปุ่มลูกศรเลื่อนบน
                onPressed: () async { // ถ้ากด
                  // เรียกคำสั่ง javascript  เลื่อน scroll ไปด้านบนสุด
                  await controller.data!.evaluateJavascript('window.scrollTo(0, 0);');
                },
                child: const Icon(Icons.arrow_upward),
              );
            }
            return Container(); // ยังไม่มีข้อมูล คืนค่า container ว่างไปแสดงที่ปุ่ม
          });
    }
    

}


// สร้าง widget สำหรับทำปุ่มควบคุม เช่น ก่อนหน้า ย้อนหลัง รีเฟรช
class NavigationControls extends StatelessWidget {
  // รับค่าข้อมูล Future WebViewController ผ่าน parameter เข้ามาใช้งาน
  const NavigationControls(this._webViewControllerFuture);

  // กำหนดตัวแปรสำหรับรับค่าและเรียกใช้งาน WebViewController ใน widget นี้
  final Future<WebViewController> _webViewControllerFuture;

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<WebViewController>( // ดูเพิ่มเติมได้ที่ http://niik.in/1036
      future: _webViewControllerFuture,
      builder: (BuildContext context, AsyncSnapshot<WebViewController> snapshot) {
        // กำหนดตัวแปรเงื่อนไข controller พร้อมทำงานหรือ webview พร้อมทำงาน
        final bool webViewReady =
        snapshot.connectionState == ConnectionState.done;
        // กำนหดตัวแปร controller เพื่อควบคุม webview
        final WebViewController? controller = snapshot.data;
        // สร้าง widget ปุมต่างๆ ไปใช้งาน
        return Row(
          children: <Widget>[
            IconButton(
              icon: const Icon(Icons.arrow_back_ios),
              onPressed: !webViewReady // ไม่พร้อมทำงาน
                  ? null // คืนค่า null 
                  : () async { // พร้อมทำงาน
                      if (await controller!.canGoBack()) { //เช็คย้อนหลังได้ไหม
                        await controller.goBack(); // ถ้าได้ ก็ย้อนหลัง
                      } else {
                        // ย้อนหลังไม่ได้ แสดงข้อความแจ้ง
                        ScaffoldMessenger.of(context).showSnackBar(
                          const SnackBar(content: Text("No back history item")),
                        );
                        return;
                      }
                    },
            ),
            IconButton(
              icon: const Icon(Icons.arrow_forward_ios),
              onPressed: !webViewReady // ไม่พร้อมทำงาน
                  ? null // คืนค่า null 
                  : () async { // พร้อมทำงาน
                      if (await controller!.canGoForward()) { // เช็คไปหน้าได้ไหม
                        await controller.goForward(); // ถ้าได้ ก็ไปหน้าถัดไป
                      } else {
                        // ถ้าไปหน้าไม่ได้ แสดงข้อความแจ้ง
                        ScaffoldMessenger.of(context).showSnackBar(
                          const SnackBar(
                              content: Text("No forward history item")),
                        );
                        return;
                      }
                    },
            ),
            IconButton(
              icon: const Icon(Icons.replay),
              onPressed: !webViewReady // ไม่พร้อมทำงาน
                  ? null // คืนค่า null 
                  : () { // พร้อมทำงาน
                      controller!.reload(); // โหลดหน้าเว็บเพจใหม่อีกครั้ง
                    },
            ),
          ],
        );
      },
    );
  }
}
 
    ผลลัพธ์ที่ได้
 

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


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



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









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









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











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