web analytics

การทำ Animation ใน Flutter ตอนที่ 2

สวัสดีผู้อ่านครับ บล็อกนี้เป็นตอนที่ 2 ของการทำ Animation ใน Flutter จากตอนที่แล้ว เราได้รู้จักกับ Animation controller และการทำ transition ด้วย AnimatedBuilder ไปแล้ว ซึ่งเป็น Widget ที่ทาง Flutter เตรียมไว้สำหรับ build Animation แบบ custom โดยเฉพาะ ในตอนที่ 2 นี้จะพามารู้จักกับ Widget Animated แบบอื่นๆกัน

ใครยังไม่ได้อ่าน ตอนที่ 1 อ่านได้ที่ลิงค์ด้านล่างนี้

Animated Widget

จากตอนที่แล้วได้ลองเล่น AnimatedBuilder ซึ่งเป็นหนึ่งใน widget ของกลุ่ม Animated โดย widget ประเภทนี้จะใช้ prefix ว่า animated และนอกจาก AnimatedBuilder แล้วยังมี widget สำหรับ animated อีกหลายตัว มาลองเล่นกัน

Animated Opacity

AnimatedOpacity จะช่วยให้เราทำ fade transition กับ widget ที่ต้องการได้สะดวกมาก

สร้างตัวแปรสำหรับเก็บค่า opacity

class _MyHomePageState extends State<MyHomePage> {
  double currentOpacity = 0;

จากนั้น เอา widget ที่ต้องการมาเป็น child ของ AnimatedOpacity แล้วกำหนด duration กับ opacity ให้มัน จากนั้นเรียก setState เพื่อเปลี่ยนค่า opacity มันก็จะทำ animation ของ opacity ให้อัตโนมัติ ตามเวลา duration ที่กำหนด จะเห็นว่า เราไม่ต้องไปยุ่งกับ animation controller เลย

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Animation"),
      ),
      body: Center(
          child: AnimatedOpacity(               // เพิ่ม Animated Opacity
              opacity: currentOpacity,          //
              duration: Duration(seconds: 1),   //
              child: Container(                 //
                width: 200,                     //
                height: 200,                    //
                color: Colors.orange[400],      //
              ))),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.play_arrow),
        onPressed: () {                         // เพิ่ม on pressed
          setState(() {
            currentOpacity = currentOpacity == 0 ? 1 : 0;
          });
        },
      ),
    );
  }

ลองรันดูผลลัพธ์

Animated Container

animated container ใช้งานคล้ายกับ animated opacity แต่จะทำ animation เกี่ยวกับคุณสมบัติของ container แทน เช่น ขนาด สี

เริ่มจาก สร้างตัวแปรไว้หนึ่งตัว สำหรับเก็บค่าขนาด container

class _MyHomePageState extends State<MyHomePage> {
  double size = 200;

จากนั้นกำหนดตัวแปรให้กับ width , height ของ container แล้วลอง เรียก setState เปลี่ยนค่าของตัวแปร

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      ...
      body: Center(
          child: AnimatedContainer(              // เพิ่ม 
        width: size,                             // 
        height: size,                            // 
        color: Colors.orange[400],               // 
        duration: Duration(milliseconds: 500),   // 
      )),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.play_arrow),
        onPressed: () {
          setState(() {
            size = size + 50;                    // เพิ่ม 
          });
        },
      ),
    );
  }

เท่านี้ container ก็จะทำ animation ตามตัวแปรที่เรากำหนดแล้ว ตัวอย่างนี้ผมกำหนดให้กดปุมแล้ว ขนาดจะใหญ่ขึ้น 50

Animated Align

Animated ยังมีอีกเยอะ มาลองเล่นอีกตัว AnimatedAlign ที่จะช่วยทำ animation ตามค่าของ align

กำหนดตัวแปร alignment มีค่าเริ่มต้นอยู่ ตรงกลาง

