Flutter 動畫入門

作者能力有限, 如果您在閱讀過程中發現任何錯誤, 還請您務必聯繫本人,指出錯誤, 避免後來讀者再學習錯誤的知識.謝謝!

參考: https://flutter.dev/docs/development/ui/animations/tutorial

基本概念

Animation: Flutter 提供的核心動畫庫, 使用指定的值來構建動畫.

Animation 實例: 它只知道動畫當前的狀態(state)(比如,動態是否啓動,結束,或者正在執行), 但是不知道當前屏幕顯示的內容.

CurveAnimation: 用於定義一個非線性曲線.

Tween: 補間動畫,用於在動畫狀態之間插入補充的值。 比如,它可以用與在從紅色變成綠色的動畫中插入中間值.

ListensersStatusListeners: 用於監聽動畫狀態(state)的改變.

下面簡要介紹一下常用的 Animation.

Animation<double>

Animation 的實例會在給定的兩個值之間生成連續的中間值。 生成數據的間隔由 duration 決定. 生成連續值的方式可以有以下幾種: 線性,曲線,階躍,或者其他方法.

Animation 實例是有狀態的, 可以通過 .value 字段獲取它當前的狀態值.

Animtation 實例無法知道 build 函數的狀態和其他繪製狀態.

CurvedAnimation

CurvedAnimation 用於定義一個非線性動畫.

animation = CurvedAnimation(parent: controller, curve: Curves.easeIn);

Curves 類中定義了許多常用的曲線

CurvedAnimation 和 AnimationController 類型都是 Animation 的子類型. 因此你可以將他們賦值給 Animation.

AnimationController

AnimationController 是一個特殊的 Animation, 用戶生成新的值在硬件準備好之後. 默認情況下, 它會已特定間隔生成一系列間與 0.0 - 1.0 的值.

例如:

controller =
    AnimationController(duration: const Duration(seconds: 2), vsync: this);

正如前文所說,AnimationController 是 Animation 的子類,相比於基類,它提供了一些額外的方法來控制動畫:
比如,你可以使用 forward() 來開始一段動畫.

生成數據的頻率由屏幕刷新次數決定. 通常情況下是每分鐘 60 次.

每當新數據生成時,每個 Animation 實例的 listenser 會被調用.

當創建一個動畫對象時,需要傳入一個 vsync 參數. 使用 vsync 可以防止屏幕外的動畫消耗不必要的資源。

Tween

通常情況下, AnimationController 實例的值範圍爲 0.0 - 1.0. 當你需要使用其他範圍的值時, 你可以通過使用 Tween 來配置動態來生成不同範圍的值.

比如: 當你想指定值的範圍爲 -200 - 0.0

tween = Tween<double>(begin: -200, end: 0);

Tween 是一個無狀態的對象(stateless), 它僅僅知道 begin 和 end. 它的任務通常是用來將一個值範圍映射爲另外一個值範圍. 被映射的值範圍通常都是 0.0 - 1.0.

Tween 是 Animatable 的子類,注意不是 Animation. Animatable 不需要生成的 double 值的輸出.

比如: ColroTween 用於生成在兩個 Color 之間的動畫.

colorTween = ColorTween(begin: Colors.transparent, end: Colors.black54);

Tween 實例不保存任何狀態,但是它提供了 evalute(Animation animation) 方法. 這個方法可以用於映射當前動畫值. 當前動畫值可以通過 Animation 實例的 value 字符訪問.

Tween.animate

可以通過將 controller 對象傳給 Tween.animate 方法來使用一個 Tween.
比如: 如下代碼在 500 ms 中生成 0 - 255 之間的值.

AnimationController controller = AnimationController(
    duration: const Duration(milliseconds: 500), vsync: this);
// 注意,animate 返回一個 Animation 對象,而不是 Animatable 對象.
Animation<int> alpha = IntTween(begin: 0, end: 255).animate(controller);

Animation Notification

Animation 實例可以由它的 Listenser 和 StatusListener,分別通過 addListener()addStatusListener() 方法添加.

Listenser 將會被回調當動畫值改變的時候. 最常見的做法是在 Listener 中調用 setState() 來重繪頁面.
StatusListener 將會被調用當動畫開始, 結束,前進,後退的時候.

