การกำหนดและใช้งาน BottomSheet ใน Flutter
เขียนเมื่อ 2 ปีก่อน โดย Ninenik Narkdeedraggablescrollablesheet gesturedetector bottomsheet flutter
คำสั่ง การ กำหนด รูปแบบ ตัวอย่าง เทคนิค ลูกเล่น การประยุกต์ การใช้งาน เกี่ยวกับ draggablescrollablesheet gesturedetector bottomsheet flutter
ไปที่
Copy








เนื้อหาต่อไปนี้จะมาดูเกี่ยวกับการใช้งาน Bottom Sheet ส่วน
ที่ใช้สำหรับแสดงเนื้อหาหรือรายละเอียด หรือปุ่มคำสั่งเพิ่มเติม
โดยจะเป็นส่วนที่เลื่อนมาจากขอบด้านล่างของหน้าจอ มีทั้งแบบ
แสดงถาวรและแบบแสดงแบบ modal ชั่วคราวคล้าย popup ก็ได้
ขึ้นกับการปรับใช้งาน เราจะใช้เนื้อหาต่อเนื่องจากการใช้งาน WebView
ในบทความตอนที่แล้ว เดิมที่เราเพิ่มส่วนของ PopupMenuButton เราจะ
เปลี่ยนมาเป็นปุ่มเมนูเพิ่มเติม แสดงในส่วนของ Bottom Sheet แทน
ทบทวนเนื้อหาตอนที่แล้วได้ที่บทความ
การกำหนดและใช้งาน PopupMenuButton ใน Flutter http://niik.in/1044
https://www.ninenik.com/content.php?arti_id=1044 via @ninenik
*เนื้อหานี้ใช้เนื้อหาต่อเนื่องจากบทความ 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 เป็นกำลังใจ ให้มีบทความใหม่ๆ เรื่อยๆ น่ะครับ

อ่านต่อที่บทความ
-
05 Nov2021การใช้งาน Provider จัดการข้อมูล App State ใน Flutter อ่าน 11,373
เนื้อหาตอนต่อไปนี้เราจะมาดูเกี่ยวกับการใช้งาน provider จ้ดการข้อมูล app s
เนื้อหาที่เกี่ยวข้อง
-
18 Dec2019การใช้งาน BottomNavigationBar ใน Flutter เบื้องต้น อ่าน 18,300
ในตอนที่แล้ว เราได้รู้จัก การใช้งาน Drawer หรือที่เรียกว่า SideMenu เบื้อ
-
21 Oct2021แสดงข้อความด้านล่างหน้าจอด้วย SnackBar ใน flutter อ่าน 2,792
SnackBar เป็น widget ที่เราสามารถใช้สำหรับแสดง popup ข้อความ แ
-
22 Oct2021การกำหนดเพิ่ม Fonts เพื่อใช้งานใน App ของ Flutter อ่าน 4,013
เนื้อหานี้จะมาแนะนำแนวทางการเพิ่มฟอนท์สำหรับ ใช้งานใน app กรณีเราอยากได้ร
-
23 Oct2021จัดการ Tab ด้วย TabController ใน Flutter อ่าน 7,733
เนื้อหานี้มาดูเกี่ยวกับการใช้งาน tab ใน flutter ซึ่งเป็นรูปแบบ การใช้งานท
-
24 Oct2021การใช้งาน FutureBuilder ที่เป็น Async widgets ใน Flutter อ่าน 5,519
เนื้อหาตอนต่อไปนี้ จะมาดูเกี่ยวกับการใช้งาน async widgets ที่ชื่อว่า Futu
-
25 Oct2021การใช้งาน StreamBuilder จัดการข้อมูล Stream ใน Flutter อ่าน 5,045
ในตอนที่แล้วเรารู้จักกับ FutureBuilder ซึ่งเป็น async widgets หนึ่งใน Flu
-
26 Oct2021การใช้งาน Http ดึงข้อมูลจาก Server มาแสดงใน Flutter อ่าน 9,794
เนื้อหาตอนต่อไปนี้ เราจะมาดูเกี่ยวกับวิธีการดึงข้อมูลจาก Server ที่เป็นข้
-
27 Oct2021การใช้งาน Card Widget ร่วมกับ ListView ใน Flutter อ่าน 6,668
เนื้อหาตอนต่อไปนี้ จะมาดูเกี่ยวกับ widget เล็กๆ ที่จะมาช่วย ให้การแสดงผลใ
-
28 Oct2021การใช้งาน RefreshIndicator ปัดเพื่อรีเฟรชข้อมูล ใน Flutter อ่าน 4,377
ต่อเนื่องจากเนื้อหาตอนที่แล้ว เนื้อหานี้เราจะมาดูเกี่ยวกับ widget ที่ให้เ
-
29 Oct2021จัดการข้อมูล Model และแนวทางการนำมาใช้งาน ใน Flutter อ่าน 6,048
เนื้อหาตอนต่อไปนี้จะประยุกต์ต่อเนื่องจากตอนที่แล้ว เราจะลองใช้ ข้อมูลทดสอ
-
30 Oct2021การใช้งาน GridView widget ใน Flutter อ่าน 9,858
เนื้อหาตอนต่อไปนี้ จะมาดูเกี่ยวกับการใช้งาน GridView เป็นเนื้อหาต่อ
-
31 Oct2021การใช้งาน WebView แสดงเว็บไซต์ ใน Flutter อ่าน 7,136
เนื้อหานี้จะมาดูเกี่ยวกับการใช้งาน WebView widget ซึ่งจะเป็น package ที่เ
-
01 Nov2021การกำหนดและใช้งาน PopupMenuButton ใน Flutter อ่าน 3,254
เนื้อหาตอนต่อไปนี้จะมาดู Widget เล็กๆ ที่มีรูปแบบการใช้งาน ง่ายๆ ที่เรียก
-
กำลังอ่านเนื้อหานี้อยู่02 Nov2021การกำหนดและใช้งาน BottomSheet ใน Flutter อ่าน 4,289
เนื้อหาต่อไปนี้จะมาดูเกี่ยวกับการใช้งาน Bottom Sheet ส่วน ที่ใช้สำหรับแสด
-
05 Nov2021การใช้งาน Provider จัดการข้อมูล App State ใน Flutter อ่าน 11,373
เนื้อหาตอนต่อไปนี้เราจะมาดูเกี่ยวกับการใช้งาน provider จ้ดการข้อมูล app s
URL สำหรับอ้างอิง
Top
Copy
ขอบคุณทุกการสนับสนุน
![]()