Flutter中的動(dòng)畫(huà)

參考來(lái)源:https://flutterchina.club/animations/

思維導(dǎo)圖.jpg

Widget執(zhí)行動(dòng)畫(huà)的方法.jpg

動(dòng)畫(huà)類(lèi)型

  • 補(bǔ)間(Tween)動(dòng)畫(huà)
    :在補(bǔ)間動(dòng)畫(huà)中,定義了開(kāi)始點(diǎn)和結(jié)束點(diǎn)、時(shí)間線(xiàn)以及定義轉(zhuǎn)換時(shí)間和速度的曲線(xiàn),然后由框架自動(dòng)計(jì)算如何從開(kāi)始點(diǎn)過(guò)渡到結(jié)束點(diǎn)。

  • 基于物理的動(dòng)畫(huà)
    :在基于物理的動(dòng)畫(huà)中,運(yùn)動(dòng)被模擬為與真實(shí)世界的行為相似。例如,當(dāng)你擲球時(shí),它在何處落地,取決于拋球速度有多快、球有多重、距離地面有多遠(yuǎn)。 類(lèi)似地,將連接在彈簧上的球落下(并彈起)與連接到繩子上的球放下的方式也是不同。

常見(jiàn)的動(dòng)畫(huà)模式

基本的動(dòng)畫(huà)概念和類(lèi)

Animation對(duì)象:

  • Animation對(duì)象是Flutter動(dòng)畫(huà)庫(kù)中的一個(gè)核心類(lèi),它生成指導(dǎo)動(dòng)畫(huà)的值。
  • Animation對(duì)象知道動(dòng)畫(huà)的當(dāng)前狀態(tài)(例如,它是開(kāi)始、停止還是向前或向后移動(dòng)),但它不知道屏幕上顯示的內(nèi)容。
  • Flutter中的Animation對(duì)象是一個(gè)在一段時(shí)間內(nèi)依次生成一個(gè)區(qū)間之間值的類(lèi)。Animation對(duì)象的輸出可以是線(xiàn)性的、曲線(xiàn)的、一個(gè)步進(jìn)函數(shù)或者任何其他可以設(shè)計(jì)的映射。 根據(jù)Animation對(duì)象的控制方式,動(dòng)畫(huà)可以反向運(yùn)行,甚至可以在中間切換方向。
  • Animation還可以生成除double之外的其他類(lèi)型值,如:Animation<Color> 或 Animation<Size>
  • Animation對(duì)象有狀態(tài),可以通過(guò)訪(fǎng)問(wèn)其value屬性獲取動(dòng)畫(huà)的當(dāng)前值
  • Animation對(duì)象本身和UI渲染沒(méi)有任何關(guān)系

Interval

在[begin]之前為0.0的曲線(xiàn),然后根據(jù)[curve曲線(xiàn)]在[end]時(shí)從0.0到1.0,然后是1.0。
可以使用[Interval]來(lái)延遲動(dòng)畫(huà)。例如,使用[Interval]將[begin]設(shè)置為0.5,將[end]設(shè)置為1.0的[Interval]使用[Interval],這6秒的動(dòng)畫(huà)將本質(zhì)上變成三秒鐘后開(kāi)始的動(dòng)畫(huà)。

CurvedAnimation:將動(dòng)畫(huà)過(guò)程定義為一個(gè)非線(xiàn)性曲線(xiàn),屬于A(yíng)nimation<double>類(lèi)型

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

注: Curves 類(lèi)類(lèi)定義了許多常用的曲線(xiàn),也可以創(chuàng)建自己的,例如:

class ShakeCurve extends Curve {
  @override
  double transform(double t) {
    return math.sin(t * math.PI * 2);
  }
}

AnimationController:

  • AnimationController是一個(gè)特殊的Animation對(duì)象,在屏幕刷新的每一幀,就會(huì)生成一個(gè)新的值,默認(rèn)情況下,AnimationController在給定的時(shí)間段內(nèi)會(huì)線(xiàn)性的生成從0.0到1.0的數(shù)字
  • 屬于A(yíng)nimation<double>類(lèi)型
  • 具有控制動(dòng)畫(huà)的方法,例如,.forward()方法可以啟動(dòng)動(dòng)畫(huà)
  • 當(dāng)創(chuàng)建一個(gè)AnimationController時(shí),需要傳遞一個(gè)vsync參數(shù),存在vsync時(shí)會(huì)防止屏幕外動(dòng)畫(huà)(動(dòng)畫(huà)的UI不在當(dāng)前屏幕時(shí))消耗不必要的資源。
    通過(guò)將SingleTickerProviderStateMixin添加到類(lèi)定義中,可以將stateful對(duì)象作為vsync的值。如果要使用自定義的State對(duì)象作為vsync時(shí),請(qǐng)包含TickerProviderStateMixin。
