Flutter 动画

3,039 阅读9分钟

所有代码都已经上传至github,如果觉得有用,麻烦给个star, 谢谢。

介绍

  • 常用类介绍

    • Tween

      补间动画,可以定义开始点和结束点,动画执行时会根据开始点与结束点给的范围进行取值,每一帧取一个值,可以用于绘制新视图,一帧帧的绘制行程了动画效果。

      Tween<T>(begin: begin, end: end)
      

      begin与end 类型由Tween传入的类型决定。

      变化.gif

      变形.gif

      渐变.gif

      色变.gif

      缩放.gif

      移动.gif

      组合动画.gif

    • Animation

      这是一个用于管理动画状态与生成动画每一帧需要的值,在这个类中,可以设置动画每一帧的监听与动画状态的监听。

    • CurvedAnimation 曲线动画,它继承与Animation,可用于实现带有曲线模型的动画,点击Curves可以查看更多

    • AnimationController

      动画控制器,用于管理Animation,使用AnimationController 需要传入vsync,通常是通过使用AnimationController的类混入SingleTickerProviderStateMixin,把当前this传入就可以,当一个类有多个AnimationController时也混入TickerProviderStateMixin。还需要传入一个duration,用于控制动画执行时间。

    • AnimatedWidget 动画部件,用于把有动画的视图与主视图隔离开来,当动画绘制时不会引起主视图跟随重新绘制,这是一个抽象类,通常是通过AnimatedWidget的派生类来实现动画隔离。

    • AnimatedBuilder
      继承于AnimatedWidget,更便于使用。

      AnimateBuilder.gif

  • 动画添加监听器

    动画添加监听器就是对Animation操作。

    • addListener

      可以监听每一帧

      animation.addListener(() {
          //打印每帧的值
          print(animation.value);
      });
      
    • addStatusListener

      用于监听Animation的状态

      animation.addStatusListener((AnimationStatus status) {
          if (status == AnimationStatus.completed) {
            //动画完成
          } else if (status == AnimationStatus.dismissed) {
            //动画取消
          } else if (status == AnimationStatus.reverse) {
            //动画反向
          } else if (status == AnimationStatus.forward) {
            //动画向前(开始)
          }
      });
      
  • 动画状态介绍

    • dismissed 动画取消
    • forward 动画向前,通常调用次方法开始动画。
    • reverse 动画反向
    • completed 动画完成
  • 动画控制 动画控制就是AnimationController来执行动画。

    • forward 动画向前,可以用于启动动画。
    • reverse 动画反向,可以用于启动动画。
    • reset 重置动画
    • stop 停止动画
    • repeat 重复动画,可以用于启动动画。
  • 动画实践方式

    • Tween 单独使用 Tween 单独使用就是对Animation设置一个addListener,捕获到每帧的回调在来重新绘制视图
    import 'package:flutter/material.dart';
    
    ///位移
    class DisplacementPage extends StatefulWidget {
      @override
      DisplacementPageState createState() => DisplacementPageState();
    }
    
    class DisplacementPageState extends State<DisplacementPage> with SingleTickerProviderStateMixin {
      AnimationController controller;
    
      Animation<Offset> animation;
    
      @override
      void initState() {
        super.initState();
    
        controller = AnimationController(vsync: this, duration: Duration(milliseconds: 1000));
    
        Offset begin = Offset(0.0, 0.0);
        Offset end = Offset(0.0, 200);
        animation = Tween<Offset>(begin: begin, end: end).animate(controller)
          ..addListener(() {
            setState(() {});
          })
          ..addStatusListener((AnimationStatus status) {
            if (status == AnimationStatus.completed) {
              controller.reverse();
            } else if (status == AnimationStatus.dismissed) {
              controller.forward();
            }
          });
    
        controller.forward();
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text('位移'),
            centerTitle: true,
          ),
          body: Align(
            child: Container(
              width: 100,
              height: 100,
              margin: EdgeInsets.only(top: animation.value.dy),
              color: Colors.green,
            ),
          ),
        );
      }
    
      @override
      void dispose() {
        controller.dispose();
        super.dispose();
      }
    }
    
    
    • AnimatedBuilder 使用

      AnimateBuilder继承与AnimatedWidget,需要传入一个AnimationControllerbuilder, builder是一个TransitionBuilder虚拟类型,它真实类型是一个Widget

       typedef TransitionBuilder = Widget Function(BuildContext context, Widget child);
      

      为配合AnimatedBuilder展示动画效果,系统还为我们提供了Transform,Transform其实就是继承了 SingleChildRenderObjectWidget,由Transform构建下面三种方式使用:

      • Transform.rotate
        旋转变换,需要传入一个Widget与一个angle,angle是角度值,0°值是0,360°值是2π π通常使用 math.pi,使用这个时需要引入 import 'dart:math' as math;
      • Transform.translate
        移动变换,需要传入一个Widget与offset
      • Transform.scale 缩放变换,需要传入一个Widget与scale,scale的值是相对自身大小的倍数,例如 scale:1.2 放大到自身的1.2倍大
        import 'package:flutter/material.dart';
        import 'package:flutter_animation/util.dart';
        import 'dart:math' as math;
        
        ///AnimatedBuilder
        class AnimatedBuilderPage extends StatefulWidget {
        @override
        AnimatedBuilderPageState createState() => AnimatedBuilderPageState();
        }
        
        class AnimatedBuilderPageState extends State<AnimatedBuilderPage> with SingleTickerProviderStateMixin {
        AnimationController controller;
        
        //旋转
        Animation<double> rotate;
        
        //移动
        Animation<Offset> translate;
        
        //缩放
        Animation<double> scale;
        
        @override
        void initState() {
          super.initState();
        
          controller = AnimationController(vsync: this, duration: Duration(milliseconds: 2000));
        
          CurvedAnimation curvedAnimation = CurvedAnimation(parent: controller, curve: Curves.linear);
        
          rotate = Tween<double>(begin: 0, end: math.pi * 2).animate(curvedAnimation);
          translate = Tween<Offset>(begin: Offset(0, 0), end: Offset(300, 50)).animate(curvedAnimation);
          scale = Tween<double>(begin: 1, end: 2).animate(curvedAnimation);
        
          controller.repeat();
        }
        
        @override
        Widget build(BuildContext context) {
          return Scaffold(
            appBar: AppBar(
              title: Text('AnimatedBuilder'),
              centerTitle: true,
            ),
            body: Align(
              child: Column(
                children: <Widget>[
                  titleBarWidget(
                      title: 'AnimatedBuilder就是把动画视图跟主页面分离开了,动画执行时调用的setState只会重新绘制AnimatedBuilder中的视图,不会对主页面重新绘制'
                  '\nAnimatedBuilder的builder需要返回一个TransitionBuilder,'
                  '\n构建TransitionBuilder时可以有三种动画,'
                  '\nTransform.rotate  旋转'
                  '\nTransform.translate  移动'
                  '\nTransform.scale 缩放'),
                  sizedBox,
                  AnimatedBuilder(
                    animation: controller,
                    builder: (context, child) {
                      return Transform.rotate(
                        angle: rotate.value,
                        child: Container(
                          width: 100,
                          height: 100,
                          color: Colors.green,
                          child: Icon(Icons.android, color: Colors.white),
                        ),
                      );
                    },
                  ),
                  sizedBox,
                  AnimatedBuilder(
                    animation: controller,
                    builder: (context, child) {
                      return Transform.translate(
                        offset: translate.value,
                        child: Container(
                          width: 100,
                          height: 100,
                          color: Colors.green,
                          child: Icon(Icons.android, color: Colors.white),
                        ),
                      );
                    },
                  ),
                  sizedBox,
                  AnimatedBuilder(
                    animation: controller,
                    builder: (context, child) {
                      return Transform.scale(
                        scale: scale.value,
                        child: Container(
                          width: 100,
                          height: 100,
                          color: Colors.green,
                          child: Icon(Icons.android, color: Colors.white),
                        ),
                      );
                    },
                  ),
                ],
              ),
            ),
          );
        }
        
          @override
          void dispose() {
            controller.dispose();
            super.dispose();
          }
        }
                    
        

      注意: Transform的变化是在应用绘制阶段,Transform中的Widget在布局阶段就确 定了大小与位置,在Transform动画变化时都不会对其他的视图造成影响。

    • Transition使用 Transition是一种继承AnimatedWidget实现动画的一种总称,它有下面方式可以使用:

      • SlideTransition 位移 过渡
      • ScaleTransition 缩放 过渡
      • RotationTransition 旋转 过渡
      • SizeTransition 大小 过渡
      • FadeTransition 渐变 过渡
      • PositionedTransition 定位 过渡
      • RelativePositionedTransition
        相对定位 过渡
      • DecoratedBoxTransition 装饰 过渡
      • AlignTransition 对齐 过渡
      • DefaultTextStyleTransition 默认字体样式 过渡

      Transition.gif

      import 'package:flutter/material.dart';
      import 'package:flutter_animation/util.dart';
      
      ///Transition
      class TransitionPage extends StatefulWidget {
        @override
        TransitionPageState createState() => new TransitionPageState();
      }
      
      class TransitionPageState extends State<TransitionPage> with SingleTickerProviderStateMixin {
        AnimationController controller;
      
        //旋转
        Animation<double> rotate;
      
        //移动
        Animation<Offset> translate;
      
        //缩放
        Animation<double> scale;
      
        @override
        void initState() {
          super.initState();
      
          controller = AnimationController(vsync: this, duration: Duration(milliseconds: 2000));
      
          //加入曲线模型
          CurvedAnimation curvedAnimation = CurvedAnimation(parent: controller, curve: Curves.linear);
      
          rotate = Tween<double>(begin: 0, end: 1).animate(curvedAnimation);
          translate = Tween<Offset>(begin: Offset(0, 0), end: Offset(1.5, 1.5)).animate(curvedAnimation);
          scale = Tween<double>(begin: 1, end: 2).animate(curvedAnimation);
      
          controller.repeat();
        }
      
        @override
        Widget build(BuildContext context) {
          return Scaffold(
            appBar: AppBar(
              title: Text('Transition'),
              centerTitle: true,
            ),
            body: Align(
              child: Column(
                children: <Widget>[
                  titleBarWidget(title: 'Transition 继承AnimatedWidget'),
                  sizedBox,
                  RotationTransition(
                    turns: rotate,
                    child: Container(
                      width: 100,
                      height: 100,
                      color: Colors.green,
                      child: Icon(Icons.android, color: Colors.white),
                      ),
                  ),
                  sizedBox,
                  SlideTransition(
                    position: translate,
                    child: Container(
                      width: 100,
                      height: 100,
                      color: Colors.green,
                        child: Icon(Icons.android, color: Colors.white),
                    ),
                  ),
                  sizedBox,
                  ScaleTransition(
                    scale: scale,
                    child: Container(
                      width: 100,
                      height: 100,
                      color: Colors.green,
                      child: Icon(Icons.android, color: Colors.white),
                    ),
                  ),
                ],
              ),
            ),
          );
        }
      
        @override
        void dispose() {
          controller.dispose();
          super.dispose();
        }
      }
      

      注意:这些的构造函数里面需要传入的值都是相对自身大小、角度、位置、坐标倍数来设置的,具体可以查看对应类里的build方法

