美文网首页Flutter之旅FlutterFlutter圈子
Flutter动画之自定义动画组件-FlutterLayout

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

作者: 张风捷特烈 | 来源:发表于2019-07-19 13:03 被阅读8次
    前言:

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

    当你遇到一群共事之人,开始难免会觉得某某人高冷而帅气,某某人美丽而大方,某某人能力超级强  
    作为普通人的你也许很想和他们结交但又很难进入他们的世界,于是你在角落静静凝望,细心观察
    随着时间的流逝,也许偶尔的交谈,你会发现他们并非看上去的那么难以接近,于是开始和他们交流  
    随着关系的加深,也许某个傍晚,你们会走在回去的路上,诉说着人生,从此渐渐无话不说。
    然后会发现,这世间的隔阂也许只是自己为自己施加的屏障,这个屏障会为你抵御伤害,
    但它同时也可能让你失去一个对的人,一个未来的止步于陌生的知己。
    
    学习亦如此,一个框架就是那个高冷而帅气公子,一个类就是那个美丽而大方姑娘,结合上面再看看。  
    有时候错过了,也就错过了,你不可能认识所有的人,但你可以用真诚选择一位知己。
    认识的人当然越多越好,但知己,宁缺毋滥。 ----XXX,你现在还好吗? 
                                                        (张风捷特烈 2019.7.19 字)
    

    首先,留图镇楼

    image image

    1.AnimatedWidget与AnimatedBuilder

    1.1:前情回顾

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

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

    image
    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

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

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

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

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

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

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

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

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

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

    image
    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

    相关文章

      网友评论

        本文标题:Flutter动画之自定义动画组件-FlutterLayout

        本文链接:https://www.haomeiwen.com/subject/vuvjlctx.html