Flutter 中動畫的使用

移動的小太陽發表於2021-04-06

Flutter 中動畫的使用

在學習了靜態頁面的搭建後,需要再加上一點動畫效果來提升使用者體驗。

動畫靜態的畫面根據事先定義好的規律,在一定時間內不斷微調,產生變化效果。而動畫實現由靜止到動態,主要是靠人眼的視覺殘留效應。所以,對動畫系統而言,為了實現動畫,它需要做三件事兒:

  1. 確定畫面變化的規律;
  2. 根據這個規律設定好動畫週期,然後啟動動畫
  3. 通過不斷獲取動畫當前值,重繪畫面。

在Flutter 中,通過Animation、AnimationController 與 Listener完完成這三件事。

Animation:根據預定規則,在動畫週期內不斷輸出動畫當前值。

AnimationController:用於控制 Animation,可以設定動畫時長、啟動、停止動畫。

Listener:用於動畫的監聽,根據回撥,獲取動畫的當前值,進行渲染。

class _AnimationDemo1State extends State with SingleTickerProviderStateMixin {
  AnimationController controller;
  Animation<double> animation;

  @override
  void initState() {
    super.initState();
    // 建立 controller
    controller = AnimationController(
        vsync: this, duration: Duration(milliseconds: 1000));

    // 建立 animation
    animation = Tween(begin: 50.0, end: 250.0).animate(controller)
      ..addListener(() {
        // 更新狀態
        setState(() {});
      });
    //在啟動動畫時,使用 repeat(reverse: true),讓動畫來回重複執行。
    controller.repeat(reverse: true);
    // 監聽動畫狀態。在動畫結束時,反向執行;在動畫反向執行完畢時,重新啟動執行。
    animation.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        //在動畫結束時,反向執行
        controller.reverse();
      } else if (status == AnimationStatus.dismissed) {
        //在動畫反向執行完畢時,重新啟動執行
        controller.forward();
      }
    });
    // 開始動畫
    controller.forward();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("基礎動畫"),
      ),
      body: Center(
        child: Container(
          width: animation.value,
          height: animation.value,
          color: Colors.yellow,
        ),
      ),
    );
  }

  @override
  void dispose() {
    // 停止動畫
    controller.stop();
    super.dispose();
  }
}
複製程式碼

可以看到 animation只提供動畫資料,因此我們還需要監聽動畫執行進度,並在回撥中使用 setState 強制重新整理介面才能看到動畫效果。這些步驟都是固定的,有沒有什麼更簡單的方法嗎?

AnimatedWidget 與 AnimatedBuilder

通過這兩個widget 可以簡化我們的動畫步驟。 AnimatedWidget: 把對動畫的監聽放到 AnimatedWidget中,在裡面就可以拿到動畫的值,進行頁面的重繪。

// 通過 AnimatedWidget 來實現動畫

class AnimatedWidgetDemo extends AnimatedWidget {
  AnimatedWidgetDemo({Key key, Animation<double> animation})
      : super(key: key, listenable: animation);

  @override
  Widget build(BuildContext context) {
    final Animation<double> animation = listenable;
    return Container(
      width: animation.value,
      height: animation.value,
      color: Colors.red,
    );
  }
}

class AnimationDemo2 extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return _AnimationDemo2State();
  }
}

class _AnimationDemo2State extends State with SingleTickerProviderStateMixin {
  AnimationController controller;
  Animation<double> animation;

  @override
  void initState() {
    super.initState();
    // 建立 controller
    controller = AnimationController(
        vsync: this, duration: Duration(milliseconds: 1000));

    animation = Tween(begin: 50.0, end: 250.0).animate(controller);

    //在啟動動畫時,使用 repeat(reverse: true),讓動畫來回重複執行。
    controller.repeat(reverse: true);
    // 監聽動畫狀態。在動畫結束時,反向執行;在動畫反向執行完畢時,重新啟動執行。
    animation.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        //在動畫結束時,反向執行
        controller.reverse();
      } else if (status == AnimationStatus.dismissed) {
        //在動畫反向執行完畢時,重新啟動執行
        controller.forward();
      }
    });
    // 開始動畫
    controller.forward();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("AnimatedWidgetDemo"),
      ),
      body: Center(
        child: AnimatedWidgetDemo(
          animation: animation,
        ),
      ),
    );
  }

  @override
  void dispose() {
    controller.stop();
    super.dispose();
  }
}
複製程式碼