动画类型

  • 补间动画

    • 介绍

      Tween补间动画就如上面介绍的那样,在创建Tween时,还可以直接使用下面类型来创建Tween,下面类型都是继承于Tween,只是在Tween基础上多了一步lerp处理

    • 类型

      • ReverseTween 反向
      • ColorTween 颜色
      • SizeTween 大小
      • RectTween 矩形
      • IntTween 整型
      • StepTween 渐变
      • ConstantTween 常数
      • CurveTween 曲线
  • 基于物理动画

    • 介绍

      基于物理动画就是模拟日常生活中的一些想象,例如球落地、橡皮筋拉起松开效果等等。

    • 类型

      类型太多了,具体可以查看Curves

    • 使用

       AnimationController controller;
       //缩放
       Animation<double> scale;
      
      @override
      void initState() {
        super.initState();
      
          controller = AnimationController(vsync: this, duration: Duration(milliseconds: 2000));
      
          //加入曲线模型
          CurvedAnimation curvedAnimation = CurvedAnimation(parent: controller, curve: Curves.linear);
      
          scale = Tween<double>(begin: 1, end: 2).animate(curvedAnimation);
          controller.repeat();
      }
      

常用动画

  • 动画列表

    AnimatedList是对ListView中的item执行动画效果,可以根据需要传入Tween 类型的动画,使用代码如下:

    列表动画.gif

  ///列表动画
  class AnimatedListPage extends StatefulWidget {
    @override
    AnimatedListPageState createState() => AnimatedListPageState();
  }

  class AnimatedListPageState extends State<AnimatedListPage> {
    final GlobalKey<AnimatedListState> globalKey = GlobalKey<AnimatedListState>();
    int index = 5;

    int type = 1;

    ///item视图
    Widget itemWidget({context, animation, index}) {
      Widget child = Container(
        width: double.infinity,
        height: 100,
        margin: EdgeInsets.only(left: 16, top: 16, right: 16),
        color: Colors.primaries[index],
      );
      Widget size = SizeTransition(sizeFactor: animation, child: child);
      Widget scale = ScaleTransition(scale: animation, child: child);
      Widget fade = FadeTransition(opacity: animation, child: child);

      switch (type) {
        case 1:
          return size;
        case 2:
          return scale;
        case 3:
          return fade;
        default:
          return size;
      }
    }
  
    ///构建 视图
    Widget buildItemWidget(context, index, animation) {
      return itemWidget(context: context, index: index, animation: animation);
    }
  
    ///删除视图
    Widget removeItemBuilder(BuildContext context, Animation<double> animation) {
      return itemWidget(context: context, index: index, animation: animation);
    }


    @override
    Widget build(BuildContext context) {
      return Scaffold(
        appBar: AppBar(
          title: Text('AnimatedList'),
          actions: <Widget>[
            IconButton(
              icon: Icon(Icons.add_circle),
              onPressed: () {
                index += 1;
                globalKey.currentState.insertItem(index - 1);
              },
            ),
            IconButton(
              icon: Icon(Icons.remove_circle),
              onPressed: () {
                if (index > 1) {
                  index -= 1;
                  globalKey.currentState.removeItem(index, removeItemBuilder);
                }
              },
            ),
            PopupMenuButton(
              icon: Icon(Icons.more_vert),
              itemBuilder: (context) {
                return [
                  PopupMenuItem(
                    child: Text('展开收起'),
                    value: 1,
                  ),
                  PopupMenuItem(
                    child: Text('放大缩小'),
                    value: 2,
                  ),
                  PopupMenuItem(
                    child: Text('渐变'),
                    value: 3,
                  ),  
                ];
              },
              onSelected: (value) {
                type = value;
                setState(() {});
              },
            ),
          ],
        ),  
        body: AnimatedList(
          key: globalKey,
          itemBuilder: buildItemWidget,
          initialItemCount: index,
        ),
      );
    }
  }
  • 共享元素

    Hero是共享元素必须的控件,系统会根据相同Tag的Hero执行动画效果,代码如下:

    共享元素.gif