// 下面代碼創(chuàng)建一個(gè)Animation對(duì)象,但不會(huì)啟動(dòng)它運(yùn)行:
final AnimationController controller = new AnimationController(
    duration: const Duration(milliseconds: 2000), vsync: this);

Tween:

  • 默認(rèn)情況下,AnimationController對(duì)象的范圍從0.0到1.0,使用Tween使動(dòng)畫(huà)可以生成不同的范圍或數(shù)據(jù)類(lèi)型的值。
  • Tween是一個(gè)無(wú)狀態(tài)(stateless)對(duì)象,需要begin和end值,Tween的唯一作用就是定義從輸入范圍到輸出范圍的映射。
  • Tween繼承自Animatable<T>,而不是繼承自Animation<T>。Animatable與Animation相似,不是必須輸出double值。例如,ColorTween指定兩種顏色之間的過(guò)渡。
  • evaluate(Animation<double> animation)方法將映射函數(shù)應(yīng)用于動(dòng)畫(huà)當(dāng)前值,Animation對(duì)象的當(dāng)前值可以通過(guò)value()方法取到。
  • 要使用Tween對(duì)象,請(qǐng)調(diào)用其animate()方法,傳入一個(gè)控制器對(duì)象(Tween.animate),注意animate()返回的是一個(gè)Animation,而不是一個(gè)Animatable。
// 以下示例,Tween生成從-200.0到0.0的值
final Tween doubleTween = new Tween<double>(begin: -200.0, end: 0.0);
// ColorTween指定兩種顏色之間的過(guò)渡
final Tween colorTween =
    new ColorTween(begin: Colors.transparent, end: Colors.black54);
// 以下代碼在500毫秒內(nèi)生成從0到255的整數(shù)值
final AnimationController controller = new AnimationController(
    duration: const Duration(milliseconds: 500), vsync: this);
Animation<int> alpha = new IntTween(begin: 0, end: 255).animate(controller);
// 以下示例構(gòu)建了一個(gè)控制器、一條曲線(xiàn)和一個(gè)Tween:
final AnimationController controller = new AnimationController(
    duration: const Duration(milliseconds: 500), vsync: this);
final Animation curve =
    new CurvedAnimation(parent: controller, curve: Curves.easeOut);
Animation<int> alpha = new IntTween(begin: 0, end: 255).animate(curve);

動(dòng)畫(huà)通知

  • 一個(gè)Animation對(duì)象可以擁有Listeners和StatusListeners監(jiān)聽(tīng)器,可以用addListener()和addStatusListener()來(lái)添加
  • 只要?jiǎng)赢?huà)的值發(fā)生變化,就會(huì)調(diào)用監(jiān)聽(tīng)器
  • 動(dòng)畫(huà)開(kāi)始、結(jié)束、向前移動(dòng)或向后移動(dòng)(如AnimationStatus所定義)時(shí)會(huì)調(diào)用StatusListener
  • 一個(gè)Listener最常見(jiàn)的行為是調(diào)用setState()來(lái)觸發(fā)UI重建。

動(dòng)畫(huà)示例
要使用Animation<>對(duì)象進(jìn)行渲染,請(qǐng)將Animation對(duì)象存儲(chǔ)為Widget的成員,然后使用其value值來(lái)決定如何繪制

考慮下面的應(yīng)用程序,它繪制Flutter logo時(shí)沒(méi)有動(dòng)畫(huà):

import 'package:flutter/material.dart';

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

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

void main() {
  runApp(new LogoApp());
}

修改以上代碼,通過(guò)一個(gè)逐漸放大的動(dòng)畫(huà)顯示logo。定義AnimationController時(shí),必須傳入一個(gè)vsync對(duì)象。

import 'package:flutter/animation.dart';
import 'package:flutter/material.dart';

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

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

  initState() {
    super.initState();
    controller = new AnimationController(
        duration: const Duration(milliseconds: 2000), vsync: this);
    animation = new Tween(begin: 0.0, end: 300.0).animate(controller)
      ..addListener(() {
        setState(() {
          // the state that has changed here is the animation object’s value
        });
      });
    controller.forward();
  }

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

  dispose() {
    controller.dispose();
    super.dispose();
  }
}

void main() {
  runApp(new LogoApp());
}

