web analytics

ทำ Custom Segmented Control ใน Flutter ตอนที่ 2

สวัสดีครับ มาต่อตอนที่ 2 กันเรื่อง Segmented Control ในตอนที่แล้ว เราได้ลองเล่น การ Custom Segmented Control และการใช้งานร่วมกับ PageView ในบล็อกนี้ จะเป็นการเพิ่มลูกเล่นให้กับ Segmented Control โดยใช้ Custom Painter ครับ มาดูกัน

ตอนที่แล้ว ถ้ายังไม่ได้อ่าน แนะนำให้อ่านก่อนนะครับ

หากยังไม่ได้อ่านเรื่อง Custom Painter ก็อ่านได้ที่บล็อกของผม ตามลิงค์ด้านล่างนี้เลย

ปัญหา

มาดู Segmented Control ที่เราทำไปก่อนหน้านี้ แม้มันจะสามารถทำงานได้ดี แต่ว่าสิ่งที่ขาดไป ก็คือมันยังไม่มี Animation กล่าวคือ กดแล้วก็เปลี่ยน segment ในทันที ซึ่งถ้าเอามาใช้สำหรับเป็น UI เลือก choice ธรรมดาๆก็น่าจะยังพอใช้งานได้ แต่ถ้าเอามาใช้เป็น Tab ของ PageView อาจจะดูแข็งๆไปสักหน่อย

สักเกตอีกจุด คือการลาก PageView ระหว่างหน้า segment จะแสดงสลับไปสลับมา ซึ่งมันจะดูลื่นใหลกว่าถ้ามัน animate ตามจังหวะที่เราเลื่อน page

เริ่มต้น

เผื่อคนที่ยังมองไม่ออกว่ามันจะออกมาเป็นอย่างไร ด้านล่างนี้คือ สิ่งที่เรากำลังจะทำกัน คือทำให้ Segmented control มัน animate เลื่อนใหลได้นั่นเอง

อธิบายก่อนว่า ส่วนที่เป็นกรอบด้านนอกกับ ส่วนที่ทำ custom painter เพื่อ animate นั้นจะทำแยกกัน