import 'package:flutter/material.dart';
import 'package:flutter_animation/util.dart';

///共享元素控件
class PhotoWidget extends StatelessWidget {
  final double width;
  final double height;
  final String path;

  PhotoWidget({this.width = double.infinity, this.height, this.path});

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: width,
      child: Hero(
        tag: path,
        child: Image.asset(
          path,
          fit: BoxFit.contain,
          package: 'flutter_animation',
        ),
      ),
    );
  }
}

///共享元素
class HeroAnimationPage extends StatefulWidget {
  @override
  HeroAnimationPageState createState() => new HeroAnimationPageState();
}

class HeroAnimationPageState extends State<HeroAnimationPage> {
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text('共享元素'),
        centerTitle: true,
      ),
      body: Align(
        child: SingleChildScrollView(
          child: Column(
            children: <Widget>[
              titleBarWidget(title: 'Flutter 动画分为补间动画(Tween)与物理动画'),
              sizedBox,
              PhotoWidget(
                width: 300,
                height: 100,
                path: 'assets/images/photo.png',
              ),
              sizedBox,
              buttonWidget(
                title: '查看详情',
                onPressed: () {
                  toPage(context, PhotoDetail());
                },
              ),
            ],
          ),
        ),
      ),
    );
  }

  void toPage(BuildContext context, Widget widget) {
    Navigator.push(context, MaterialPageRoute(builder: (context) => widget));
  }
}