動畫實例

Rendering animations

一個顯示動畫 Flutter Logo 的實例

原始代碼,無動畫

import 'package:flutter/material.dart';

void main() => runApp(LogoApp());

class LogoApp extends StatefulWidget {
  _LogoAppState createState() => _LogoAppState();
}

class _LogoAppState extends State<LogoApp> {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        margin: EdgeInsets.symmetric(vertical: 10),
        height: 300,
        width: 300,
        child: FlutterLogo(),
      ),
    );
  }
}

接下來,修改代碼,讓這個 logo 從沒有到佔滿屏幕

import 'package:flutter/material.dart';

void main() => runApp(LogoApp());

class LogoApp extends StatefulWidget {
  _LogoAppState createState() => _LogoAppState();
}

class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {

  Animation<double>   animation;
  AnimationController controller;

  @override
  void initState() {
    super.initState();
    controller = AnimationController(duration: const Duration(seconds: 2), vsync: this);
    animation = Tween<double>(begin: 0, end: 300).animate(controller)
      ..addListener(() {
        setState(() {});
      });
    controller.forward();
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        margin: EdgeInsets.symmetric(vertical: 10),
        height: animation.value,
        width: animation.value,
        child: FlutterLogo(),
      ),
    );
  }

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }
}

我們使用 addListener 調用 setState

每次 Animation 生成一個新的值之後會調用我們添加的 listener, 通過調用 setState 將當前幀標記爲 dirty, 之後 Flutter 就會重繪整個幀.

在 build 方法中, 將 container 的 height 和 width 指定爲 animation 當前的值,這樣每次動畫值更新後,container 的大小就會跟着變化.

在 initState 中初始話 controller, 在 dispose 中釋放 controller, 以避免內存泄漏.

效果如下:

在這裏插入圖片描述

Simplifying with Animated­Widget

這裏我們使用 AnimatedWidget 來簡化上一步的代碼, 將動畫代碼從 widget 的構建代碼中分離出來。

import 'package:flutter/material.dart';

void main() => runApp(LogoApp());

class LogoApp extends StatefulWidget {
  _LogoAppState createState() => _LogoAppState();
}

class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {

  Animation<double>   animation;
  AnimationController controller;

  @override
  void initState() {
    super.initState();
    controller = AnimationController(duration: const Duration(seconds: 2), vsync: this);
    animation = Tween<double>(begin: 0, end: 300).animate(controller);
    controller.forward();
  }

  @override
  Widget build(BuildContext context) => AnimatedLogo(animation: animation);

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }
}

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

  Widget build(BuildContext context) {
    final animation = listenable as Animation<double>;
    return Center(
      child: Container(
        margin: EdgeInsets.symmetric(vertical: 10),
        height: animation.value,
        width: animation.value,
        child: FlutterLogo(),
      ),
    );
  }
}

Monitoring the progress of the animation

本實例將使用 addStatusListener 來監聽動畫的狀態, 實現一個呼吸動畫.

import 'package:flutter/material.dart';

void main() => runApp(LogoApp());

class LogoApp extends StatefulWidget {
  _LogoAppState createState() => _LogoAppState();
}

class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {

  Animation<double>   animation;
  AnimationController controller;

  @override
  void initState() {
    super.initState();
    controller = AnimationController(duration: const Duration(seconds: 2), vsync: this);
    animation = Tween<double>(begin: 0, end: 300).animate(controller)
      ..addStatusListener((status) {
        if (status == AnimationStatus.completed) {
          controller.reverse();
        } else if (status == AnimationStatus.dismissed) {
          controller.forward();
        }
      })
    ..addStatusListener((status) => print('$status'));
    controller.forward();
  }

  @override
  Widget build(BuildContext context) => AnimatedLogo(animation: animation);

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }
}

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

  Widget build(BuildContext context) {
    final animation = listenable as Animation<double>;
    return Center(
      child: Container(
        margin: EdgeInsets.symmetric(vertical: 10),
        height: animation.value,
        width: animation.value,
        child: FlutterLogo(),
      ),
    );
  }
}

主要添加了 18-36 行的代碼.

效果如下:

在這裏插入圖片描述