เริ่มจากการสร้างตัวกรอบของ segmented control ก่อน ซึ่งในที่นี้ก็คือ Container ธรรมดา กำหนดขนาดเอาไว้ ขนาดที่ผมกำหนด คือขนาด 300×60


  @override
  Widget build(BuildContext context) {
    return Scaffold(
        ...
        body: Container(
          child: Column(
            children: [
              Container(
                padding: EdgeInsets.all(4),
                margin: EdgeInsets.only(top: 16),
                width: 300,
                height: 60,
                decoration: BoxDecoration(color: Colors.pink[50], borderRadius: BorderRadius.circular(16)),
              ),
       ...

ได้แบบนี้

สร้าง Custom Painter

จากนั้นสร้าง class ที่ extends CustomPainter ของเราเอง ในที่นี้ผมใช้ชื่อว่า PageTabIndicationPainter โดยใน paint() เราจะวาด RRect ความกว้าง 1/3 เพราะว่า segment ของเราแบ่งเป็น 3 ส่วน


class PageTabIndicationPainter extends CustomPainter {

  PageTabIndicationPainter() : super();
  @override
  void paint(Canvas canvas, Size size) {

    Paint painterIndicator = Paint()
      ..color = Colors.pink[400]
      ..style = PaintingStyle.fill;
    canvas.drawRRect(
         RRect.fromRectAndRadius(
               Rect.fromLTWH(0, 0, (size.width / 3),size.height), 
               Radius.circular(16)), painterIndicator);
  }

  @override
  bool shouldRepaint(TabIndicationPainter oldDelegate) => true;
}

จากนั้นเราก็เพิ่ม CustomPaint ในตัวกรอบ Container ของเราและกำหนด PageTabIndicationPainter ให้มันด้วย และอย่าลืมกำหนดขนาดของ CustomPaint โดยกำหนด child ให้เป็นขนาดที่ต้องการ ในที่นี้ผมใช้ 300×60

              Container(
                padding: EdgeInsets.all(4),
                margin: EdgeInsets.only(top: 16),
                decoration: BoxDecoration(
                    color: Colors.pink[50], 
                    borderRadius: BorderRadius.circular(16)),
                child: CustomPaint(
                          painter: PageTabIndicationPainter (),
                          child: Container(width:300,height:60)),
              )

ได้แล้ว Segmented Control จาก การวาด Custom Painter ของเรา แต่ว่ามันจะยังขยับไม่ได้

ทำ Animate กับ PageView

ต่อมา คือทำส่วนที่ให้ segment เลื่อนตาม page view วิธีการก็ไม่ยากเลย ก่อนอื่นเราจะต้องหาค่า offset ของ page view ก่อน โดยจะมีค่า 0 ถึง 1 วิธีการหาค่า offset ก็ง่ายๆคือ คือ ความกว้างของหน้าก่อนหน้านี้ทั้งหมด หารด้วยความกว้างทั้งหมด ดังนั้น เมื่อเราเลื่อน segment ไปอันขวาสุด ค่า offset ที่ได้ก็คือ 2/3 นั่นเอง และเราก็นำ offset นี้ไปทำ animate นั่นเอง

โดยค่า offset นี้เราจะได้จาก PageController ซึ่งเป็นตัวควบคุมการทำงานของ PageView ดังนั้นเราต้องเพิ่ม PageController เข้ามาใน class PageTabIndicationPainter ของเรา

class PageTabIndicationPainter extends CustomPainter {

  final PageController pageController;

  PageTabIndicationPainter({this.pageController}) : super(repaint: pageController);

   ...

วิธีการหา offset ก็จะประมาณนี้

    double pageOffset = 0;
    if (pageController.hasClients) {
      final position = pageController.position;
      double fullExtent = (position.maxScrollExtent - position.minScrollExtent + position.viewportDimension);
      pageOffset = position.extentBefore / fullExtent;
    }

จากนั้นส่วน drawRRect เราจะปรับค่า left ให้วิ่งไปตาม offset * size.width นั่นเอง พร้อมกับสั่ง canvas.translate

class PageTabIndicationPainterextends CustomPainter {

  ...

  @override
  void paint(Canvas canvas, Size size) {
    double pageOffset = 0;
    if (pageController.hasClients) {
      final position = pageController.position;
      double fullExtent = (position.maxScrollExtent - position.minScrollExtent + position.viewportDimension);
      pageOffset = position.extentBefore / fullExtent;
    }

    Paint painterIndicator = Paint()
      ..color = Colors.pink[400]
      ..style = PaintingStyle.fill;
    canvas.drawRRect(RRect.fromRectAndRadius(Rect.fromLTWH(pageOffset * size.width ,0, (size.width / 3), size.height), Radius.circular(16)), painterIndicator);
    canvas.translate(size.width * pageOffset , 0.0);
  }

  @override
  bool shouldRepaint(TabIndicationPainter oldDelegate) => true;
}

แล้วก็อย่าลืมส่ง pageController ให้ PageTabIndicationPainter ด้วย

              Container(
                padding: EdgeInsets.all(4),
                margin: EdgeInsets.only(top: 16),
                decoration: BoxDecoration(color: Colors.pink[50], borderRadius: BorderRadius.circular(16)),
                child: CustomPaint(painter: 
                       PageTabIndicationPainter (pageController: _pageController),child: Container(width:300,height:60),),
              )

ลองรัน จะเห็นว่าได้ Segmented Control ที่มี animation แล้ว

เพิ่ม Text

ยังขาด Text แสดงเป็นหัวข้อในแต่ละ Segment ก็ไม่ยากเลย เพียงเพิ่มใน child ของ CustomPaint เท่านั้น ในที่นี้ผมใช้ Row และ Expand 3 ตัว และแสดงสีของ Text ตาม index

              Container(
                ...
                child: CustomPaint(
                  painter: PageTabIndicationPainter(pageController: _pageController),
                  child: Container(
                    width: 300,
                    height: 60,
                    child: Row(children: [
                      Expanded(
                          child: Text(
                        "One",
                        style: TextStyle(fontSize: 20, color: _indexSegmentSelected == 0 ? Colors.white : Colors.pink[400]),
                        textAlign: TextAlign.center,
                      )),
                      Expanded(
                          child: Text(
                        "Two", 
                        style: TextStyle(fontSize: 20, color: _indexSegmentSelected == 1 ? Colors.white : Colors.pink[400]), textAlign: TextAlign.center)),
                      Expanded(
                          child: Text(
                        "Three", 
                        style: TextStyle(fontSize: 20, color: _indexSegmentSelected == 2 ? Colors.white : Colors.pink[400]), textAlign: TextAlign.center)),
                    ]),
                  ),
                ),
              )

ได้แล้ว

แต่ทว่าเรา เรายังไม่ได้เพิ่ม onTap ให้กับ Segment ทำให้ยังกดที่ segment เพื่อเปลี่ยนหน้าไม่ได้

แบบง่ายที่สุด ก็คือเพิ่ม GestureDetector onTap ให้กับทุก Container ใน Expand แล้วสั่ง เปลี่ยน page ไปตาม index

                child: CustomPaint(
                  painter: PageTabIndicationPainter(pageController: _pageController),
                  child: Container(
                    width: 300,
                    height: 60,
                    child: Row(children: [
                      Expanded(
                          child: GestureDetector(
                                     onTap: () {
                                       changePage(0);
                                     },
                        child: Text("One",
                          style: TextStyle(
                                 fontSize: 20, 
                                 color: _indexSegmentSelected == 0 ? Colors.white : Colors.pink[400]),
                                 textAlign: TextAlign.center,
                        ),
                      )),

                      ...

                    ]),
                  ),
                ),

เรียบร้อยครับ ได้ Segmented Control ตามต้องการ ง่ายๆแบบนี้เลย

สรุป

จากการทำ Segmented Control เราได้ลองทำ ทั้งแบบ Widget ง่ายๆ กับแบบที่ใช้ Custom Painter เพื่อเพิ่ม animation ลูกเล่นให้กับ Segment แล้วก็ลองนำมาใช้ร่วมกับ PageView หวังว่าบล็อกของผมจะมีประโยชน์กับผู้อ่านนะครับ

สำหรับเรื่องถัดไปนั้นจะเป็นเรื่องอะไรฝากติดตามด้วยนะครับ ขอบพระคุณครับ สวัสดีครับ

Source code สามารถดูได้ที่ DartPad ครับ
https://dartpad.dev/86de13f48cd6a2f418a792ccfb3f3971