web analytics

Material Motion ใน Flutter ตอนที่ 1

สวัสดีครับ วันก่อนผมได้เขียนบล็อกสรุปเรื่อง Flutter 1.17 stable มีอะไรใหม่ๆไป หนึ่งในหัวข้อนั้นที่น่าสนใจคือ เรื่อง Material Design และภายในนั้นก็มีเรื่อง Motion ที่ทีมงานได้เพิ่ม package animations ให้กับ Flutter สามารถใช้ Material Motion ได้ เหมือนกับใน Android native ดังนั้นในบล็อกนี้ ผมเลยถือโอกาสลองเล่น Material Motion ใน Flutter และจะสรุปเนื้อหาเรื่องนี้ครับ มันจะเป็นยังไงบ้างนะ มาลองเล่นกันครับ

บล็อกตอนก่อนหน้านี้ สรุป Flutter 1.17

บล็อกนี้เป็นข้อมูล Material Design 2020 นะ ในอนาคตอาจมีการเปลี่ยนแปลง

Material Motion คืออะไร

Motion คือ รูปแบบต่างๆของ transition ที่จะเกิดระหว่าง component (ใน Flutter เรียกว่า widget ดังนั้นผมจะขอเรียกว่า widget แทนนะ) หรือหน้าจอทั้งหน้าก็ได้ ซึ่งการทำ Motion จะช่วยให้เกิดความสัมพันธ์ระหว่างหน้าจอ ช่วยให้ user รู้สึกถึงความลื่นไหลของแอปมากขึ้น โดย Google ได้ออกแบบ Motion มาจำนวนนึงให้กับ Material Design มันเลยถูกเรียกว่า Material Motion นั่นเอง

โดย Material Motion มี transition patterns 4 แบบ (ในตอนนี้) คือ

  • Container transform
  • Shared axis
  • Fade through
  • Fade

Container transform

Container transform จะเป็นการ transition ระหว่าง widget 2 ตัว สิ่งที่สำคัญคือ ต้องทำให้ widget ที่ user กำลังสนใจอยู่ในตอนนี้กับ widget เป้าหมาย ดูเหมือนว่าถูกเชื่อมกันอยู่ หรือเป็นตัวเดียวกัน ตัวอย่างที่ชัดเจนคือการใช้กับ card หรือ container ที่อยู่คงที่ และเมื่อกดแล้วก็จะขยายเพื่อแสดงรายละเอียด

ตัวอย่างการใช้งานจากเว็บ Material Design
เช่น การใช้กับรูปแบบ card, list, floating action button แสดงหน้าจอแบบเต็มจอ

หรือจะใช้กับรูปแบบ dialog, bottom sheet, pop-up

เริ่มต้นใน Flutter

เพิ่ม dependencies ที่ชื่อว่า animations ใน pubspec.yaml

dependencies:
  flutter:
    sdk: flutter
  ...
  animations: 1.0.0+5  

มาลองใช้งาน Container transform ใน Flutter กัน ก่อนอื่น ผมสร้างหน้าตัวอย่างขึ้นมาอันหนึ่ง มีข้อมูลสมมุติ item 1 อัน


class MyHome extends StatefulWidget {
  @override
  _MyHomeState createState() => _MyHomeState();
}

class _MyHomeState extends State<MyHome> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("Material Motion"),
        ),
        backgroundColor: Colors.grey[300],
        body: ListView(
          children: <Widget>[
            GestureDetector(
              onTap: () {
                Navigator.push(context, MaterialPageRoute(builder: (_) => DetailScreen()));
              },
              child: Container(
                  color: Colors.white,
                  padding: EdgeInsets.all(16),
                  child: Row(
                    children: <Widget>[
                      Image.network(
                        "https://storage.googleapis.com/spec-host-backup/mio-develop%2Fassets%2F1dKRB-OZstott5AMjlOqXYRmCmbf4La0R%2Fdevelop-flutter-1x1-small.png",
                        width: 80,
                        height: 80,
                        fit: BoxFit.cover,
                      ),
                      SizedBox(
                        width: 16,
                      ),
                      Expanded(
                        child: Column(
                          crossAxisAlignment: CrossAxisAlignment.start,
                          children: <Widget>[
                            Text(
                              "Lorem ipsum dolor",
                              style: TextStyle(fontSize: 20),
                            ),
                            Text(
                              "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore.",
                              style: TextStyle(fontSize: 14, color: Colors.grey[500]),
                            )
                          ],
                        ),
                      ),
                    ],
                  )),
            ),
          ],
        ));
  }
}

จากนั้นสร้างหน้าที่สองเป็นหน้าแสดงรายละเอียด

class DetailScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("Material Motion"),
        ),
        backgroundColor: Colors.grey[300],
        body: ListView(
          children: <Widget>[
            Container(
                color: Colors.white,
                child: Column(
                  children: <Widget>[
                    Image.network(
                      "https://storage.googleapis.com/spec-host-backup/mio-develop%2Fassets%2F1dKRB-OZstott5AMjlOqXYRmCmbf4La0R%2Fdevelop-flutter-1x1-small.png",
                      height: 200,
                      width: MediaQuery.of(context).size.width,
                      fit: BoxFit.cover,
                    ),
                    SizedBox(
                      width: 16,
                    ),
                    Container(
                      padding: EdgeInsets.all(16),
                      child: Column(
                        children: <Widget>[
                          Text(
                            "Lorem ipsum dolor",
                            style: TextStyle(fontSize: 28),
                          ),
                          SizedBox(
                            height: 6,
                          ),
                          Text(
                            "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip",
                            style: TextStyle(fontSize: 14, color: Colors.grey[500]),
                          )
                        ],
                      ),
                    ),
                  ],
                )),
          ],
        ));
  }
}

ก็จะได้ประมาณนี้

การ navigate หน้าจอโดยปกติแล้ว วิธีง่ายๆก็คือเราจะเพิ่ม GestureDetector แล้วเรียก Navigator.push ใน onTap เพื่อให้กดแล้ว navigate ไปที่หน้าที่สอง และ transition ที่ได้คือ Fade transition ที่เรามักเห็นใน android แบบปกตินั่นเอง

            GestureDetector(
              onTap: () {
                Navigator.push(context, MaterialPageRoute(builder: (_) => DetailScreen()));
              },
              ...

แต่ว่าการใช้ Material Motion จะแตกต่างจากเดิม เพราะเราจะใช้ widget ที่ Matetrial ทำไว้ให้แทนการใช้ Navigator.push

วิธีการคือใช้ widget ที่ชื่อว่า OpenContainer โดยกำหนด
transitionType = รูปแบบที่ต้องการ fade หรือ through fade (เดี๋ยวอธิบายเพิ่มเติมว่าต่างกันอย่างไร)
transitionDuration = ระยะเวลา
openBuilder = หน้าที่สอง
closeBuilder = widget ปัจจุบัน

ตัวอย่างนี้ ผมจะใช้ OpenContainer ใส่ใน ListView แทน แล้วก็นำ code ส่วนที่สร้าง item widget แยกออกมาเป็น method ชื่อว่า buildItem()

class _MyHomeState extends State<MyHome> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        ...
        body: ListView(
          children: <Widget>[
            OpenContainer(
              transitionType: ContainerTransitionType.fade,
              transitionDuration: Duration(seconds: 2),
              openBuilder: (context, action) {
                return DetailScreen();
              },
              closedBuilder: (context, action) {
                return buildItem(); 
              },
            )
          ],
        ));
  }

เพียงเท่านี้ เราก็จะได้ Motion แบบ Container transform แล้วละ ง่ายๆแบบนี้เลยนะ


Container transition type

มี container transition type ให้เลือก 2 แบบ สำหรับ Container transform คือ Fade และ Through fade ลองมาดูนิยามกันก่อนนะ

Fade จะค่อยๆแสดง widget ที่สองพร้อมกับค่อยๆทับ widget เดิม
Fade Through จะ fade widget แรกออกก่อน แล้วค่อย fade widget ใหม่เข้ามา

ลองดู animation ได้ที่เว็บของ Material Design เลยนะ
https://material.io/design/motion/the-motion-system.html#container-transform

เพื่อความเข้าใจที่มากขึ้น ลองทำความเข้าใจหลักการทำงานของมันกันนะ Transform Container แบบ Fade จะมี animation หลายตัวที่ซ้อนกันอยู่ คือระหว่างที่ Fade widget ที่สองเข้ามาก็จะ scale ให้ widget ที่สองใหญ่ขึ้น พร้อมๆกับ fade พื้นหลังสีมืดไปด้วย

ซึ่งตัว Transform container fade ขากลับ จะไม่ได้ reverse ตัว transition นะ แต่ใช้ transition อีกตัวนึงเลยทำเป็นขากลับ โดยใช้หลักการเดียวกัน

ในขณะที่ Fade through จะ fade widget แรกออกพร้อมกับขยาย widget ไปด้วย พอ widget แรกหายไปแล้ว จึงค่อยๆ fade widget ที่สองเข้ามาแทนที่ พร้อมขยายขนาดจนเต็ม ซึ่ง fade through ขากลับจะทำ reverse กับ ขามา ไม่ได้ทำ transition ใหม่เหมือน fade

จบตอนที่ 1

ขอบคุณผู้อ่านที่ติดตามอ่านจนจบครับ สำหรับตอนถัดไป จะพาไปเล่น Motion แบบอื่นๆ ใน Material Design ครับ น่าสนใจมากสำหรับ Flutter

สำหรับเนื้อหาสามารถอ่านเพิ่มได้ได้ที่ บล็อกและเว็บไซต์ของ Google Material Design ครับ
https://medium.com/google-design/implementing-motion-9f2839002016
https://material.io/design/motion/the-motion-system.html
https://pub.dev/packages/animations

ขอบคุณพี่แอมที่ช่วยตรวจอักษรครับ