Flutter動畫之自定義動畫元件-FlutterLayout

張風捷特烈發表於2019-07-19
前言:

本文將自定義一個FlutterWidget的動畫元件,Flutter有顫動的意思
在此之前會講一下AnimatedWidget與AnimatedBuilder是什麼,如何使用
所以本文是一篇挺重要的文章,不僅是內容,還有思想和靈魂。
今天也悟到了一段話分享給大家:

當你遇到一群共事之人,開始難免會覺得某某人高冷而帥氣,某某人美麗而大方,某某人能力超級強  
作為普通人的你也許很想和他們結交但又很難進入他們的世界,於是你在角落靜靜凝望,細心觀察
隨著時間的流逝,也許偶爾的交談,你會發現他們並非看上去的那麼難以接近,於是開始和他們交流  
隨著關係的加深,也許某個傍晚,你們會走在回去的路上,訴說著人生,從此漸漸無話不說。
然後會發現,這世間的隔閡也許只是自己為自己施加的屏障,這個屏障會為你抵禦傷害,
但它同時也可能讓你失去一個對的人,一個未來的止步於陌生的知己。

學習亦如此,一個框架就是那個高冷而帥氣公子,一個類就是那個美麗而大方姑娘,結合上面再看看。  
有時候錯過了,也就錯過了,你不可能認識所有的人,但你可以用真誠選擇一位知己。
認識的人當然越多越好,但知己,寧缺毋濫。 ----XXX,你現在還好嗎? 
                                                    (張風捷特烈 2019.7.19 字)
複製程式碼

首先,留圖鎮樓

Flutter動畫之自定義動畫元件-FlutterLayout

Flutter動畫之自定義動畫元件-FlutterLayout


1.AnimatedWidget與AnimatedBuilder

1.1:前情回顧

現在回到昨天的最後一個元件,這樣寫不夠優雅,什麼東西都在一塊
Flutter中提供了AnimatedWidget類可以讓動畫的元件更加簡潔

Flutter動畫之自定義動畫元件-FlutterLayout

class FlutterText extends StatefulWidget {
  var str;
  var style;

  FlutterText(this.str, this.style);
  _FlutterTextState createState() => _FlutterTextState();
}

class _FlutterTextState extends State<FlutterText>
    with SingleTickerProviderStateMixin {
  Animation<double> animation;
  AnimationController controller;

  initState() {
    super.initState();
    controller = AnimationController(
        duration: const Duration(milliseconds: 1000), vsync: this);
    
    animation = TweenSequence<double>([//使用TweenSequence進行多組補間動畫
      TweenSequenceItem<double>(tween: Tween(begin: 0, end: 15), weight: 1),
      TweenSequenceItem<double>(tween: Tween(begin: 15, end: 0), weight: 2),
      TweenSequenceItem<double>(tween: Tween(begin: 0, end: -15), weight: 3),
      TweenSequenceItem<double>(tween: Tween(begin: -15, end: 0), weight: 4),
    ]).animate(controller)
      ..addListener(() {
        setState(() {});
      })
      ..addStatusListener((s) {
        if (s == AnimationStatus.completed) {
          setState(() {});
        }
      });
    controller.forward();
  }

  Widget build(BuildContext context) {
    var result = Transform(
      transform: Matrix4.rotationZ(animation.value * pi / 180),
      alignment: Alignment.center,
      child: Text(
        widget.str,
        style: widget.style,
      ),
    );
    return result;
  }
  dispose() {
    controller.dispose();
    super.dispose();
  }
}
複製程式碼

2.使用AnimatedWidget抽離元件

AnimatedWidget也不是什麼神奇的東西,它的優勢在於:
將元件的建立邏輯單獨封裝在一個類中,而且不用再呼叫setState方法,也能自動更新資訊

Flutter動畫之自定義動畫元件-FlutterLayout