///详情页面
class PhotoDetail extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text('共享元素-详情'),
        centerTitle: true,
      ),
      body: Column(
        children: <Widget>[
          PhotoWidget(
            width: double.infinity,
            height: 300,
            path: 'assets/images/photo.png',
          ),
          Padding(
            padding: EdgeInsets.all(16),
            child: Text('\t\t\t\t蒙奇·D·路飞,日本漫画《航海王》的主角,外号“草帽”路飞,草帽海贼团、草帽大船团船长,极恶的世代之一。'
                '橡胶果实能力者,悬赏金15亿贝里。梦想是找到传说中的One Piece,成为海贼王。'
                '\n\n\t\t\t\t路飞性格积极乐观,爱憎分明,而且十分重视伙伴,不甘屈居于他人之下,对任何危险的事物都超感兴趣。'
                '和其他传统的海贼所不同的是,他并不会为了追求财富而杀戮,而是享受着身为海贼的冒险和自由。'),
          ),
        ],
      ),
    );
  }
}

  • 交错动画

    交错动画就是多个动画组合使用,使用时通过Interval来控制动画执行时间区间。代码如下:

    交错动画.gif

import 'package:flutter/material.dart';

///交错动画
class StaggeredAnimationPage extends StatefulWidget {
  @override
  StaggeredAnimationPageState createState() => StaggeredAnimationPageState();
}