該addListener()函數(shù)調(diào)用了setState(),所以每次動(dòng)畫(huà)生成一個(gè)新的數(shù)字時(shí),當(dāng)前幀被標(biāo)記為臟(dirty),這會(huì)導(dǎo)致widget的build()方法再次被調(diào)用。 在build()中,改變container大小,因?yàn)樗母叨群蛯挾痊F(xiàn)在使用的是animation.value。動(dòng)畫(huà)完成時(shí)釋放控制器(調(diào)用dispose()方法)以防止內(nèi)存泄漏。

用AnimatedWidget簡(jiǎn)化

  • 使用AnimatedWidget助手類(lèi)(而不是addListener()和setState())來(lái)給widget添加動(dòng)畫(huà)
  • 使用AnimatedWidget創(chuàng)建一個(gè)可重用動(dòng)畫(huà)的widget。要從widget中分離出動(dòng)畫(huà)過(guò)渡,請(qǐng)使用AnimatedBuilder
  • Flutter API提供的關(guān)于A(yíng)nimatedWidget的示例包括:AnimatedBuilder、AnimatedModalBarrier、DecoratedBoxTransition、FadeTransition、PositionedTransition、RelativePositionedTransition、RotationTransition、ScaleTransition、SizeTransition、SlideTransition。
  • AnimatedWidget類(lèi)允許您從setState()調(diào)用中的動(dòng)畫(huà)代碼中分離出widget代碼。AnimatedWidget不需要維護(hù)一個(gè)State對(duì)象來(lái)保存動(dòng)畫(huà)。

在下面的重構(gòu)示例中,LogoApp現(xiàn)在繼承自AnimatedWidget而不是StatefulWidget。AnimatedWidget在繪制時(shí)使用動(dòng)畫(huà)的當(dāng)前值。LogoApp仍然管理著AnimationController和Tween。

// Demonstrate a simple animation with AnimatedWidget

import 'package:flutter/animation.dart';
import 'package:flutter/material.dart';

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

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

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

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

  initState() {
    super.initState();
    controller = new AnimationController(
        duration: const Duration(milliseconds: 2000), vsync: this);
    animation = new Tween(begin: 0.0, end: 300.0).animate(controller);
    controller.forward();
  }

  Widget build(BuildContext context) {
    return new AnimatedLogo(animation: animation);
  }

  dispose() {
    controller.dispose();
    super.dispose();
  }
}

void main() {
  runApp(new LogoApp());
}

LogoApp將Animation對(duì)象傳遞給基類(lèi)并用animation.value設(shè)置容器的高度和寬度,因此它的工作原理與之前完全相同。

監(jiān)視動(dòng)畫(huà)的過(guò)程

  • 使用addStatusListener來(lái)處理動(dòng)畫(huà)狀態(tài)更改的通知,例如啟動(dòng)、停止或反轉(zhuǎn)方向。

知道動(dòng)畫(huà)何時(shí)改變狀態(tài)通常很有用的,如完成、前進(jìn)或倒退。你可以通過(guò)addStatusListener()來(lái)得到這個(gè)通知。

// 以下代碼用來(lái)監(jiān)聽(tīng)動(dòng)態(tài)狀態(tài)更改并打印更新:
class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin {
  AnimationController controller;
  Animation<double> animation;

  initState() {
    super.initState();
    controller = new AnimationController(
        duration: const Duration(milliseconds: 2000), vsync: this);
    animation = new Tween(begin: 0.0, end: 300.0).animate(controller)
      ..addStatusListener((state) => print("$state"));
    controller.forward();
  }
  //...
}

運(yùn)行此代碼將輸出以下內(nèi)容:

AnimationStatus.forward
AnimationStatus.completed

接下來(lái),使用addStatusListener()在開(kāi)始或結(jié)束時(shí)反轉(zhuǎn)動(dòng)畫(huà)。這產(chǎn)生了循環(huán)效果:

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

  initState() {
    super.initState();
    controller = new AnimationController(
        duration: const Duration(milliseconds: 2000), vsync: this);
    animation = new Tween(begin: 0.0, end: 300.0).animate(controller);

    animation.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        controller.reverse();
      } else if (status == AnimationStatus.dismissed) {
        controller.forward();
      }
    });
    controller.forward();
  }
  //...
}