class FlutterText extends StatefulWidget {
  var str;
  var style;
  FlutterText(this.str, this.style);
  _FlutterTextState createState() => _FlutterTextState();
}
class _FlutterTextState extends State<FlutterText>
    with SingleTickerProviderStateMixin {
  Animation<double> animation;
  AnimationController controller;
  initState() {
    super.initState();
    
    controller = AnimationController(
        duration: const Duration(milliseconds: 1000), vsync: this);
    animation = TweenSequence<double>([//使用TweenSequence進行多組補間動畫
      TweenSequenceItem<double>(tween: Tween(begin: 0, end: 15), weight: 1),
      TweenSequenceItem<double>(tween: Tween(begin: 15, end: 0), weight: 2),
      TweenSequenceItem<double>(tween: Tween(begin: 0, end: -15), weight: 3),
      TweenSequenceItem<double>(tween: Tween(begin: -15, end: 0), weight: 4),
    ]).animate(controller);
    controller.forward();
  }
  Widget build(BuildContext context) {
    return AnimateWidget(animation: animation);
  }
  
  dispose() {
    controller.dispose();
    super.dispose();
  }
}

class AnimateWidget extends AnimatedWidget{
  AnimateWidget({Key key, Animation<double> animation})
      : super(key: key, listenable: animation);
  @override
  Widget build(BuildContext context) {
    final Animation<double> animation = listenable;
    var result = Transform(
      transform: Matrix4.rotationZ(animation.value * pi / 180),
      alignment: Alignment.center,
      child: Text(
        "捷",
        style: TextStyle(fontSize: 50),
      ),
    );
    return result;
  }
}
複製程式碼

可以看出程式碼明確了很多,AnimateWidget專門負責Widget的構建
FlutterText只注重Animation構成,分工明確,易於複用、維護和擴充


3.使用AnimatedBuilder抽離動畫

AnimatedWidget不挺好的嗎,又來一個AnimatedBuilder什麼鬼
AnimateWidget負責元件的抽離,可以看出元件中雜糅了動畫邏輯
而AnimatedBuilder恰好相反,它不在意元件是什麼,只是將動畫抽離達到複用簡單
這樣針對不同的元件,都可以產生同樣的動畫效果,比如傳入一個Image

Flutter動畫之自定義動畫元件-FlutterLayout

class FlutterText extends StatefulWidget {
  final Widget child;
  FlutterText({this.child});
  _FlutterTextState createState() => _FlutterTextState();
}