class StaggeredAnimationPageState extends State<StaggeredAnimationPage>
    with SingleTickerProviderStateMixin {
  AnimationController controller;
  Animation<Offset> translate;
  Animation<double> sizeWidth;
  Animation<double> sizeHeight;
  Animation<double> radius;
  Animation<Color> color;
  Animation<double> opacity;

  @override
  void initState() {
    super.initState();
    controller =
        AnimationController(vsync: this, duration: Duration(seconds: 2));

    translate = Tween<Offset>(begin: Offset(0, 0), end: Offset(0, 200)).animate(
      CurvedAnimation(
        parent: controller,
        curve: Interval(0, 0.3, curve: Curves.linear),
      ),
    );

    sizeWidth = Tween<double>(begin: 50, end: 200).animate(CurvedAnimation(
      parent: controller,
      curve: Interval(0.3, 0.4, curve: Curves.linear),
    ));
    sizeHeight = Tween<double>(begin: 50, end: 200).animate(CurvedAnimation(
      parent: controller,
      curve: Interval(0.4, 0.6, curve: Curves.linear),
    ));
    radius = Tween<double>(begin: 0, end: 100).animate(CurvedAnimation(
      parent: controller,
      curve: Interval(0.6, 0.7, curve: Curves.linear),
    ));
    color = ColorTween(begin: Colors.green, end: Colors.red)
        .animate(CurvedAnimation(
      parent: controller,
      curve: Interval(0.6, 0.8, curve: Curves.bounceOut),
    ));

    opacity = Tween<double>(begin: 0, end: 1).animate(CurvedAnimation(
      parent: controller,
      curve: Interval(0.8, 1.0, curve: Curves.linear),
    ));

    controller.addStatusListener((AnimationStatus status) {
      if (status == AnimationStatus.completed) {
        controller.reverse();
      } else if (status == AnimationStatus.dismissed) {
        controller.forward();
      }
    });

    controller.forward();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('交错动画'),
      ),
      body: Align(
        alignment: Alignment.topCenter,
        child: AnimatedBuilder(
          animation: controller,
          builder: (context, child) {
            return Transform.translate(
              offset: translate.value,
              child: Container(
                width: sizeWidth.value,
                height: sizeHeight.value,
                decoration: BoxDecoration(
                  border: Border.all(color: Colors.black, width: 1),
                  borderRadius: BorderRadius.all(Radius.circular(radius.value)),
                  color: color.value,
                ),
                child: Opacity(
                  opacity: opacity.value,
                  child: Icon(
                    Icons.android,
                    size: 50,
                    color: Colors.white,
                  ),
                ),
              ),
            );
          },
        ),
      ),
    );
  }

  @override
  void dispose() {
    controller?.dispose();
    super.dispose();
  }
}