Flutter實現動畫

對於一個前端的App來講,添加適當的動畫,能夠給用戶更好的體驗和視覺效果。因此不管是原生的iOS或Android,仍是前端開發中都會提供完成某些動畫的API。html

Flutter有本身的渲染閉環,咱們固然能夠給它提供必定的數據模型,來讓它幫助咱們實現對應的動畫效果。前端

一. 動畫API認識

動畫實際上是咱們經過某些方式(好比對象,Animation對象)給Flutter引擎提供不一樣的值,而Flutter能夠根據咱們提供的值,給對應的Widget添加順滑的動畫效果。vue

針對動畫這個章節,我打算先理清楚他們的API關係和做用,再來說解如何利用這些API來實現不一樣的動畫效果。web

1.1. Animation

在Flutter中,實現動畫的核心類是Animation,Widget能夠直接將這些動畫合併到本身的build方法中來讀取它們的當前值或者監聽它們的狀態變化。算法

咱們一塊兒來看一下Animation這個類,它是一個抽象類:api

  • addListener方法
    • 每當動畫的狀態值發生變化時,動畫都會通知全部經過 addListener 添加的監聽器。
    • 一般,一個正在監聽動畫的 state對象會調用自身的 setState方法,將自身傳入這些監聽器的回調函數來通知 widget 系統須要根據新狀態值進行從新構建。
  • addStatusListener
    • 當動畫的狀態發生變化時,會通知全部經過 addStatusListener 添加的監聽器。
    • 一般狀況下,動畫會從 dismissed 狀態開始,表示它處於變化區間的開始點。
    • 舉例來講,從 0.0 到 1.0 的動畫在 dismissed 狀態時的值應該是 0.0。
    • 動畫進行的下一狀態多是 forward(好比從 0.0 到 1.0)或者 reverse(好比從 1.0 到 0.0)。
    • 最終,若是動畫到達其區間的結束點(好比 1.0),則動畫會變成 completed 狀態。
abstract class Animation<T> extends Listenable implements ValueListenable<T> {
 const Animation();   // 添加動畫監聽器  @override  void addListener(VoidCallback listener);   // 移除動畫監聽器  @override  void removeListener(VoidCallback listener);   // 添加動畫狀態監聽器  void addStatusListener(AnimationStatusListener listener);   // 移除動畫狀態監聽器  void removeStatusListener(AnimationStatusListener listener);   // 獲取動畫當前狀態  AnimationStatus get status;   // 獲取動畫當前的值  @override  T get value; 複製代碼

1.2. AnimationController

Animation是一個抽象類,並不能用來直接建立對象實現動畫的使用。數據結構

AnimationController是Animation的一個子類,實現動畫一般咱們須要建立AnimationController對象。app

  • AnimationController會生成一系列的值,默認狀況下值是0.0到1.0區間的值;

除了上面的監聽,獲取動畫的狀態、值以外,AnimationController還提供了對動畫的控制:less