class _MyHomePageState extends State<MyHomePage> {
  Alignment alignment = Alignment.center;

จากนั้นใช้ AnimatedAlign แล้วอัพเดทค่า align ใน setState ให้ alignment ไป บนซ้าย แทน

@override
  Widget build(BuildContext context) {
    return Scaffold(
      ...
      body: Center(
          child: AnimatedAlign(               // เพิ่ม
            alignment: alignment,             //
            duration: Duration(seconds: 1),   //
            child: Container(                 //
              width: 200,                     //
              height: 200,                    //
              color: Colors.orange[400],      //
            ),
          )),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.play_arrow),
        onPressed: () {
          setState(() {                        // เพิ่ม
            alignment = alignment == Alignment.center ? Alignment.topLeft : Alignment.center;
          });
        },
      ),
    );
  }

ลองรัน จะเห็นว่ามันทำ animation ตาม alignment แล้ว ง่ายมากๆ

Animated Positioned

ถ้าใช้ align แล้ว Animated positioned ก็ใช้งานไม่ต่างกันนัก โดยจะใช้งานเพื่อกำหนดตำแหน่งใน parent และทำ animation อัตโนมัติหาก เราอัพเดท position

สร้างตัวแปร กำหนดค่า position

class _MyHomePageState extends State<MyHomePage> {
  double top = 99;
  double right = 251;

จากนั้นนำค่าไปใช้กับ AnimatedPositioned แล้วลอง setState อัพเดทค่า position จากการกดปุ่ม

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      ...
      body: Stack(
        children: [
          AnimatedPositioned(                    // เพิ่ม 
              top: top,                          //
              right: right,                      //
              duration: Duration(seconds: 1),    //
              child: Container(                  //
                color: Colors.red[400],          //
                width: 100,                      //
                height: 100,                     //
              ))
        ],
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.play_arrow),
        onPressed: () {                           // เพิ่ม
          setState(() {                           //
            top = top + 20;                       //
            if(right >= 0){                       //
              right = right - 50;                 //
            }else{                                //
              right = 250;                        //
            }                                     //
          });                                     //
        },                                        //
      ),
    );
  }

ลองรัน

ถึงตรงนี้ น่าจะเริ่มจับทางได้แล้วใช่ไหมครับ ว่า AnimatedOpacity , AnimatedContainer , AnimatedAlign พวกนี้ ใช้งานคล้ายกันมาก แค่นำมันมา wrap ให้กับ widget ที่ต้องการ กำหนด duration และค่าต่างๆ จากนั้นเมื่อเรา setState เพื่ออัพเดท มันก็จะทำ animation อัตโนมัติ สะดวกมากๆ

แต่ถึงอย่างนั้น widget ในกลุ่ม Animated ก็ยังมีอีกซึ่งมีอีกหลายตัวที่น่าสนใจ แต่การใช้งานจะซับซ้อนขึ้นอีกนิด

Animated Icon

Animated icon เป็น widget อีกตัวที่น่าสนใจ โดยมันจะ transition เปลี่ยนไอคอนนึง ไปอีกไอคอนนึง ซึ่งทาง Flutter ได้เตรียมไอคอนมาให้เราจำนวนนึงแล้ว

โดยเราจำเป็นต้องใช้ Animation controller

class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
  AnimationController animationController;
  
  @override
  void initState() {
    animationController = AnimationController(duration: Duration(milliseconds: 1000), vsync: this);
    super.initState();
  }

จากนั้น Wrap widget ที่ต้องการด้วย AnimatedIcons โดยใช้ icon จาก AnimatedIcons เวลาใช้งานก็เรียกใช้ animationController.forward()

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      ...
      body: Center(
        child: AnimatedIcon(
          icon: AnimatedIcons.menu_home,
          progress: animationController,
          color: Colors.red[400],
          size: 100,
        ),
      ),
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.play_arrow),
        onPressed: () {
          if (animationController.isCompleted) {
            animationController.reverse();
          } else {
            animationController.forward();
          }
        },
      ),
    );
  }