class _FlutterTextState extends State<FlutterText>
    with SingleTickerProviderStateMixin {
  Animation<double> animation;
  AnimationController controller;

  initState() {
    super.initState();
    controller = AnimationController(
        duration: const Duration(milliseconds: 1000), vsync: this);

    animation = TweenSequence<double>([ //使用TweenSequence進行多組補間動畫
      TweenSequenceItem<double>(tween: Tween(begin: 0, end: 15), weight: 1),
      TweenSequenceItem<double>(tween: Tween(begin: 15, end: 0), weight: 2),
      TweenSequenceItem<double>(tween: Tween(begin: 0, end: -15), weight: 3),
      TweenSequenceItem<double>(tween: Tween(begin: -15, end: 0), weight: 4),
    ]).animate(controller);
    controller.forward();
  }

  Widget build(BuildContext context) {
    return FlutterAnim(animation: animation,child: widget.child,);
  }

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

class FlutterAnim extends StatelessWidget {
  FlutterAnim({this.child, this.animation});
  final Widget child;
  final Animation<double> animation;
  Widget build(BuildContext context) {
    var result = AnimatedBuilder(
          animation: animation,
          builder: (BuildContext context, Widget child) {
            return new Transform(
                transform: Matrix4.rotationZ(animation.value * pi / 180),
                alignment: Alignment.center,
                child: this.child);
          },
    );
    return Center(child: result,);
  }
}

---->[使用]----
var child = Image(
  image: AssetImage("images/icon_head.png"),
);
var scaffold = Scaffold(
  body: Center(child: FlutterText(child: child),),
);

var app = MaterialApp(
  title: 'Flutter Demo',
  theme: ThemeData(
    primarySwatch: Colors.blue,
  ),
  home:scaffold,
);

void main() => runApp(app);
複製程式碼

可以看到,現在不止針對於文字,對於所有的Widget都有效,實現了功能的更高層抽象。


2.元件之所為元件

2.1:元件是什麼

模組化的思想大家應該都聽過,為了讓已有程式碼更好複用,將專案拆成不同模組
元件也是這樣,對於一個頁面,便是元件的組合,可以拆裝,拼湊和批量生成
在程式碼中我們可以很輕易的將多個控制元件批量動效。比如一段話的每個字都有效果:

Flutter動畫之自定義動畫元件-FlutterLayout

_formChild(String str) {
  var li = <Widget>[];
  for (var i = 0; i < str.length; i++) {
    li.add(FlutterText(child: Text(str[i],style: TextStyle(fontSize: 30),),
    ));
  }
  return li;
}

var textZone=Row(children:_formChild("程式碼,改變生活"),mainAxisSize: MainAxisSize.min,);
複製程式碼

使用_formChild批量生成單個文字,每個文字都加有抖動的光環,所以呈現每個字都抖動的效果


2.2:FlutterText的修改與封裝

現在類名叫FlutterText有點不妥了,它包含一個孩子,可以讓其中的孩子抖動,改名:FlutterLayout 那現在想讓每個文字都抖一下,每次都寫這麼多也不爽,所以可以單獨封裝一下
這裡FlutterText繼承自Text,並定義所有屬性。在build方法裡生成剛才的帶有顫動效果的元件

Flutter動畫之自定義動畫元件-FlutterLayout

class FlutterText extends Text {

  const FlutterText(
    this.data, {
    Key key,
    this.style,
    this.strutStyle,
    this.textAlign,
    this.textDirection,
    this.locale,
    this.softWrap,
    this.overflow,
    this.textScaleFactor,
    this.maxLines,
    this.semanticsLabel,
    this.textWidthBasis,
  }) : super(data);

  final String data;
  final TextStyle style;
  final StrutStyle strutStyle;
  final TextAlign textAlign;
  final TextDirection textDirection;
  final Locale locale;
  final bool softWrap;
  final TextOverflow overflow;
  final double textScaleFactor;
  final int maxLines;
  final String semanticsLabel;
  final TextWidthBasis textWidthBasis;

  @override
  Widget build(BuildContext context) {
    var textZone = Row(
      children: _formChild(data),
      crossAxisAlignment: CrossAxisAlignment.start,
      mainAxisSize: MainAxisSize.min,
    );

    return textZone;
  }

  _formChild(String str) {
    var li = <Widget>[];
    for (var i = 0; i < str.length; i++) {
      li.add(FlutterLayout(
        child: Text(
          str[i],
          style: style,
          strutStyle: strutStyle,
          textAlign: textAlign,
          textDirection: textDirection,
          locale: locale,
          softWrap: softWrap,
          overflow: overflow,
          textScaleFactor: textScaleFactor,
          maxLines: maxLines,
          semanticsLabel: semanticsLabel,
          textWidthBasis: textWidthBasis,
        ),
      ));
    }
    return li;
  }
}

複製程式碼

2.3:FlutterText的使用

你可以完全當它是一個Text來用,只不過有個抖動的效果

Flutter動畫之自定義動畫元件-FlutterLayout

var child = Image(
  image: AssetImage("images/icon_head.png"),
);

var text = FlutterText("程式碼,改變生活", style: TextStyle(
    color: Colors.blue,
    fontSize: 30,
    letterSpacing: 3
),);

var scaffold = Scaffold(
  body: Center(child: Column(mainAxisAlignment: MainAxisAlignment.center,
    children: <Widget>[child, text],
  ),),
);

var app = MaterialApp(
  title: 'Flutter Demo',
  theme: ThemeData(
    primarySwatch: Colors.blue,
  ),
  home: scaffold,
);

void main() => runApp(app);
複製程式碼

這樣一個抖動的Text就完成了,本文結束了嗎?不,才剛剛開始。


2.升級FlutterLayout的功能

Flutter動畫之自定義動畫元件-FlutterLayout

2.1.抖動樣式:RockMode

分上下抖動,左右抖動,搖擺抖動,隨機抖動

enum RockMode {
  random, //隨機
  up_down, //上下
  left_right, //左右
  lean //傾斜
}
複製程式碼

2.2.定義配置引數:AnimConfig
class AnimConfig {//動畫配置
  int duration;//時長
  double offset;//偏移大小
  RockMode mode;//搖晃模式
  AnimConfig({this.duration, this.offset, this.mode});
}
複製程式碼

2.3.FlutterLayout具體實現

這裡只是把常量配置引數化,在生成_formTransform的時候根據模式來生成

class FlutterLayout extends StatefulWidget {
  final Widget child;
  final AnimConfig config;
  FlutterLayout({this.child, this.config});
  _FlutterLayoutState createState() => _FlutterLayoutState();
}
class _FlutterLayoutState extends State<FlutterLayout>
    with SingleTickerProviderStateMixin {
  Animation<double> animation;
  AnimationController controller;
  initState() {
    super.initState();
    controller = AnimationController(
        duration: Duration(milliseconds: widget.config.duration), vsync: this);
    var dx = widget.config.offset;
    var sequence = TweenSequence<double>([
      //使用TweenSequence進行多組補間動畫
      TweenSequenceItem<double>(tween: Tween(begin: 0, end: dx), weight: 1),
      TweenSequenceItem<double>(tween: Tween(begin: dx, end: -dx), weight: 2),
      TweenSequenceItem<double>(tween: Tween(begin: -dx, end: dx), weight: 3),
      TweenSequenceItem<double>(tween: Tween(begin: dx, end: 0), weight: 4),
    ]);
    animation = sequence.animate(controller)
      ..addStatusListener((s) {
        if (s == AnimationStatus.completed) {}
      });
    controller.forward();
  }
  Widget build(BuildContext context) {
    return FlutterAnim(
        animation: animation, child: widget.child, config: widget.config);
  }
  dispose() {
    controller.dispose();
    super.dispose();
  }
}
class FlutterAnim extends StatelessWidget {
  FlutterAnim({this.child, this.animation, this.config});
  Random random = Random();
  final Widget child;
  final Animation<double> animation;
  final AnimConfig config;
  Widget build(BuildContext context) {
    var result = AnimatedBuilder(
      animation: animation,
      builder: (BuildContext context, Widget child) {
        return new Transform(
            transform: _formTransform(config),
            alignment: Alignment.center,
            child: this.child);
      },
    );
    return Center(
      child: result,
    );
  }
  _formTransform(AnimConfig config) {//分類獲取
    var result;
    switch (config.mode) {
      case RockMode.random:
        result = Matrix4.rotationZ(animation.value * pi / 180);
        break;
      case RockMode.up_down:
        result = Matrix4.translationValues(0, animation.value*pow(-1, random.nextInt(20)), 0);
        break;
      case RockMode.left_right:
        result = Matrix4.translationValues(animation.value*pow(-1, random.nextInt(20)), 0, 0);
        break;
      case RockMode.lean:
        result = Matrix4.rotationZ(animation.value * pi / 180);
        break;
    }
    return result;
  }
}
複製程式碼

2.4.FlutterText的修改
class FlutterText extends Text {

  FlutterText(this.data, {
    //略同...
    this.config,
  }) : super(data);
  final AnimConfig config;
  Random random = Random();

  _formChild(String str) {
    var li = <Widget>[];
    for (var i = 0; i < str.length; i++) {
      li.add(FlutterLayout(
        config: AnimConfig(duration: config.duration,offset: config.offset,mode: _dealRandom()),
        child: Text(
            //略同...
        ),
      ));
    }
    return li;
  }

  RockMode _dealRandom() {
    var modes = [RockMode.lean, RockMode.up_down, RockMode.left_right];
    return modes[random.nextInt(3)];
  }
}
複製程式碼

2.5:使用MultiShower測試一下

關於MultiShower,可以看一下Flutter自定義元件-MultiShower,主要用於批量產生不同配置的同類元件

Flutter動畫之自定義動畫元件-FlutterLayout

var configs=<AnimConfig>[
  AnimConfig(duration: 1000,offset: 4,mode: RockMode.random),
  AnimConfig(duration: 1000,offset: 4,mode: RockMode.up_down),
  AnimConfig(duration: 1000,offset: 4,mode: RockMode.left_right),
  AnimConfig(duration: 1000,offset: 5,mode: RockMode.lean),
];

var configsInfo=["random","up_down","left_right","lean"];

var show = MultiShower(configs,(config) =>FlutterText("程式碼,改變生活",
  config:config,
  style: TextStyle(
      color: Colors.blue,
      fontSize: 30,
      letterSpacing: 3
  ),),infos: configsInfo,width: 250,color: Colors.transparent,);

var scaffold = Scaffold(
  body: Center(child: show,)
);
複製程式碼

另外還有我們的FlutterLayout,可以包含任意元件,那Image來測試

Flutter動畫之自定義動畫元件-FlutterLayout

var child = Image(
  image: AssetImage("images/icon_head.png"),
);

var configs=<AnimConfig>[
  AnimConfig(duration: 1000,offset: 4,mode: RockMode.up_down),
  AnimConfig(duration: 1000,offset: 4,mode: RockMode.left_right),
  AnimConfig(duration: 1000,offset: 5,mode: RockMode.lean),
];

var configsInfo=["up_down","left_right","lean"];

var show = MultiShower(configs,(config) =>FlutterLayout(child: child,
  config:config,
 ),infos: configsInfo,width: 200,color: Colors.transparent,);

var scaffold = Scaffold(
  body: Center(child: show,)
);
複製程式碼

好了,到這也差不多了,你以為結束了,稍安勿躁,還有一點


3.增加運動曲線

可以用曲線補間來讓動畫的執行不那麼古板

Flutter動畫之自定義動畫元件-FlutterLayout


3.1:程式碼修改
class AnimConfig {//動畫配置
  int duration;//時長
  double offset;//偏移大小
  RockMode mode;//搖晃模式
  CurveTween curveTween;//運動曲線
  AnimConfig({this.duration, this.offset, this.mode,this.curveTween});
}

class _FlutterLayoutState extends State<FlutterLayout>
    with SingleTickerProviderStateMixin {

    var curveTween = widget.config.curveTween;
    animation = sequence.animate(curveTween==null?controller:curveTween.animate(controller))
      ..addStatusListener((s) {
        if (s == AnimationStatus.completed) {}
      });
複製程式碼

3.2:MultiShower測試

Curves內建四十幾種曲線,這裡就隨便挑一些,你也可以用MultiShower自己玩一玩

var child = Image(
  image: AssetImage("images/icon_head.png"),
);

var configs = <CurveTween>[
  CurveTween(curve: Curves.bounceIn),
  CurveTween(curve: Curves.bounceInOut),
  CurveTween(curve: Curves.bounceOut),
  CurveTween(curve: Curves.decelerate),
  CurveTween(curve: Curves.ease),
  CurveTween(curve: Curves.easeIn),
  CurveTween(curve: Curves.easeInBack),
  CurveTween(curve: Curves.easeInCirc),
  CurveTween(curve: Curves.easeInCubic),
  CurveTween(curve: Curves.easeInExpo),
  CurveTween(curve: Curves.easeInOut),
  CurveTween(curve: Curves.easeInOutBack),
  CurveTween(curve: Curves.easeOut),
  CurveTween(curve: Curves.easeOutBack),
  CurveTween(curve: Curves.linear),
  CurveTween(curve: Curves.linearToEaseOut),
];

var configsInfo = <String>[
  "bounceIn","bounceInOut","bounceOut","decelerate",
  "ease","easeIn","easeInBack","easeInCirc","easeInCubic",
  "easeInExpo","easeInOut","easeInOutBack",
  "easeOut","easeOutBack",linear","linearToEaseOut",
];


var show = MultiShower(configs, (config) =>
    FlutterLayout(child: child,
      config: AnimConfig(
          duration: 2000, offset: 45, mode: RockMode.lean, curveTween: config),
    ), width: 60, color: Colors.transparent,infos: configsInfo,);
複製程式碼

3.3:動畫完成的監聽

定義一個FinishCallback回撥作為配置引數,在animation.addStatusListener裡回撥

class AnimConfig {//動畫配置
  int duration;//時長
  double offset;//偏移大小
  RockMode mode;//搖晃模式
  CurveTween curveTween;//運動曲線
  FinishCallback onFinish;
  AnimConfig({this.duration, this.offset, this.mode,this.curveTween,this.onFinish});
}

typedef FinishCallback = void Function();

---->[_FlutterLayoutState]----
animation = sequence.animate(curveTween==null?controller:curveTween.animate(controller))
  ..addStatusListener((s) {
    if (s == AnimationStatus.completed) {
      if(widget.config.onFinish!=null)
        widget.config.onFinish();
    }
  });
複製程式碼

好了,到這裡,本文完結散花。看到這的,贊點起來。


結語

本文到此接近尾聲了,如果想快速嚐鮮Flutter,《Flutter七日》會是你的必備佳品;如果想細細探究它,那就跟隨我的腳步,完成一次Flutter之旅。
另外本人有一個Flutter微信交流群,歡迎小夥伴加入,共同探討Flutter的問題,本人微訊號:zdl1994328,期待與你的交流與切磋。

本文所有原始碼見github/flutter_journey

相關文章