  • forward:向前執行動畫
  • reverse:方向播放動畫
  • stop:中止動畫

AnimationController的源碼:編輯器

class AnimationController extends Animation<double>  with AnimationEagerListenerMixin, AnimationLocalListenersMixin, AnimationLocalStatusListenersMixin {  AnimationController({  // 初始化值  double value,  // 動畫執行的時間  this.duration,  // 反向動畫執行的時間  this.reverseDuration,  // 最小值  this.lowerBound = 0.0,  // 最大值  this.upperBound = 1.0,  // 刷新率ticker的回調(看下面詳細解析)  @required TickerProvider vsync,  }) } 複製代碼

AnimationController有一個必傳的參數vsync,它是什麼呢?

  • 以前我講過關於Flutter的渲染閉環,Flutter每次渲染一幀畫面以前都須要等待一個vsync信號。
  • 這裏也是爲了監聽vsync信號,當Flutter開發的應用程序再也不接受同步信號時(好比鎖屏或退到後臺),那麼繼續執行動畫會消耗性能。
  • 這個時候咱們設置了Ticker,就不會再出發動畫了。
  • 開發中比較常見的是將SingleTickerProviderStateMixin混入到State的定義中。

1.3. CurvedAnimation

CurvedAnimation也是Animation的一個實現類,它的目的是爲了給AnimationController增長動畫曲線:

  • CurvedAnimation能夠將AnimationController和Curve結合起來,生成一個新的Animation對象
class CurvedAnimation extends Animation<double> with AnimationWithParentMixin<double> {
 CurvedAnimation({  // 一般傳入一個AnimationController  @required this.parent,  // Curve類型的對象  @required this.curve,  this.reverseCurve,  }); } 複製代碼

Curve類型的對象的有一些常量Curves(和Color類型有一些Colors是同樣的),能夠供咱們直接使用:

  • 對值的效果,能夠直接查看官網(有對應的gif效果,一目瞭然)
  • https://api.flutter.dev/flutter/animation/Curves-class.html

官方也給出了本身定義Curse的一個示例:

import 'dart:math';
 class ShakeCurve extends Curve {  @override  double transform(double t) => sin(t * pi * 2); } 複製代碼

1.4. Tween

默認狀況下,AnimationController動畫生成的值所在區間是0.0到1.0

  • 若是但願使用這個之外的值,或者其餘的數據類型,就須要使用Tween

Tween的源碼:

  • 源碼很是簡單,傳入兩個值便可,能夠定義一個範圍。
class Tween<T extends dynamic> extends Animatable<T> {
 Tween({ this.begin, this.end }); } 複製代碼

Tween也有一些子類,好比ColorTween、BorderTween,能夠針對動畫或者邊框來設置動畫的值。

Tween.animate

要使用Tween對象,須要調用Tween的animate()方法,傳入一個Animation對象。

二. 動畫案例練習

2.1. 動畫的基本使用

咱們來完成一個案例:

  • 點擊案例後執行一個心跳動畫,能夠反覆執行
  • 再次點擊能夠暫停和從新開始動畫
動畫效果
動畫效果
class HYHomePage extends StatelessWidget {
 final GlobalKey<_AnimationDemo01State> demo01Key = GlobalKey();   @override  Widget build(BuildContext context) {  return Scaffold(  appBar: AppBar(  title: Text("動畫測試"),  ),  body: AnimationDemo01(key: demo01Key),  floatingActionButton: FloatingActionButton(  child: Icon(Icons.play_circle_filled),  onPressed: () {  if (!demo01Key.currentState.controller.isAnimating) {  demo01Key.currentState.controller.forward();  } else {  demo01Key.currentState.controller.stop();  }  },  ),  );  } }  class AnimationDemo01 extends StatefulWidget {  AnimationDemo01({Key key}): super(key: key);   @override  _AnimationDemo01State createState() => _AnimationDemo01State(); }  class _AnimationDemo01State extends State<AnimationDemo01> with SingleTickerProviderStateMixin {  AnimationController controller;  Animation<double> animation;   @override  void initState() {  super.initState();   // 1.建立AnimationController  controller = AnimationController(duration: Duration(seconds: 1), vsync: this);  // 2.動畫添加Curve效果  animation = CurvedAnimation(parent: controller, curve: Curves.elasticInOut, reverseCurve: Curves.easeOut);  // 3.監聽動畫  animation.addListener(() {  setState(() {});  });  // 4.控制動畫的翻轉  animation.addStatusListener((status) {  if (status == AnimationStatus.completed) {  controller.reverse();  } else if (status == AnimationStatus.dismissed) {  controller.forward();  }  });  // 5.設置值的範圍  animation = Tween(begin: 50.0, end: 120.0).animate(controller);  }   @override  Widget build(BuildContext context) {  return Center(  child: Icon(Icons.favorite, color: Colors.red, size: animation.value,),  );  }   @override  void dispose() {  controller.dispose();  super.dispose();  } } 複製代碼

2.2. AnimatedWidget

在上面的代碼中,咱們必須監聽動畫值的改變,而且改變後須要調用setState,這會帶來兩個問題:

  • 1.執行動畫必須包含被部分代碼,代碼比較冗餘
  • 2.調用setState意味着整個State類中的build方法就會被從新build

如何能夠優化上面的操做呢?AnimatedWidget

建立一個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,);  } } 複製代碼

修改_AnimationDemo01State中的代碼:

class _AnimationDemo01State extends State<AnimationDemo01> with SingleTickerProviderStateMixin {
 AnimationController controller;  Animation<double> animation;   @override  void initState() {  super.initState();   // 1.建立AnimationController  controller = AnimationController(duration: Duration(seconds: 1), vsync: this);  // 2.動畫添加Curve效果  animation = CurvedAnimation(parent: controller, curve: Curves.elasticInOut, reverseCurve: Curves.easeOut);  // 3.監聽動畫  // 4.控制動畫的翻轉  animation.addStatusListener((status) {  if (status == AnimationStatus.completed) {  controller.reverse();  } else if (status == AnimationStatus.dismissed) {  controller.forward();  }  });  // 5.設置值的範圍  animation = Tween(begin: 50.0, end: 120.0).animate(controller);  }   @override  Widget build(BuildContext context) {  return Center(  child: IconAnimation(animation),  );  }   @override  void dispose() {  controller.dispose();  super.dispose();  } } 複製代碼

2.3. AnimatedBuilder

Animated是否是最佳的解決方案呢?

  • 1.咱們每次都要新建一個類來繼承自AnimatedWidget

  • 2.若是咱們的動畫Widget有子Widget,那麼意味着它的子Widget也會從新build

如何能夠優化上面的操做呢?AnimatedBuilder

class _AnimationDemo01State extends State<AnimationDemo01> with SingleTickerProviderStateMixin {
 AnimationController controller;  Animation<double> animation;   @override  void initState() {  super.initState();   // 1.建立AnimationController  controller = AnimationController(duration: Duration(seconds: 1), vsync: this);  // 2.動畫添加Curve效果  animation = CurvedAnimation(parent: controller, curve: Curves.elasticInOut, reverseCurve: Curves.easeOut);  // 3.監聽動畫  // 4.控制動畫的翻轉  animation.addStatusListener((status) {  if (status == AnimationStatus.completed) {  controller.reverse();  } else if (status == AnimationStatus.dismissed) {  controller.forward();  }  });  // 5.設置值的範圍  animation = Tween(begin: 50.0, end: 120.0).animate(controller);  }   @override  Widget build(BuildContext context) {  return Center(  child: AnimatedBuilder(  animation: animation,  builder: (ctx, child) {  return Icon(Icons.favorite, color: Colors.red, size: animation.value,);  },  ),  );  }   @override  void dispose() {  controller.dispose();  super.dispose();  } } 複製代碼

三. 其它動畫補充

3.1. 交織動畫

案例說明:

  • 點擊floatingActionButton執行動畫
  • 動畫集合了透明度變化、大小變化、顏色變化、旋轉動畫等;
  • 咱們這裏是經過多個Tween生成了多個Animation對象;
交織動畫
交織動畫
import 'dart:math';
 import 'package:flutter/material.dart';  void main() => runApp(MyApp());   class MyApp extends StatelessWidget {  // This widget is the root of your application.  @override  Widget build(BuildContext context) {  return MaterialApp(  title: 'Flutter Demo',  theme: ThemeData(  primarySwatch: Colors.blue, splashColor: Colors.transparent),  home: HYHomePage(),  );  } }  class HYHomePage extends StatelessWidget {  final GlobalKey<_AnimationDemo01State> demo01Key = GlobalKey();   @override  Widget build(BuildContext context) {  return Scaffold(  appBar: AppBar(  title: Text("列表測試"),  ),  body: AnimationDemo01(key: demo01Key),  floatingActionButton: FloatingActionButton(  child: Icon(Icons.play_circle_filled),  onPressed: () {  demo01Key.currentState.controller.forward();  },  ),  );  } }  class AnimationDemo01 extends StatefulWidget {  AnimationDemo01({Key key}): super(key: key);   @override  _AnimationDemo01State createState() => _AnimationDemo01State(); }  class _AnimationDemo01State extends State<AnimationDemo01> with SingleTickerProviderStateMixin {  AnimationController controller;  Animation<double> animation;   Animation<Color> colorAnim;  Animation<double> sizeAnim;  Animation<double> rotationAnim;   @override  void initState() {  super.initState();   // 1.建立AnimationController  controller = AnimationController(duration: Duration(seconds: 2), vsync: this);  // 2.動畫添加Curve效果  animation = CurvedAnimation(parent: controller, curve: Curves.easeIn);  // 3.監聽動畫  animation.addListener(() {  setState(() {});  });   // 4.設置值的變化  colorAnim = ColorTween(begin: Colors.blue, end: Colors.red).animate(controller);  sizeAnim = Tween(begin: 0.0, end: 200.0).animate(controller);  rotationAnim = Tween(begin: 0.0, end: 2*pi).animate(controller);  }   @override  Widget build(BuildContext context) {  return Center(  child: Opacity(  opacity: animation.value,  child: Transform(  alignment: Alignment.center,  transform: Matrix4.rotationZ(animation.value),  child: Container(  width: sizeAnim.value,  height: sizeAnim.value,  color: colorAnim.value,  alignment: Alignment.center,  ),  ),  ),  );  }   @override  void dispose() {  controller.dispose();  super.dispose();  } } 複製代碼

固然,咱們可使用Builder來對代碼進行優化

@override
 Widget build(BuildContext context) {  return Center(  child: AnimatedBuilder(  animation: controller,  builder: (ctx, child) {  return Opacity(  opacity: animation.value,  child: Transform(  alignment: Alignment.center,  transform: Matrix4.rotationZ(rotationAnim.value),  child: Container(  width: sizeAnim.value,  height: sizeAnim.value,  color: colorAnim.value,  alignment: Alignment.center,  ),  ),  );  },  ),  );  } 複製代碼

3.2. Hero動畫

移動端開發會常常遇到相似這樣的需求:

  • 點擊一個頭像,顯示頭像的大圖,而且從原來圖像的Rect到大圖的Rect
  • 點擊一個商品的圖片,能夠展現商品的大圖,而且從原來圖像的Rect到大圖的Rect

這種跨頁面共享的動畫被稱之爲享元動畫(Shared Element Transition)

在Flutter中,有一個專門的Widget能夠來實現這種動畫效果:Hero

實現Hero動畫,須要以下步驟:

  • 1.在第一個Page1中,定義一個起始的Hero Widget,被稱之爲source hero,而且綁定一個tag;
  • 2.在第二個Page2中,定義一個終點的Hero Widget,被稱之爲 destination hero,而且綁定相同的tag;
  • 3.能夠經過Navigator來實現第一個頁面Page1到第二個頁面Page2的跳轉過程;

Flutter會設置Tween來界定Hero從起點到終端的大小和位置,而且在圖層上執行動畫效果。

首頁Page代碼:

首頁Page
首頁Page
import 'dart:math';
 import 'package:flutter/material.dart'; import 'package:testflutter001/animation/image_detail.dart';  void main() => runApp(MyApp());  class MyApp extends StatelessWidget {  // This widget is the root of your application.  @override  Widget build(BuildContext context) {  return MaterialApp(  title: 'Flutter Demo',  theme: ThemeData(  primarySwatch: Colors.blue, splashColor: Colors.transparent),  home: HYHomePage(),  );  } }  class HYHomePage extends StatelessWidget {  @override  Widget build(BuildContext context) {  return Scaffold(  appBar: AppBar(  title: Text("Hero動畫"),  ),  body: HYHomeContent(),  );  } }  class HYHomeContent extends StatelessWidget {  @override  Widget build(BuildContext context) {  return GridView(  gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(  crossAxisCount: 2,  crossAxisSpacing: 8,  mainAxisSpacing: 8,  childAspectRatio: 2  ),  children: List.generate(20, (index) {  String imageURL = "https://picsum.photos/id/$index/400/200";  return GestureDetector(  onTap: () {  Navigator.of(context).push(PageRouteBuilder(  pageBuilder: (ctx, animation, animation2) {  return FadeTransition(  opacity: animation,  child: HYImageDetail(imageURL),  );  }  ));  },  child: Hero(  tag: imageURL,  child: Image.network(imageURL)  ),  );  }),  );  } } 複製代碼

圖片展現Page

圖片展現Page
圖片展現Page
import 'package:flutter/material.dart';
 class HYImageDetail extends StatelessWidget {  final String imageURL;   HYImageDetail(this.imageURL);   @override  Widget build(BuildContext context) {  return Scaffold(  backgroundColor: Colors.black,  body: Center(  child: GestureDetector(  onTap: () {  Navigator.of(context).pop();  },  child: Hero(  tag: imageURL,  child: Image.network(  this.imageURL,  width: double.infinity,  fit: BoxFit.cover,  ),  )),  ),  );  } } 複製代碼

備註:全部內容首發於公衆號,以後除了Flutter也會更新其餘技術文章,TypeScript、React、Node、uniapp、mpvue、數據結構與算法等等,也會更新一些本身的學習心得等,歡迎你們關注

公衆號
公衆號
相關文章
相關標籤/搜索