ลองรัน

มีไอคอนให้ใช้งานได้ประมาณสิบอัน สามารถดูได้ที่คลาส AnimatedIcons

Animated Cross Fade

ที่น่าสนใจอีกตัวคือ Cross Fade โดยจะทำงานง่ายๆคือ กำหนด widget 2 ตัว คือ first , second จากนั้นก็กำหนดว่าจะให้แสดงตัวไหน แล้วมันจะทำ fade animation ให้อัตโนมัติ

สร้าง bool มาตัวนึงว่าจะให้แสดง widget ตัวแรกมัย

class _MyHomePageState extends State<MyHomePage> {
  bool _first = true;

จากนั้นก็กำหนด firstChild , secondChild ให้กับ AnimatedCrossFade ส่วนวิธีแสดง child ไหนจะใช้ CrossFadeState.showFirst หรือ CrossFadeState.showSecond

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      ...
      body: Center(
        child: AnimatedCrossFade(
          duration:  Duration(seconds: 1),
          firstChild:  Container(width: 200,height: 200,color: Colors.blue[300]),
          secondChild:  FlutterLogo(style: FlutterLogoStyle.stacked, size: 150.0),
          crossFadeState: _first ? CrossFadeState.showFirst : CrossFadeState.showSecond,
        ),
      ),
      floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
      floatingActionButton: FloatingActionButton(
        backgroundColor: Colors.red[300],
        child: Icon(Icons.refresh),
        onPressed: () {
          setState(() {
            _first = !_first;
          });
        },
      ),
    );
  }

จะได้แบบนี้

Animated List

Widget พระเอกอีกตัวที่ผมคิดว่าน่าจะใช้กันบ่อย คือ AnimatedList มันคือการทำ animation กับ item ใน ListView เช่น การเพิ่ม item หรือการลบ item

เริ่มจากลอง สร้างข้อมูลจำลองขึ้นมา สำหรับเป็น item ใน ListView

class _MyHomePageState extends State<MyHomePage> {
  List<String> listData = List.generate(3, (index) {
    return "New item";
  });

AnimatedList การใช้งานแทบจเหมือน Listview.builder คือกำหนด itemCount และ implement ตัว itemBuilder ในตัวอย่างนี้ ผมจะแยก method สำหรับ build UI แต่ละ item เอาไว้

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      ...
      body: Container(
        color: Colors.grey[300],
        child: AnimatedList(
          initialItemCount: listData.length,
          padding: EdgeInsets.all(16),
          itemBuilder: (BuildContext context, int index, Animation<double> animation) {
            return buildRowData(listData[index]);
          },
        ),
      ),
    );
  }

ลอง return item แบบธรรมดาๆไปก่อน แบบที่ยังไม่ใส่ animation

  Widget buildRowData(String title) {
    return Container(
      margin: EdgeInsets.symmetric(vertical: 4),
      decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(8)),
      width: double.infinity,
      padding: EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          Text(
            title,
            style: TextStyle(fontSize: 18),
          ),
          Text(
            "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut",
            style: TextStyle(fontSize: 14, color: Colors.grey),
          ),
        ],
      ),
    );
  }

จะได้เหมือน ListView ปกติ

ใส่ floating action button สำหรับกดสร้างและลบ item

@override
  Widget build(BuildContext context) {
    return Scaffold(
      ...
      floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
      floatingActionButton: Row(
        mainAxisSize: MainAxisSize.min,
        children: <Widget>[
          FloatingActionButton(
            backgroundColor: Colors.green[300],
            child: Icon(
              Icons.add,
            ),
            onPressed: () { 
              // addData();         // รอ implement สำหรับกดเพิ่ม item
            },
          ),
          SizedBox(
            width: 8,
          ),
          FloatingActionButton(
            backgroundColor: Colors.red[300],
            child: Icon(Icons.remove),
            onPressed: () {
              // removeData();       // รอ implement สำหรับกดลบ item
            },
          ),
        ],
      ),
    );
  }

