Flutter动画之自定义动画组件-FlutterLayout

前言:

本文将自定义一个FlutterWidget的动画组件,Flutter有颤动的意思
在此之前会讲一下AnimatedWidget与AnimatedBuilder是什么,如何使用
所以本文是一篇挺重要的文章,不仅是内容,还有思想和灵魂。
今天也悟到了一段话分享给大家:

当你遇到一群共事之人,开始难免会觉得某某人高冷而帅气,某某人美丽而大方,某某人能力超级强  
作为普通人的你也许很想和他们结交但又很难进入他们的世界,于是你在角落静静凝望,细心观察
随着时间的流逝,也许偶尔的交谈,你会发现他们并非看上去的那么难以接近,于是开始和他们交流  
随着关系的加深,也许某个傍晚,你们会走在回去的路上,诉说着人生,从此渐渐无话不说。
然后会发现,这世间的隔阂也许只是自己为自己施加的屏障,这个屏障会为你抵御伤害,
但它同时也可能让你失去一个对的人,一个未来的止步于陌生的知己。

学习亦如此,一个框架就是那个高冷而帅气公子,一个类就是那个美丽而大方姑娘,结合上面再看看。  
有时候错过了,也就错过了,你不可能认识所有的人,但你可以用真诚选择一位知己。
认识的人当然越多越好,但知己,宁缺毋滥。 ----XXX,你现在还好吗? 
                                                    (张风捷特烈 2019.7.19 字)

首先,留图镇楼


1.AnimatedWidget与AnimatedBuilder

1.1:前情回顾

现在回到昨天的最后一个组件,这样写不够优雅,什么东西都在一块
Flutter中提供了AnimatedWidget类可以让动画的组件更加简洁

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方法,也能自动更新信息

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

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:组件是什么

模块化的思想大家应该都听过,为了让已有代码更好复用,将项目拆成不同模块
组件也是这样,对于一个页面,便是组件的组合,可以拆装,拼凑和批量生成
在代码中我们可以很轻易的将多个控件批量动效。比如一段话的每个字都有效果:

_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方法里生成刚才的带有颤动效果的组件

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来用,只不过有个抖动的效果

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的功能

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,主要用于批量产生不同配置的同类组件

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来测试

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.增加运动曲线

可以用曲线补间来让动画的执行不那么古板


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