ในตอนที่แล้ว เราได้รู้จัก การใช้งาน Drawer หรือที่เรียกว่า
SideMenu เบื้องต้นมาแล้ว ซึ่งเป็นรูปแบบการ Design หนึ่ง
ที่นิยมใช้งาน และเหมาะกับ App ที่อาจจะมีจำนวนหน้าจัดการต่างๆ
จำนวนมาก ทบทวนเนื้อหาตอนที่แล้วได้ที่
การใช้งาน Drawer กำหนด SideMenu ใน Flutter เบื้องต้น http://niik.in/960
https://www.ninenik.com/content.php?arti_id=960 via @ninenik
ในตอนต่อไปนี้ เราจะมาดู รูปแบบการใช้งานเมนูอีกแบบ ที่น่าจะพบเห็นบ่อย นั่น
ก็คือการกำหนดแถบเมนูที่ส่วนด้านล่างของ App ซึ่งในที่นี้จะใช้งาน BottomNavigationBar
Widget สำหรับกำหนดเมนู เพื่อเชื่อมโยงไปยังหน้า ต่างๆ ใน App
รู้จัก Widget เพิ่มเติม
การใช้งาน BottomNavigationBar Widget
เป็น Material widget ที่อยู่ส่วนด้านล่างของ App สำหรับใช้แสดงข้อมูลประมาณ 3 - 5 หน้า
ประกอบด้วย Icon หรือ Text หรือทั้งสองอย่าง ทำหน้าที่เชื่อมโยงไปยังหน้าต่างๆ ภายใน App
คล้ายกับรูปแบบการทำงานของ Drawer หรือ SideMenu จากบทความที่ผ่านมา แต่ไม่เหมือนกัน
เสียทีเดียว เพราะมีการจัดการกันคนละรูปแบบ โดยเฉพาะในเรื่องของ Navigation Stack
อย่างไรก็ตาม ในบาง App ที่ อาจจะไม่มีหน้าจัดการมากนัก ก็มักใช้รูปแบบการกำหนดเมนู
ในลักษณะนี้แทน SideMenu แต่ถ้า เป็นหน้าจอขนาดใหญ่ๆ การใช้ SideMenu จะเป็นวิธีที่เหมาะสมกว่า
เพราะถ้าเราใช้งาน NavigationBar แถบเมนูแต่ละเมนูอาจจะอยู่ห่างกัน เกิดช่องว่างขึ้นได้
BottomNavigationBar จะใช้งานร่วมกับ Scaffold และกำหนดเป็นค่า argument ให้กับ
bottomNavigationBar property ดังนี้
Scaffold( bottomNavigationBar: // เพิ่ม BottomNavigationBar // เข้าไปใน bottomNavigationBar property ของ scaffold );
จะขอเรียกสั่นๆ ว่า "NavigationBar"
การกำหนดจำนวนของเมนู มีผลต่อรูปแบบการแสดงของสีพื้นหลัง สีเมนูปัจจุบัน สีเมนูที่ไม่ได้เลือก ถ้าไม่กำหนดไว้ก่อน
โดยถ้ามีเมนู ไม่เกิน 3 เมนู ตัว NavigationBar จะมี type เป็นแบบ "fixed" แต่ถ้ามีมากกว่า 3 เมนูขึ้นไป จะมี type เป็น "shifting"
Type แบบ fixed เมนูจะแสดงแบบไม่มีการเคลื่อนไหว ทั้ง Icon และ Text ของเมนูแต่ละ item จะเรียงอยู่ในแนวปกติ
สีพื้นหลังของ NavigationBar จะเป็นสีขาว อิงจากสี ThemeData.canvasColor สีของ item เมนูปัจจุบันจะเป็นสีตาม
ThemeData.primaryColor หรือเป็นสีตาม selectedItemColor ถ้ากำหนด
Type แบบ shifting เมนูจะแสดงเฉพาะ Icon เป็นค่าเริ่มต้น เฉพาะเมนูปัจจุบัน ที่จะแสดง Text ด้วย โดยเมนูที่เป็นปัจจุบัน จะมี
ลักษณะดูเหมือนถูกยกให้สูงขึ้นกว่าเมนูอื่นๆ และมี animation เมื่อเปลี่ยนเมนู ถ้าไม่ได้กำหนดสีของ selectedItemColor สีของทั้งหมด
จะเป็นสีขาว สีพืนหลังของ NavigationBar จะเป็นสีเดียวกับสีพื้นหลังของ item แต่ละเมนู นั่นคือแทบจะไม่เห็นเมนู
ดังนั้นในกรณีที่มากกว่า 3 เมนู เราควรจัดรูปแบบการแสดงโดยกำหนด property ต่างๆ ให้เหมาะสม เช่น ควรกำหนดอย่างน้อย คือสีของ
selectedItemColor และสีของ unselectedItemColor หรือกำหนดโดยใช้สีจาก Theme โดยกำหนดให้กับ unselectedIconTheme
และ selectedIconTheme ก็ได้เหมือนกัน
รูปแบบสี ขนาดข้อความ สีพื้นหลัง เหล่านี้ เราสามารถกำหนดเพิ่มเติมได้ ตาม property ที่มีมาให้ อย่างไรก็ตาม ในกรณีที่เป็น type
แบบ shifting หรือมีมากกว่า 3 เมนูค่าเริ่มต้นสีพื้นหลังของ NavigationBar จะบังคับเน้นไปที่โทนสีขาว จะเปลี่ยนสี backgroundColor
โดยตรงไม่ได้ แต่ก็มีวิธีเปลี่ยนสีโดยใช้ widget อื่นมาครอบและจัดรูปแบบอีกที
ตัวอย่างการกำหนด ส่วนของ NavigationBar แบบ fixed อย่างง่าย และผลลัพธ์ที่ได้
Scaffold( bottomNavigationBar: BottomNavigationBar( items: const <BottomNavigationBarItem>[ BottomNavigationBarItem( icon: Icon(FontAwesomeIcons.home), label: 'Home', ), BottomNavigationBarItem( icon: Icon(FontAwesomeIcons.infoCircle), label: 'About', ), BottomNavigationBarItem( icon: Icon(FontAwesomeIcons.userAlt), label: 'Profile', ), ], currentIndex: _selectedIndex, onTap: _onItemTapped, ), );
ผลลัพธ์ที่ได้
ตัวอย่างการกำหนด ส่วนของ NavigationBar แบบ shifting อย่างง่าย และผลลัพธ์ที่ได้
Scaffold( bottomNavigationBar: BottomNavigationBar( items: const <BottomNavigationBarItem>[ BottomNavigationBarItem( icon: Icon(FontAwesomeIcons.home), label: 'Home', ), BottomNavigationBarItem( icon: Icon(FontAwesomeIcons.infoCircle), label: 'About', ), BottomNavigationBarItem( icon: Icon(FontAwesomeIcons.userAlt), label: 'Profile', ), BottomNavigationBarItem( icon: Icon(FontAwesomeIcons.addressCard), label: 'Contact', ), BottomNavigationBarItem( icon: Icon(FontAwesomeIcons.cog), label: 'Settings', ), ], currentIndex: _selectedIndex, selectedItemColor: Theme.of(context).primaryColor, unselectedItemColor: Colors.grey, onTap: _onItemTapped, ), );
ผลลัพธ์ที่ได้
NavigationBar จะใช้ index ของ Items เมนูที่เลือก เป็นตัวกำหนด และอ้างอิงส่วนของเนื้อหาของเมนูนั้นๆ โดย index จะเริ่มต้นที่ 0
อย่างในโค้ดตัวอย่างข้างต้น เรากำหนด _selectedIndex ให้เป็นค่าของ currentIndex property ของ NavigationBar เพื่อบอกว่า ขณะนี้
อยู่ที่เมนูไหน หรือ index ไหน โดย _selectedIndex จะรับค่า index เมนูที่เลือก จากการงานใน onTap event หรือเมื่อแตะที่เมนูใดๆ
ก็จะเรียกใช้งานฟังก์ชั่น เช่นข้างต้น เรียกใช้ฟังก์ชั่น _onItemTapped ที่มีใช้งาน typedef ฟังก์ชั่น ที่ส่งค่า index ที่เปลี่ยนแปลงเข้าไป
ในฟังก์ชั่น โดยทั้งฟังก์ชั่น _onItemTapped และ _selectedIndex property เป็นส่วนที่เราจะต้องกำหนดเพิ่มเข้าไป จะได้เห็นในโค้ดเต็ม
ทำความเข้าใจเกี่ยวกับ การอ้างอิงอาเรย์ข้อมูลที่แสดง กับอาเรย์เมนู โดยใช้ index ดังนี้
static const List<Widget> _pageWidget = <Widget>[ Text( 'Index 0: Home', ), Text( 'Index 1: Business', ), Text( 'Index 2: School', ), ];
ส่วนแรกคือ อาเรย์ของข้อมูล โดยให้เข้าใจดังนี้ List<Widget> คือกำหนด อาเรย์ของข้อมูลประเภท widget ในตัวแปรชื่อ _pageWidget
เป็นแบบ static และเป็นค่า const (constant) หากใครไม่เข้าใจส่วนนี้แนะนำเนื้อหาเกี่ยวกับ บทความภาษา Dart เพิ่มเติม
ก็จะได้เป็น _pageWidget[0] คือข้อความคำว่า Home ..... _pageWidget[2] คือ School เป็นต้น
ต่อไปส่วนของ อาเรย์ของ เมนู
static const List<BottomNavigationBarItem> _menuBar = <BottomNavigationBarItem>[ BottomNavigationBarItem( icon: Icon(FontAwesomeIcons.home), label: 'Home', ), BottomNavigationBarItem( icon: Icon(FontAwesomeIcons.infoCircle), label: 'Business', ), BottomNavigationBarItem( icon: Icon(FontAwesomeIcons.userAlt), label: 'School', ), ];
เข้าใจในรูปแบบเดียวกัน List<BottomNavigationBarItem> คือกำหนด อาเรย์ของข้อมูลประเภท BottomNavigationBarItem
ในตัวแปรชื่อ _menuBar เป็นแบบ static และเป็นค่า const (constant)
ก็จะได้เป็น _menuBar[0] คือเมนูแรก Home ..... _menuBar[2] คือ เมนูที่ 3 คำว่า School เป็นต้น
จะเห็นว่าทั้งตัวแปรที่เป็นเมนู และตัวแปรที่เป็น ข้อมูล จะมีค่า index ที่เชื่อมโยงกัน ดังนั้นเมื่อเราแตะที่เมนูใดๆ ค่า index ก็จะเปลี่ยน
หากเราแสดงเนื้อหาตามค่า index ข้อมูลของทั้งสองส่วนก็จะสัมพันธ์กัน
ดูคำสั่งการใช้งาน และ property เพิ่มเติมได้ที่ BottomNavigationBar Widget API
ประยุกต์การใช้งาน BottomNavigationBar
ในตอนที่แล้วเราออกแบบ App ในรูปแบบ มี Drawer หรือ SideMenu เนื้อหานี้ เราจะออกแบบหน้า App อีกรูปแบบ สำหรับ App ที่อาจจะ
มีเมนูไม่มากนัก โดยจะใช้งาน BottomNavigationBar แสดงข้อมูลหน้าต่างๆ เราจะสร้างหน้าใหม่เพิ่มอีกหน้าชื่อว่า launcher.dart เป็นหน้า
ที่มีการใช้งาน scaffold และกำหนด bottomNavigationBar กับ body แต่ไม่ได้กำหนด AppBar
โดยเมื่อเปิด App มาจะมาที่หน้า Launcher ก่อน โดย หน้า Launcher จะเป็นหน้าที่แสดง NavigationBar และแสดงหน้า Home เป็นหน้า
แรกหรือเป็นเมนูแรกที่ Active และเมื่อเราเปลี่ยนเมนู ก็จะไปดึงหน้าต่างๆ ตามที่กำหนดใน อาเรย์ข้อมูล ที่สัมพันธ์กับเมนูมาแสดง เราจะได้
โครงสร้างไฟล์ทั้งหมดดังนี้
จะมีไฟล์ที่เกี่ยวข้องอยู่ 3 ไฟล์ คือ ไฟล์ main.dart ที่ปรับการเรียก route ไปเฉพาะหน้าแรก ที่เป็น Launcher แทน Home
ไฟล์ launcher.dart เป็นหน้าหลัก มีการใช้งาน และกำหนด NavigationBar และสุดท้ายไฟล์ home.dart เนื่องจากเรากำหนด
routeName ให้ Launcher เป็น "/" แล้ว ดังนั้นหน้าจะแค่เปลี่ยนเป็น "/home" แทน ส่วนไฟล์อื่นๆ เหมือนเดิมจากตอนที่แล้ว
ไฟล์ launcher.dart
import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'home.dart'; import 'contact.dart'; import 'profile.dart'; import 'about.dart'; import 'settings.dart'; class Launcher extends StatefulWidget { static const routeName = '/'; const Launcher({Key? key}) : super(key: key); @override State<StatefulWidget> createState() { return _LauncherState(); } } class _LauncherState extends State<Launcher> { int _selectedIndex = 0; final List<Widget> _pageWidget = <Widget>[ const Home(), const About(), const Profile(), const Contact(), const Settings(), ]; final List<BottomNavigationBarItem> _menuBar = <BottomNavigationBarItem>[ BottomNavigationBarItem( icon: Icon(FontAwesomeIcons.home), label: 'Home', ), BottomNavigationBarItem( icon: Icon(FontAwesomeIcons.infoCircle), label: 'About', ), BottomNavigationBarItem( icon: Icon(FontAwesomeIcons.userAlt), label: 'Profile', ), BottomNavigationBarItem( icon: Icon(FontAwesomeIcons.addressCard), label: 'Contact', ), BottomNavigationBarItem( icon: Icon(FontAwesomeIcons.cog), label: 'Settings', ), ]; void _onItemTapped(int index) { setState(() { _selectedIndex = index; }); } @override Widget build(BuildContext context) { return Scaffold( body: _pageWidget.elementAt(_selectedIndex), bottomNavigationBar: BottomNavigationBar( items: _menuBar, currentIndex: _selectedIndex, selectedItemColor: Theme .of(context) .primaryColor, unselectedItemColor: Colors.grey, onTap: _onItemTapped, ), ); } }
ไฟล์ main.dart
import 'package:flutter/material.dart'; import './screens/launcher.dart'; void main() { runApp(const MyApp());} // ส่วนของ Stateless widget class MyApp extends StatelessWidget{ const MyApp({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return MaterialApp( theme: ThemeData( colorScheme: ColorScheme.fromSwatch( primarySwatch: Colors.blue, ).copyWith( secondary: Colors.purple, ), ), title: 'First Flutter App', initialRoute: '/', // สามารถใช้ home แทนได้ routes: { Launcher.routeName: (context) => Launcher(), }, ); } }
ไฟล์ home.dart
import 'package:flutter/material.dart'; class Home extends StatefulWidget { static const routeName = '/'; const Home({Key? key}) : super(key: key); @override State<StatefulWidget> createState() { return _HomeState(); } } class _HomeState extends State<Home> { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Home'), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text('Home Screen'), ], ) ), ); } }
ผลลัพธฺ์ที่ได้
ในรูปแบบการใช้งานนี้ เราไม่ได้ใช้งาน Drawer หรือ SideMenu ร่วมด้วย แต่หากจะประยุกต์ใช้งานด้วยกันก็ได้ ซึ่งอาจจะต้องทำความ
เข้าใจเพิ่มเติมก่อนประยุกต์ใช้งานร่วมกัน เพราะการใช้งาน SideMenu จะมองที่การวางซ้อนทับกันของหน้าข้อมูล แต่ในส่วนของ การใช้งาน
NavigationBar จะเหมือนกับเน้นไปที่ Navigation Stack แรก แล้วข้อมูในหน้าอื่นๆ ก็เหมือนมาแทรกอยู่ใน Widget ของ Stack แรก อย่าง
ในตัวอย่างของเรา เราเปิด App ขึ้นมาโดยใช้ Launcher เป็น หน้าหลัก หรือก็คือ Stack แรก หรือชั้นแรก และเมื่อเลือกที่เมนู ก็เป็นแค่
การเปลี่ยนในส่วนของ body ที่ไปดึงข้อมูลจากหน้าอื่นๆ มาใช้งาน หรือมาแทรกเข้ามา ไม่ได้เป็นการเปิดหน้าใหม่ซ้อนทับเข้ามา เหมือนการ
ใช้งาน SideMenu ดังนั้นจึงให้เข้าใจความแตกต่างส่วนนี้ เป็นแนวทาง หากต้องการประยุกต์เพิ่มเติม
ในตอนต่อๆ ไป เราอาจจะเลือกรูปแบบจากทั้งสองการใช้งาน มาเป็นตัวอย่าง ทั้งแบบ SideMenu โดยใช้ Drawer และแบบที่มีการใช้งาน
NavigationBar ประกอบการอธิบายส่วนอืนๆ ต่อ ไป และเนื้อหาตอนหน้าจะเป็นอะไร รอติดตาม