概述
- 動畫API認(rèn)識
- 動畫案例練習(xí)
- 其它動畫補充
一、動畫API認(rèn)識
動畫實際上是我們通過某些方式(某種對象,Animation對象)給Flutter引擎提供不同的值,而Flutter可以根據(jù)我們提供的值,給對應(yīng)的小部件添加順滑的動畫效果。
-
1.1、Animation
在Flutter中,實現(xiàn)動畫的核心類是動畫,小部件可以直接將這些動畫合并到自己的構(gòu)建方法中來讀取它們的當(dāng)前值或監(jiān)聽其狀態(tài)變化。
我們一起來看一下Animation這個類,它是一個抽象類:- addListener方法(
監(jiān)聽動畫值的概念)- 建立動畫的狀態(tài)值發(fā)生變化時,動畫都會通知所有通過addListener添加的監(jiān)聽器。
- 通常,一個正在監(jiān)聽的動畫的state對象會調(diào)用自身的setState方法,將自身本身作為這些監(jiān)聽器的插入函數(shù)來通知小部件,系統(tǒng)需要根據(jù)新狀態(tài)值進行重新生成。
- addStatusListener(
監(jiān)聽動畫狀態(tài)的改變)當(dāng)動畫的狀態(tài)發(fā)生變化時,會通知所有通過addStatusListener添加的監(jiān)聽器。
通常情況下,動畫會從dismissed狀態(tài)開始,表示它處于變化區(qū)間的開始點。
舉例來說,從0.0到1.0的動畫在dismissed狀態(tài)時的值應(yīng)該是0.0。動畫進行的下一狀態(tài)可能是forward(例如從0.0到1.0)或者reverse(例如從1.0到0.0)。
-
最終,如果動畫到達其區(qū)間的結(jié)束點(例如1.0),則動畫會變成completed狀態(tài)。
abstractclass Animation<T> extends Listenable implements ValueListenable<T> { const Animation(); // 添加動畫監(jiān)聽器 @override void addListener(VoidCallback listener); // 移除動畫監(jiān)聽器 @override void removeListener(VoidCallback listener); // 添加動畫狀態(tài)監(jiān)聽器 void addStatusListener(AnimationStatusListener listener); // 移除動畫狀態(tài)監(jiān)聽器 void removeStatusListener(AnimationStatusListener listener); // 獲取動畫當(dāng)前狀態(tài) AnimationStatus get status; // 獲取動畫當(dāng)前的值 @override T get value; }
- addListener方法(
-
1.2、AnimationController
Animation是一個抽象類,并不能直接創(chuàng)建對象實現(xiàn)動畫的使用。
AnimationController是Animation的一個子類,實現(xiàn)動畫通常我們需要創(chuàng)建AnimationController對象。- AnimationController會生成一系列的值,交替情況下值是0.0到1.0區(qū)間的值;
除了上面的監(jiān)聽器,獲取動畫的狀態(tài),值之外,AnimationController還提供了對動畫的控制:
- forward:向前執(zhí)行動畫
- 反向:方向播放動畫
- stop:停止動畫
AnimationController的源碼:
class AnimationController extends Animation<double> with AnimationEagerListenerMixin, AnimationLocalListenersMixin, AnimationLocalStatusListenersMixin { AnimationController({ // 初始化值 double value, // 動畫執(zhí)行的時間 this.duration, // 反向動畫執(zhí)行的時間 this.reverseDuration, // 最小值 this.lowerBound = 0.0, // 最大值 this.upperBound = 1.0, // 刷新率ticker的回調(diào)(看下面詳細(xì)解析) @required TickerProvider vsync, }) }- AnimationController有一個必傳的參數(shù)
vsync,它是什么呢?- Flutter的渲染閉環(huán),F(xiàn)lutter每次渲染一幀畫面之前都需要等待一個vsync信號。
- 這里也是為了監(jiān)聽vsync信號,當(dāng)Flutter開發(fā)的應(yīng)用程序不再接受同步信號時(比如鎖屏或退到后臺),那么繼續(xù)執(zhí)行動畫會消耗性能。
- 這個時候我們設(shè)置了Ticker,就不會再出發(fā)動畫了。
- 開發(fā)中比較常見的是將
SingleTickerProviderStateMixin混入到State的定義中。
-
1.3、CurvedAnimation(設(shè)置動畫執(zhí)行的速率-速率曲線)
CurvedAnimation也是Animation的一個實現(xiàn)類,它的目的是為了給AnimationController增加動畫曲線:
CurvedAnimation可以將AnimationController和Curve結(jié)合起來,生成一個新的Animation對象class CurvedAnimation extends Animation<double> with AnimationWithParentMixin<double> { CurvedAnimation({ // 通常傳入一個AnimationController @requiredthis.parent, // Curve類型的對象 @requiredthis.curve, this.reverseCurve, }); }- Curve類型的對象的有一些常量Curves(和Color類型有一些Colors是一樣的),可以供我們直接使用:
- 對應(yīng)值的效果,可以直接查看官網(wǎng)(有對應(yīng)的gif效果,一目了然)
- https://api.flutter.dev/flutter/animation/Curves-class.html
官方也發(fā)表了自己的定義Curse的一個示例
import'dart:math'; class ShakeCurve extends Curve { @override double transform(double t) => sin(t * pi * 2); } - Curve類型的對象的有一些常量Curves(和Color類型有一些Colors是一樣的),可以供我們直接使用:
-
1.4、Tween
默認(rèn)情況下,AnimationController動畫生成的值所在區(qū)間是0.0到1.0
如果希望使用這個以外的值,或者其他的數(shù)據(jù)類型,就需要使用Tween
Tween的源碼:源碼非常簡單,預(yù)設(shè)兩個值即可,可以定義一個范圍。class Tween<T extends dynamic> extends Animatable<T> { // begin 開始值,end 結(jié)束值 Tween({ this.begin, this.end }); }Tween也有一些子類,比如ColorTween、BorderTween,可以針對動畫或者邊框來設(shè)置動畫的值。
Tween.animate
要使用Tween對象,需要調(diào)用Tween的animate()方法,傳入一個Animation對象。
二、動畫案例練習(xí)
-
2.1. 動畫的基本使用(
不可取,優(yōu)缺點)
我們來完成一個案例:點擊案例后執(zhí)行一個心跳動畫,可以反復(fù)執(zhí)行
-
再次點擊可以暫停和重新開始動畫
import 'package:flutter/material.dart'; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, splashColor: Colors.transparent), home: HYHomePage(), ); } } class HYHomePage extends StatefulWidget { @override _HYHomePageState createState() => _HYHomePageState(); } class _HYHomePageState extends State<HYHomePage> with SingleTickerProviderStateMixin { // 創(chuàng)建AnimationController AnimationController _controller; Animation _animation; Animation _sizeAnim; @override void initState() { super.initState(); // 1.創(chuàng)建AnimationController _controller = AnimationController( vsync: this, duration: Duration(seconds: 2) ); // 2.動畫添加Curve效果 _animation = CurvedAnimation(parent: _controller, curve: Curves.linear); // 3.Tween 設(shè)置值的范圍 _sizeAnim = Tween(begin: 50.0, end: 150.0).animate(_animation); // 4.監(jiān)聽動畫值的改變 _controller.addListener(() { setState(() {}); }); // 5.監(jiān)聽動畫的狀態(tài)改變 _controller.addStatusListener((status) { if (status == AnimationStatus.completed) { _controller.reverse(); } else if (status == AnimationStatus.dismissed) { _controller.forward(); } }); } @override Widget build(BuildContext context) { print("執(zhí)行_HYHomePageState的build方法"); return Scaffold( appBar: AppBar( title: Text("首頁"), ), body: return Center( child: Icon(Icons.favorite, color: Colors.red, size: _sizeAnim.value,), ); floatingActionButton: FloatingActionButton( child: Icon(Icons.play_arrow), onPressed: () { if (_controller.isAnimating) { _controller.stop(); print(_controller.status); } else if (_controller.status == AnimationStatus.forward) { _controller.forward(); } else if (_controller.status == AnimationStatus.reverse) { _controller.reverse(); } else { _controller.forward(); } }, ), ); } @override void dispose() { _controller.dispose(); super.dispose(); } }
-
2.2、AnimatedWidget(
不可取,優(yōu)缺點)
在上面的代碼中,我們必須監(jiān)聽動畫值的改變,并且改變后需要調(diào)用setState(也就是上面的第4步),這會帶來兩個問題:- 1.執(zhí)行動畫必須包含這部分代碼,代碼比較冗余
- 2.調(diào)用setState意味著整個State類中的build方法就會被重新build
如何可以優(yōu)化上面的操作:創(chuàng)建一個Widget繼承自AnimatedWidget:
class IconAnimation extends AnimatedWidget { IconAnimation(Animation animation): super(listenable: animation); @override Widget build(BuildContext context) { Animation animation = listenable; return Icon(Icons.favorite, color: Colors.red, size: animation.value,); } }那么2.1中的 的 第四步就可以去掉了,在Icon調(diào)用的地方直接:
IconAnimation(_animation)- 缺點是:1、每次都需要創(chuàng)建一個類,類里面的build也會打??;2、如果創(chuàng)建的Widget有子類,那么子類依然會重復(fù)的build
-
2.3、
AnimatedBuilder(優(yōu)解)
AnimatedBuilder 可以解決上面 AnimatedWidget 產(chǎn)生的兩個問題,代碼如下class _HYHomePageState extends State<HYHomePage> with SingleTickerProviderStateMixin { // 創(chuàng)建AnimationController AnimationController _controller; Animation _animation; @override void initState() { super.initState(); // 1.創(chuàng)建AnimationController _controller = AnimationController( vsync: this, duration: Duration(seconds: 2) ); // 2.設(shè)置Curve的值 _animation = CurvedAnimation(parent: _controller, curve: Curves.linear); // 3.Tween _animation = Tween(begin: 50.0, end: 150.0).animate(_animation); // 監(jiān)聽動畫的狀態(tài)改變 _controller.addStatusListener((status) { if (status == AnimationStatus.completed) { _controller.reverse(); } else if (status == AnimationStatus.dismissed) { _controller.forward(); } }); } @override Widget build(BuildContext context) { print("執(zhí)行_HYHomePageState的build方法"); return Scaffold( appBar: AppBar( title: Text("首頁"), ), body: Center( child: AnimatedBuilder( animation: _controller, builder: (ctx, child) { return Icon(Icons.favorite, color: Colors.red, size: _animation.value,); }, ), ), floatingActionButton: FloatingActionButton( child: Icon(Icons.play_arrow), onPressed: () { if (_controller.isAnimating) { _controller.stop(); print(_controller.status); } else if (_controller.status == AnimationStatus.forward) { _controller.forward(); } else if (_controller.status == AnimationStatus.reverse) { _controller.reverse(); } else { _controller.forward(); } }, ), ); } @override void dispose() { _controller.dispose(); super.dispose(); } }
三、其它動畫補充
-
3.1、交織動畫(多個動畫同時執(zhí)行)、
案例說明:點擊floatingActionButton執(zhí)行動畫
動畫集合了透明度變化、大小變化、顏色變化、旋轉(zhuǎn)動畫等;
我們這里是通過多個Tween生成了多個Animation對象;
代碼如下class HYHomePage extends StatefulWidget { @override _HYHomePageState createState() => _HYHomePageState(); } class _HYHomePageState extends State<HYHomePage> with SingleTickerProviderStateMixin { // 創(chuàng)建AnimationController AnimationController _controller; Animation _animation; // 大小 Animation<double> _sizeAnim; // 顏色 Animation _colorAnim; // 透明度 Animation<double> _opactiyAnim; // 角度 Animation<double> _radiansAnim; @override void initState() { super.initState(); // 1.創(chuàng)建AnimationController _controller = AnimationController( vsync: this, duration: Duration(seconds: 2) ); // 2.設(shè)置Curve的值 _animation = CurvedAnimation(parent: _controller, curve: Curves.linear); // 3.Tween _sizeAnim = Tween(begin: 10.0, end: 150.0).animate(_controller); _colorAnim = ColorTween(begin: Colors.brown, end: Colors.green).animate(_controller); _opactiyAnim = Tween(begin: 0.0, end: 1.0).animate(_controller); _radiansAnim = Tween(begin: 0.0, end: 2 * pi).animate(_controller); // 監(jiān)聽動畫的狀態(tài)改變 _controller.addStatusListener((status) { if (status == AnimationStatus.completed) { _controller.reverse(); } else if (status == AnimationStatus.dismissed) { _controller.forward(); } }); } @override Widget build(BuildContext context) { print("執(zhí)行_HYHomePageState的build方法"); return Scaffold( appBar: AppBar( title: Text("首頁"), ), body: Center( child: AnimatedBuilder( animation: _controller, builder: (ctx, child) { return Opacity( opacity: _opactiyAnim.value, child: Transform( transform: Matrix4.rotationZ(_radiansAnim.value), alignment: Alignment.center, child: Container( width: _sizeAnim.value, height: _sizeAnim.value, color: _colorAnim.value, ), ), ); }, ) ), floatingActionButton: FloatingActionButton( child: Icon(Icons.play_arrow), onPressed: () { if (_controller.isAnimating) { _controller.stop(); print(_controller.status); } else if (_controller.status == AnimationStatus.forward) { _controller.forward(); } else if (_controller.status == AnimationStatus.reverse) { _controller.reverse(); } else { _controller.forward(); } }, ), ); } @override void dispose() { _controller.dispose(); super.dispose(); } } -
3.2、Hero動畫
移動端開發(fā)會經(jīng)常遇到類似這樣的需求:- 點擊一個頭像,顯示頭像的大圖,并且從原來圖像的Rect到大圖的Rect
- 點擊一個商品的圖片,可以展示商品的大圖,并且從原來圖像的Rect到大圖的Rect
這種跨頁面共享的動畫被稱之為享元動畫(Shared Element Transition)
在Flutter中,有一個專門的Widget可以來實現(xiàn)這種動畫效果:Hero
實現(xiàn)Hero動畫,需要如下步驟:- 1.在第一個Page1中,定義一個起始的Hero Widget,被稱之為source hero,并且綁定一個tag;
- 2.在第二個Page2中,定義一個終點的Hero Widget,被稱之為 destination hero,并且綁定相同的tag;
- 3.可以通過Navigator來實現(xiàn)第一個頁面Page1到第二個頁面Page2的跳轉(zhuǎn)過程;
Flutter會設(shè)置Tween來界定Hero從起點到終端的大小和位置,并且在圖層上執(zhí)行動畫效果。
首頁Page的核心代碼:GridView( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, crossAxisSpacing: 6, mainAxisSpacing: 6, childAspectRatio: 16/9 ), children: List.generate(20, (index) { String imageURL = "https://picsum.photos/200/300?random=$index"; return GestureDetector( onTap: () { Navigator.of(context).push(PageRouteBuilder( pageBuilder: (ctx, animation1, animation2) { return FadeTransition( opacity: animation1, child: JKImageDeyail(imageURL), ); }, )); }, child: Hero(tag: imageURL, child: Image.network(imageURL, fit: BoxFit.cover,)), ); }), ),提示:外層包裹了一個:手勢
GestureDetector,跳轉(zhuǎn)用的帶動畫的 PageRouteBuilder,對于跳轉(zhuǎn)的頁面包裹了漸變 FadeTransition
對于展示的 Image 我們包裹了一個 Hero ,對于 Hero 下個頁面也要有 Hero,并且和當(dāng)前的 Hero 的 tag 保持一致圖片展示Page
import 'package:flutter/material.dart'; class JKImageDeyail extends StatelessWidget { final String _imageUrl; JKImageDeyail(this._imageUrl); @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.black, appBar: AppBar( title: Text('圖片詳情'), ), body: Center( child: GestureDetector( onTap: () { Navigator.of(context).pop(); }, child: Hero( tag: _imageUrl, child: Image.network( _imageUrl, width: double.infinity, fit: BoxFit.cover, ), ), ), ), ); } }
