Flutter中的動(dòng)畫

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

思維導(dǎo)圖.jpg

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

動(dòng)畫類型

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

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

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

基本的動(dòng)畫概念和類

Animation對(duì)象:

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

Interval

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

CurvedAnimation:將動(dòng)畫過(guò)程定義為一個(gè)非線性曲線,屬于Animation<double>類型

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

注: Curves 類類定義了許多常用的曲線,也可以創(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ì)線性的生成從0.0到1.0的數(shù)字
  • 屬于Animation<double>類型
  • 具有控制動(dòng)畫的方法,例如,.forward()方法可以啟動(dòng)動(dòng)畫
  • 當(dāng)創(chuàng)建一個(gè)AnimationController時(shí),需要傳遞一個(gè)vsync參數(shù),存在vsync時(shí)會(huì)防止屏幕外動(dòng)畫(動(dòng)畫的UI不在當(dāng)前屏幕時(shí))消耗不必要的資源。
    通過(guò)將SingleTickerProviderStateMixin添加到類定義中,可以將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)畫可以生成不同的范圍或數(shù)據(jù)類型的值。
  • 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)畫當(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è)控制器、一條曲線和一個(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)畫通知

  • 一個(gè)Animation對(duì)象可以擁有Listeners和StatusListeners監(jiān)聽(tīng)器,可以用addListener()和addStatusListener()來(lái)添加
  • 只要?jiǎng)赢嫷闹蛋l(fā)生變化,就會(huì)調(diào)用監(jiān)聽(tīng)器
  • 動(dòng)畫開(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)畫示例
要使用Animation<>對(duì)象進(jìn)行渲染,請(qǐng)將Animation對(duì)象存儲(chǔ)為Widget的成員,然后使用其value值來(lái)決定如何繪制

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

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)畫顯示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)畫生成一個(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)畫完成時(shí)釋放控制器(調(diào)用dispose()方法)以防止內(nèi)存泄漏。

用AnimatedWidget簡(jiǎn)化

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

在下面的重構(gòu)示例中,LogoApp現(xiàn)在繼承自AnimatedWidget而不是StatefulWidget。AnimatedWidget在繪制時(shí)使用動(dòng)畫的當(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ì)象傳遞給基類并用animation.value設(shè)置容器的高度和寬度,因此它的工作原理與之前完全相同。

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

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

知道動(dòng)畫何時(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)畫。這產(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)畫分離
  • AnimatedBuilder了解如何渲染過(guò)渡.
  • AnimatedBuilder 不知道如何渲染widget,也不知道如何管理Animation對(duì)象。
  • 使用AnimatedBuilder將動(dòng)畫描述為另一個(gè)widget的build方法的一部分。如果你只是想用可復(fù)用的動(dòng)畫定義一個(gè)widget,請(qǐng)使用AnimatedWidget。
  • Flutter API中AnimatedBuilder的示例包括: BottomSheet、ExpansionTile、 PopupMenu、ProgressIndicator、RefreshIndicator、Scaffold、SnackBar、TabBar、TextField。
  • 與AnimatedWidget類似,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)畫對(duì)象。

并行動(dòng)畫

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)畫對(duì)象。 為了解決這個(gè)問(wèn)題,該示例創(chuàng)建了自己的Tween對(duì)象并顯式計(jì)算了這些值。其build方法.evaluate()在父級(jí)的動(dòng)畫對(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)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,363評(píng)論 6 532
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,497評(píng)論 3 416
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書人閱讀 176,305評(píng)論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 62,962評(píng)論 1 311
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,727評(píng)論 6 410
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 55,193評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,257評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 42,411評(píng)論 0 288
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,945評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,777評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,978評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,519評(píng)論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,216評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 34,642評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 35,878評(píng)論 1 286
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,657評(píng)論 3 391
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,960評(píng)論 2 373