用AnimatedBuilder重構(gòu)

  • AnimatedBuilder用于將widget與動(dòng)畫(huà)分離
  • AnimatedBuilder了解如何渲染過(guò)渡.
  • AnimatedBuilder 不知道如何渲染widget,也不知道如何管理Animation對(duì)象。
  • 使用AnimatedBuilder將動(dòng)畫(huà)描述為另一個(gè)widget的build方法的一部分。如果你只是想用可復(fù)用的動(dòng)畫(huà)定義一個(gè)widget,請(qǐng)使用AnimatedWidget。
  • Flutter API中AnimatedBuilder的示例包括: BottomSheet、ExpansionTile、 PopupMenu、ProgressIndicator、RefreshIndicator、Scaffold、SnackBar、TabBar、TextField。
  • 與AnimatedWidget類(lèi)似,AnimatedBuilder自動(dòng)監(jiān)聽(tīng)來(lái)自Animation對(duì)象的通知,并根據(jù)需要將該控件樹(shù)標(biāo)記為臟(dirty),因此不需要手動(dòng)調(diào)用addListener()。

從widget樹(shù)的底部開(kāi)始,渲染logo的代碼直接明了:

class LogoWidget extends StatelessWidget {
  // Leave out the height and width so it fills the animating parent
  build(BuildContext context) {
    return new Container(
      margin: new EdgeInsets.symmetric(vertical: 10.0),
      child: new FlutterLogo(),
    );
  }
}
class GrowTransition extends StatelessWidget {
  GrowTransition({this.child, this.animation});

  final Widget child;
  final Animation<double> animation;

  Widget build(BuildContext context) {
    return new Center(
      child: new AnimatedBuilder(
          animation: animation,
          builder: (BuildContext context, Widget child) {
            return new Container(
                height: animation.value, width: animation.value, child: child);
          },
          child: child),
    );
  }
}
class LogoApp extends StatefulWidget {
  _LogoAppState createState() => new _LogoAppState();
}

class _LogoAppState extends State<LogoApp> with TickerProviderStateMixin {
  Animation animation;
  AnimationController controller;

  initState() {
    super.initState();
    controller = new AnimationController(
        duration: const Duration(milliseconds: 2000), vsync: this);
    final CurvedAnimation curve =
        new CurvedAnimation(parent: controller, curve: Curves.easeIn);
    animation = new Tween(begin: 0.0, end: 300.0).animate(curve);
    controller.forward();
  }

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

  dispose() {
    controller.dispose();
    super.dispose();
  }
}

void main() {
  runApp(new LogoApp());
}

initState()方法創(chuàng)建一個(gè)AnimationController和一個(gè)Tween,然后通過(guò)animate()綁定它們。魔術(shù)發(fā)生在build()方法中,該方法返回一個(gè)帶有LogoWidget作為子對(duì)象的GrowTransition對(duì)象,以及一個(gè)用于驅(qū)動(dòng)過(guò)渡的動(dòng)畫(huà)對(duì)象。

并行動(dòng)畫(huà)

import 'package:flutter/animation.dart';
import 'package:flutter/material.dart';

class AnimatedLogo extends AnimatedWidget {
  // The Tweens are static because they don't change.
  static final _opacityTween = new Tween<double>(begin: 0.1, end: 1.0);
  static final _sizeTween = new Tween<double>(begin: 0.0, end: 300.0);

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

  Widget build(BuildContext context) {
    final Animation<double> animation = listenable;
    return new Center(
      child: new Opacity(
        opacity: _opacityTween.evaluate(animation),
        child: new Container(
          margin: new EdgeInsets.symmetric(vertical: 10.0),
          height: _sizeTween.evaluate(animation),
          width: _sizeTween.evaluate(animation),
          child: new FlutterLogo(),
        ),
      ),
    );
  }
}

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

class _LogoAppState extends State<LogoApp> with TickerProviderStateMixin {
  AnimationController controller;
  Animation<double> animation;

  initState() {
    super.initState();
    controller = new AnimationController(
        duration: const Duration(milliseconds: 2000), vsync: this);
    animation = new CurvedAnimation(parent: controller, curve: Curves.easeIn);

    animation.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        controller.reverse();
      } else if (status == AnimationStatus.dismissed) {
        controller.forward();
      }
    });

    controller.forward();
  }

  Widget build(BuildContext context) {
    return new AnimatedLogo(animation: animation);
  }

  dispose() {
    controller.dispose();
    super.dispose();
  }
}

void main() {
  runApp(new LogoApp());
}

AnimatedWidget的構(gòu)造函數(shù)只接受一個(gè)動(dòng)畫(huà)對(duì)象。 為了解決這個(gè)問(wèn)題,該示例創(chuàng)建了自己的Tween對(duì)象并顯式計(jì)算了這些值。其build方法.evaluate()在父級(jí)的動(dòng)畫(huà)對(duì)象上調(diào)用Tween函數(shù)以計(jì)算所需的size和opacity值。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。