ลองรัน จะได้แบบนี้

จากนั้นประกาศตัวแปร GlobalKey ของ AnimatedListState ใน State ของเรา ใช้สำหรับเข้าถึง State ของ AnimatedList

class _MyHomePageState extends State<MyHomePage> {
  ...
  GlobalKey<AnimatedListState> keyAnimatedList = GlobalKey();

นำ GlobalKey ไปกำหนดให้กับ AnimatedList ผ่าน parameter ที่ชื่อว่า key

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      ...
      body: Container(
        color: Colors.grey[300],
        child: AnimatedList(
          key: keyAnimatedList,               // เพิ่ม key
          ...

จากนั้นเขียน method สำหรับเพิ่ม item ลงไปใน List สิ่งที่เราต้องทำคือ เพิ่มข้อมูลลงไปใน list data จริงๆ และเรียก key current State เพื่อเข้าไปเรียก insertItem อันนี้ไม่ได้เป็นการ add ข้อมูลแต่จะเป็นการทำ animation ให้ item ที่เพิ่งถูกเพิ่มไป

  void addData() {
    listData.add("New item");
    keyAnimatedList.currentState.insertItem(listData.length - 1, duration: Duration(seconds: 1));
  }

ลองรันกด add data ได้แล้ว แต่ animation ยังไม่แสดง เพราะเรายังไม่ได้กำหนด Animation ให้แต่ละ item

กลับมาที่ itemBuilder ของ AnimatedList เพิ่ม FadeTransition ให้กับ buildRowData ของเรา จะเห็นหว่า itemBuilder ก็ส่งค่า animation มาให้แล้ว โดยค่านี้เกิดจากตอนเรากำหนดที่ insertItem นั่นเอง

@override
  Widget build(BuildContext context) {
    return Scaffold(
      ...
      body: Container(
        color: Colors.grey[300],
        child: AnimatedList(
          ...
          itemBuilder: (BuildContext context, int index, Animation<double> animation) {
            return FadeTransition(opacity: animation, child: buildRowData(listData[index]));   // เพิ่ม Fade transition
          },
        ),
      ),

ลองรัน ได้ animation ตอน add item แล้ว

ต่อมาทำ animation สำหรับลบ item ครับ วิธีการคล้ายกับ insert แต่เราจะทำ animation เพื่อลบก่อน จากนั้นค่อยลบข้อมูลจาก list จริง

  void removeData() {
    keyAnimatedList.currentState.removeItem(listData.length - 1, (context, animation) {
      return FadeTransition(
        opacity: animation.drive(Tween(begin: 0, end: 1)),
        child: buildRowData(listData.last),
      );
    }, duration: Duration(seconds: 1));
    listData.removeLast();
  }

เสร็จแล้วเพิ่ม removeData() ให้กับปุ่มลบ

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      ...
      floatingActionButton: Row(
        ...
        children: <Widget>[
          FloatingActionButton(
            ...
            onPressed: () {
              addData();                    // เพิ่ม
            },
          ),
          ...
          FloatingActionButton(
            ...
            onPressed: () {
              removeData();                 // เพิ่ม
            },
          ),
        ],
      ),
    );
  }

ลองรัน จะได้ AnimatedList แล้ว

Source code ของ AnimatedList ครับ
https://gist.github.com/benznest/0a0f63758aeabd80e2c5e360d886df1e

สรุป

ในตอนที่ 2 เราได้รู้จักกับ widget ที่มี prefix Animated เช่น Animated Opacity , Animated Container ซึ่งเป็น Widget ที่ใช้สำหรับทำ animation กับ widget นั้นๆ และ AnimatedList ที่ช่วยให้ทำ Animation กับ item ใน ListView ได้ ในตอนถัดไปจะพาไปรู้จักกับ Animation ตัวไหนอีก ฝากติดตามด้วยครับ