Refactoring with AnimatedBuilder

上面代碼中存在一個問題, 那就是動畫改變時會同時修改包含它的頁面.

一個更好的解決方案是將繪製動畫的邏輯由另外一個類型負責.

我們使用 AnimatedBuilder 類來完成該功能. 就像 AnimatedWidget 一樣, AnimatedBuilder 會自動監聽動畫的狀態, 標記當前 widget
爲 dirty 在適當的時候, 然後重繪動畫. 我們不再需要調用 addListener.

爲此,我們就構建如下的 widget tree:

在這裏插入圖片描述

LogoWidget 的實現非常簡單, 僅僅顯示一個 Flutter Logo.

我們在 GrowTransition 的 build 方法中構建中間的 widgets.

完整代碼如下:

import 'package:flutter/material.dart';

void main() => runApp(LogoApp());

class LogoApp extends StatefulWidget {
  _LogoAppState createState() => _LogoAppState();
}

class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {

  Animation<double>   animation;
  AnimationController controller;

  @override
  void initState() {
    super.initState();
    controller = AnimationController(duration: const Duration(seconds: 2), vsync: this);
    animation = Tween<double>(begin: 0, end: 300).animate(controller);
    controller.forward();
  }

  @override
  Widget build(BuildContext context) {
    return GrowTransition(
      child: LogoWidget(),
      animation: animation,
    );
  }

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }
}

class LogoWidget extends StatelessWidget {
  // Leave out the height and width so it fills the animating parent
  Widget build(BuildContext context) => Container(
    margin: EdgeInsets.symmetric(vertical: 10),
    child: FlutterLogo(),
  );
}

class GrowTransition extends StatelessWidget {
  GrowTransition({this.child, this.animation});

  final Widget child;
  final Animation<double> animation;

  Widget build(BuildContext context) => Center(
    /*
	* One tricky point in the code below is that the child looks like it’s specified twice.
	* What’s happening is that the outer reference of child is passed to AnimatedBuilder,
	* which passes it to the anonymous closure, which then uses that object as its child.
	* The net result is that the AnimatedBuilder is inserted in between the two widgets in the render tree.
	*/
    child: AnimatedBuilder(
        animation: animation,
        builder: (context, child) => Container(
          height: animation.value,
          width: animation.value,
          child: child,
        ),
        child: child),
  );
}

Simultaneous animations

這一節,我們在 “monitoring the progress of the animation” 小節的基礎上構建一個同時變化的動畫.

我們的 Logo 動畫將在改變大小的同時改變自身的透明度.

import 'package:flutter/material.dart';

void main() => runApp(LogoApp());

class LogoApp extends StatefulWidget {
  _LogoAppState createState() => _LogoAppState();
}

class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {

  Animation<double>   animation;
  AnimationController controller;

  @override
  void initState() {
    super.initState();
    controller = AnimationController(duration: const Duration(seconds: 2), vsync: this);
    animation = CurvedAnimation(parent: controller, curve: Curves.easeIn)
      ..addStatusListener((status) {
        if (status == AnimationStatus.completed) {
          controller.reverse();
        } else if (status == AnimationStatus.dismissed) {
          controller.forward();
        }
      });
    controller.forward();
  }

  @override
  Widget build(BuildContext context) => AnimatedLogo(animation: animation);

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }
}

class AnimatedLogo extends AnimatedWidget {
  // Make the Tweens static because they don't change.
  static final _opacityTween = Tween<double>(begin: 0.1, end: 1);
  static final _sizeTween    = Tween<double>(begin: 0, end: 300);

  AnimatedLogo({Key key, Animation<double> animation})
      : super(key: key, listenable: animation);

  Widget build(BuildContext context) {
    final animation = listenable as Animation<double>;
    return Center(
      child:  Opacity (
        opacity: _opacityTween.evaluate(animation), // 通過evaluate 獲取 Tween 當前的值
        child: Container(
          margin: EdgeInsets.symmetric(vertical: 10),
          height: _sizeTween.evaluate(animation),
          width: _sizeTween.evaluate(animation),
          child: FlutterLogo(),
        ),
      ),
      
    );
  }
}

效果如下:

在這裏插入圖片描述

END!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章