AnimatedBuilder: 將動畫和渲染邏輯分開。

class _AnimationDemo3State extends State with SingleTickerProviderStateMixin {
  AnimationController controller;
  Animation<double> animation;

  @override
  void initState() {
    super.initState();
    // 建立 controller
    controller = AnimationController(
        vsync: this, duration: Duration(milliseconds: 1000));

    animation = Tween(begin: 50.0, end: 250.0).animate(controller);

    //在啟動動畫時,使用 repeat(reverse: true),讓動畫來回重複執行。
    controller.repeat(reverse: true);
    // 監聽動畫狀態。在動畫結束時,反向執行;在動畫反向執行完畢時,重新啟動執行。
    animation.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        //在動畫結束時,反向執行
        controller.reverse();
      } else if (status == AnimationStatus.dismissed) {
        //在動畫反向執行完畢時,重新啟動執行
        controller.forward();
      }
    });
    // 開始動畫
    controller.forward();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("AnimatedWidgetDemo"),
        ),
        body: AnimatedBuilder(
          animation: animation,
          builder: (context, child) => Center(
            child: Container(
              width: animation.value,
              height: animation.value,
              color: Colors.deepPurple,
            ),
          ),
        ));
  }

  @override
  void dispose() {
    controller.stop();
    super.dispose();
  }
}
複製程式碼

可以看到 通過AnimatedBuilder,傳入 animation ,然後可以在裡面的widget 拿到 animation 的值,然後設定相應的屬性,更新UI。

通過上面幾種方式對比,僅僅是在動畫監聽 做了修改而已,從自己新增 listener 到繼承 AnimationWidget 傳入animation物件,在裡面可以使用到;在到 AnimationBuild ,直接傳入animation 物件,字面的子 widget 獲取到 animation的屬性後,更新UI,基本思路都是差不多。

hero 動畫

實現小圖到大圖頁面逐步放大的動畫切換效果,而當使用者關閉大圖時,也實現原路返回的動畫。這樣的跨頁面共享的控制元件動畫效果有一個專門的名詞,即“共享元素變換”(Shared Element Transition)。

Flutter 也有類似的概念,即 Hero 控制元件。通過 Hero,我們可以在兩個頁面的共享元素之間,做出流暢的頁面切換效果。

為了實現共享元素變換,我們需要將這兩個元件分別用 Hero 包裹,並同時為它們設定相同的 tag “hero”

class AnimationDemo4 extends StatelessWidget {
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("hero動畫"),
        ),
        body: GestureDetector(
          //手勢監聽點選
          child: Hero(
              tag: 'hero', //設定共享tag
              child: Center(
                child: Container(width: 80, height: 80, child: FlutterLogo()),
              )),
          onTap: () {
            Navigator.of(context)
                .push(MaterialPageRoute(builder: (_) => Page2())); //點選後開啟第二個頁面
          },
        ));
  }
}

class Page2 extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("第二個頁面"),
        ),
        body: Hero(
            tag: 'hero', //設定共享tag
            child: Center(
              child: Container(width: 320, height: 320, child: FlutterLogo()),
            )));
  }
}
複製程式碼

QQ20210323-001031.gif

總結

Flutter 中的動畫,是通過 Animation、AnimationController和Listener來完成的,通過Animation設定動畫的變化規律、AnimationController設定動畫時長、重複次數、開始、停止動畫等完成對動畫的管理,最後通過 Listener來完成動畫的監聽,改變widget 屬性,重新整理UI。Flutter 為我們提供更簡單方便的 AnimationWidget、AnimationBuild widget 來幫助我們把動畫和UI的更新邏輯分開,更好的完成複雜的動畫效果。最後Flutter 還提供了共享元素動畫 Hero。

相關文章