概述
- 動畫API認識
- 動畫案例練習
- 其它動畫補充
一、動畫API認識
動畫實際上是我們通過某些方式(某種對象茬射,Animation對象)給Flutter引擎提供不同的值却音,而Flutter可以根據(jù)我們提供的值,給對應的小部件添加順滑的動畫效果焕檬。
-
1.1宴霸、Animation
在Flutter中囱晴,實現(xiàn)動畫的核心類是動畫,小部件可以直接將這些動畫合并到自己的構建方法中來讀取它們的當前值或監(jiān)聽其狀態(tài)變化瓢谢。
我們一起來看一下Animation
這個類畸写,它是一個抽象類
:- addListener方法(
監(jiān)聽動畫值的概念
)- 建立動畫的狀態(tài)值發(fā)生變化時,動畫都會通知所有通過addListener添加的監(jiān)聽器氓扛。
- 通常枯芬,一個正在監(jiān)聽的動畫的state對象會調用自身的setState方法,將自身本身作為這些監(jiān)聽器的插入函數(shù)來通知小部件采郎,系統(tǒng)需要根據(jù)新狀態(tài)值進行重新生成千所。
- addStatusListener(
監(jiān)聽動畫狀態(tài)的改變
)當動畫的狀態(tài)發(fā)生變化時,會通知所有通過addStatusListener添加的監(jiān)聽器蒜埋。
通常情況下淫痰,動畫會從dismissed狀態(tài)開始,表示它處于變化區(qū)間的開始點整份。
舉例來說待错,從0.0到1.0的動畫在dismissed狀態(tài)時的值應該是0.0。動畫進行的下一狀態(tài)可能是forward(例如從0.0到1.0)或者reverse(例如從1.0到0.0)烈评。
-
最終火俄,如果動畫到達其區(qū)間的結束點(例如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); // 獲取動畫當前狀態(tài) AnimationStatus get status; // 獲取動畫當前的值 @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的回調(看下面詳細解析) @required TickerProvider vsync, }) }
- AnimationController有一個必傳的參數(shù)
vsync
,它是什么呢肌毅?- Flutter的渲染閉環(huán)筷转,F(xiàn)lutter每次渲染一幀畫面之前都需要等待一個vsync信號。
- 這里也是為了監(jiān)聽vsync信號悬而,當Flutter開發(fā)的應用程序不再接受同步信號時(比如鎖屏或退到后臺)呜舒,那么繼續(xù)執(zhí)行動畫會消耗性能。
- 這個時候我們設置了Ticker笨奠,就不會再出發(fā)動畫了袭蝗。
- 開發(fā)中比較常見的是將
SingleTickerProviderStateMixin
混入到State的定義中。
-
1.3般婆、CurvedAnimation(設置動畫執(zhí)行的速率-速率曲線)
CurvedAnimation也是Animation的一個實現(xiàn)類到腥,它的目的是為了給AnimationController增加動畫曲線:
CurvedAnimation可以將AnimationController
和Curve
結合起來,生成一個新的Animation對象class CurvedAnimation extends Animation<double> with AnimationWithParentMixin<double> { CurvedAnimation({ // 通常傳入一個AnimationController @requiredthis.parent, // Curve類型的對象 @requiredthis.curve, this.reverseCurve, }); }
- Curve類型的對象的有一些常量Curves(和Color類型有一些Colors是一樣的)蔚袍,可以供我們直接使用:
- 對應值的效果乡范,可以直接查看官網(wǎ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
默認情況下晋辆,AnimationController動畫生成的值所在區(qū)間是0.0到1.0
如果希望使用這個以外的值,或者其他的數(shù)據(jù)類型宇整,就需要使用Tween
Tween的源碼:源碼非常簡單瓶佳,預設兩個值即可,可以定義一個范圍鳞青。class Tween<T extends dynamic> extends Animatable<T> { // begin 開始值霸饲,end 結束值 Tween({ this.begin, this.end }); }
Tween也有一些子類,比如ColorTween盼玄、BorderTween贴彼,可以針對動畫或者邊框來設置動畫的值。
Tween.animate
要使用Tween對象埃儿,需要調用Tween的animate()
方法器仗,傳入一個Animation對象。
二、動畫案例練習
-
2.1. 動畫的基本使用(
不可取精钮,優(yōu)缺點
)
我們來完成一個案例:點擊案例后執(zhí)行一個心跳動畫威鹿,可以反復執(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 設置值的范圍 _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)聽動畫值的改變,并且改變后需要調用setState(也就是上面的第4步)臂容,這會帶來兩個問題:- 1.執(zhí)行動畫必須包含這部分代碼科雳,代碼比較冗余
- 2.調用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調用的地方直接:
IconAnimation(_animation)
- 缺點是:1脓杉、每次都需要創(chuàng)建一個類糟秘,類里面的build也會打印球散;2尿赚、如果創(chuàng)建的Widget有子類,那么子類依然會重復的build
-
2.3蕉堰、
AnimatedBuilder
(優(yōu)解)
AnimatedBuilder 可以解決上面 AnimatedWidget 產生的兩個問題凌净,代碼如下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.設置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í)行動畫
動畫集合了透明度變化
、大小變化
丑婿、顏色變化
性雄、旋轉動畫
等;
我們這里是通過多個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.設置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的跳轉過程导披;
Flutter會設置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
撩匕,跳轉用的帶動畫的 PageRouteBuilder鹰晨,對于跳轉的頁面包裹了漸變 FadeTransition
對于展示的 Image 我們包裹了一個 Hero ,對于 Hero 下個頁面也要有 Hero止毕,并且和當前的 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, ), ), ), ), ); } }