Flutter 动画剖析(一) 彻底掌握动画的使用

动画定义

早期的动画片是利用大量图片进行快速切换从而达到一种看似连续的动画效果,这就是最早期的帧动画,利用人的视觉延迟产生的一种连续的效果,其实现在的动画也是这个原理,在同一时间屏幕进行多次有规律的渲染次数,渲染次数越多,动画就越流畅,也就是我们平常说的屏幕刷新率。

本篇文章主旨让大家在使用Flutter动画的过程中游刃有余,不对具体源码进行解析。

  • 动画关键属性:动画时长、动画轨迹。

动画其实就是对象在规定的时间内进行的特定规则运动的表现。所以我们需要关心的核心属性就是动画时长和动画轨迹。

理解了动画实现的原理,所谓万变不离其宗,Flutter动画亦是如此,下面首先看下Flutter中使用动画的几个关键类。

动画核心类:

AnimationController 动画控制器

用来设置动画时长、动画开始结束数值、控制开始结束动画等操作。

构造方法以及常用方法:

_controller = AnimationController(
  vsync: this,//设置Ticker 动画帧的回调函数
  duration: const Duration(milliseconds: 2000),// 正向动画时长 //2s
  reverseDuration: const Duration(milliseconds: 2000),// 反向动画时长 //2s
  lowerBound: 0,// 开始动画数值  double类型
  upperBound: 1.0,// 结束动画数值 double类型
  animationBehavior: AnimationBehavior.normal ,// 动画器行为 是否重复动画 两个枚举值
  debugLabel: "缩放动画",// 调试标签 动画过多时方便调式,toString时显示
// _controller.toString;
//输出: AnimationController#9d121(▶ 0.000; for 缩放动画)➩Tween<double>(0.0 → 1.0)➩0.0
); 
// 常用方法:
// 监听动画运动
_controller.addListener(() { });
// 监听动画开始、停止等状态
_controller.addStatusListener((status) {
      // dismissed 动画在起始点停止
      // forward 动画正在正向执行
      // reverse 动画正在反向执行
      // completed 动画在终点停止
      if (status == AnimationStatus.completed) {
        _controller.reverse(); //反向执行 100-0
      } else if (status == AnimationStatus.dismissed) {
        _controller.forward(); //正向执行 0-100
      }
});
// 启动动画
// _controller.forward();//正向开始动画
// _controller.reverse();//反向开始动画
_controller.repeat(); // 无限循环开始动画

vsync参数需要类混入:
SingleTickerProviderStateMixinTickerProviderStateMixin,如果页面内只有一个动画控制器使用第一个,多个控制器使用第二个。

AnimatedBuilder 实现动画组件核心类

一般情况下,我们需要将需要有动画效果的组件上包裹一层AnimatedBuilder从而监听动画控制器更新数据,内部也是通过有状态部件监听不断刷新页面实现。
构造方法:

const AnimatedBuilder({
  Key? key,
  required Listenable animation,//动画控制器
  required this.builder,// 返回动画
  this.child,// 传递给build的child子组件
})

以上两个组件就可以实现简单的动画效果了,下面我们使用AnimationControllerAnimatedBuilder实现一个简单的缩放动画。使FlutterLogo组件大小不断变化。

// 开启动画
_controller.repeat(); // 无限循环开始动画

Center(
  child:   AnimatedBuilder(
      child: FlutterLogo(),
      animation: _controller,
      builder: (context, child) {
        return Container(
          width: 100 * _controller.value,
          height: 100 * _controller.value,
          child: child,
        );
      }),
)

可以看到组件通过控制器0-1不断变化,logo大小也发生了变化,也就简单实现了缩放的效果。
注:AnimatedBuilder是实现的局部组件刷新,并不会触发本身的build方法。

Animation<T> 声明动画

以上我们通过控制器实现了简单的缩放动画效果,但是我们发现开始和结束的数据只能是double类型的数字,中间的变化状态是需要我们来进行计算的,如果遇到较为复杂的过渡变化,计算也会同样变得复杂,那么为了解决这个问题,Animation<T>应运而生,该类主要用来声明控制动画运动的数据类,数据为泛型类型,可自定义。

有了这个类,我们就可以实现自定义数据算法的过渡效果,例如颜色的渐变。
Animation一般和Tween配合使用。

Tween<T> 实现声明动画

为对象在开始和结束中间运动时变化的过程类,也称为补间动画,泛型和Animation一致,通过Animation给定泛型,生成Tween类设置动画开始和动画结束数据并调用animate方法设置动画控制器。
一般情况,动画开始和结束我们用0 ~ 1 表示,实际上,我们也可以使用其他数据设置开始和结束数据。例如颜色过渡ColorTween、大小过渡SizeTween、矩形过渡RectTween变化等,而无需关心运动过程中的数值是如何计算的,因为这些官方已经帮我们计算好了。 源码中可以看到,这些扩展的Tween类都实现了lerp方法。

例如颜色渐变实现:

/// 返回0 ~ 1 颜色渐变过程中的色值。
@override
Color? lerp(double t) => Color.lerp(begin, end, t);

static Color? lerp(Color? a, Color? b, double t) {
    ///...略
    // 颜色渐变算法 具体算法可以翻代码自行查看 都是数学知识
      return Color.fromARGB(
        _clampInt(_lerpInt(a.alpha, b.alpha, t).toInt(), 0, 255),
        _clampInt(_lerpInt(a.red, b.red, t).toInt(), 0, 255),
        _clampInt(_lerpInt(a.green, b.green, t).toInt(), 0, 255),
        _clampInt(_lerpInt(a.blue, b.blue, t).toInt(), 0, 255),
      );
}

举例:

// 开始动画
_controller.repeat(reverse: true); // 无限循环开始动画 结束倒放为true
/// 颜色渐变动画
Animation<Color?> animation;// 声明动画,数据为Color颜色
animation = ColorTween(begin: Colors.red, end: Colors.yellow).animate(_controller);// 实现动画,设置动画由红向黄渐变
// Size大小变化
Animation<Size?> animation2;
animation2 = SizeTween(begin: Size(100,50), end:Size(50,100)).animate(cure);

当然我们也可以自定义补间动画的过程,实现lerp方法,这里就考验你数学知识的掌握了,就不展开了,掌握原理即可。

Curve & CurvedAnimation 动画运动曲线

上面的动画效果虽然实现了复杂过程的变化,但是还缺少我们动画的核心属性,就是运动轨迹,因为上面没有设置,默认的运动轨迹是线性变化的,所以给我们的效果都是非常平稳的。如果实现更为丰富的动画效果,那么Curve应运而生,Curve是一个数值转换器,可以理解为方程式,默认y=x;它可以让我们运动过程不再是匀速变化,而是让运动过程可以拥有加速、减速、越界等变化,并且Curves里内置了非常丰富的运动轨迹可以直接使用。 在之前的文章也有用到过。

CurvedAnimationCurve类的具体使用,将非线性曲线应用到动画中,使用也非常的简单。只需要将动画控制器赋值给CurvedAnimation,上方Tweenanimate方法里设置 CurvedAnimation即可。
代码:

//构造
CurvedAnimation({
  required this.parent,// 动画控制器
  required this.curve,// 正向动画曲线
  this.reverseCurve,// 反向动画曲线
}); 

//自定义运动曲线
CurvedAnimation cure = CurvedAnimation(parent: _controller, curve: Curves.easeIn);

// 使用 将_controller替换为cure
Animation<Color> animation;
animation = Tween<Color>(begin: Colors.black, end: Colors.white).animate(cure);

可以看到下方有非常丰富的曲线效果。

源码注释里有mp4效果演示,想方便了解效果可以看这篇文章。 Flutter 动画曲线Curves 效果一览。

自定义Curves

随便点击去一个Cubic类,实现方法很简单,

从源码中可以看到 Curve里有可以实现两个方法,官方建议实现transformInternal方法,因为transform方法内部直接返回了transformInternal这个方法。

那么实现就很简单了,定义一个类,继承Curves,实现transformInternal方法即可。transformInternal就可以将我们给定的数值转换为我们想要的数值。

class MyCurve extends Curve {
  @override
  double transformInternal(double x) {
    // 自定义变化曲线
    // 默认 y= x; 线性运动
    // y = x的立方。这里可以理解为定义方程 x可以理解为0-1的变化过程
    // y即是返回0-1变化的的自定义算法,无需关心具体的动画运动轨迹是如何计算的。
    return x * x * x;
  }
}

注:Curve只负责0 ~ 1(也就是动画开始 ~ 动画结束)的变化曲线,无论任何数据驱动的动画我们都可以用0 ~ 1来表示运动曲线。具体的过渡算法我们无需关心,那是补间动画需要做的事情,内置的补间动画Flutter已经帮我们算了,使用也非常的方便。

其他 内置常用动画组件的使用

其实在Flutter中还内置了我们常用的动画效果组件,例如平移、渐变、缩放等组件,实现过程原理基本相当,区别是我们不需要自己计算了,直接设置动画器即可。

部分内置动画使用:

1、平移动画 SlideTransition

根据组件自身大小进行平移,接收Offset数据,分别代表自身组件大小的倍数。

// 构造
const SlideTransition({
  super.key,
  required Animation<Offset> position,
  this.transformHitTests = true,
  this.textDirection,//阅读习惯方向
  this.child,
}) 

2、渐变动画 FadeTransition

渐变动画一般指组件透明度渐变效果,接收double类型,0 ~ 1为完全透明 ~ 完全不透明。

const FadeTransition({
  super.key,
  required Animation<double> opacity,
  this.alwaysIncludeSemantics = false,
  super.child,
}) 

3、缩放动画 ScaleTransition

缩放动画接收double数据,0 ~ 1 为最小到最大,可以指定缩放中心。

const ScaleTransition({
  super.key,
  required Animation<double> scale,
  this.alignment = Alignment.center,//缩放中心
  this.filterQuality,//过滤器质量
  this.child,
}) 

4、旋转动画 RotationTransition

旋转动画一般指平面二维的旋转,接收double参数,0 ~1为旋转一圈,同样可指定旋转中心。

const RotationTransition({
  super.key,
  required Animation<double> turns,
  this.alignment = Alignment.center,
  this.filterQuality,
  this.child,
}) 

5、3D旋转动画

3D动画系统没有现成的,需要我们使用矩阵变换自行计算,其实也很简单,通过Matrix4类设置矩阵变换即可,下方为绕y轴进行旋转,范围是0~2pi。

AnimatedBuilder(
    child: child,
    animation: animation,
    builder: (context, child) {
      return Transform(
          alignment: Alignment.center, //相对于坐标系原点的对齐方式
          transform: Matrix4.identity()
            ..rotateX(0)//x轴不变
            ..rotateY(animation.value),//绕y轴旋转,0-2pi
          child: Container(width: 100, height: 100, child: child));
    });

5、组合动画

将上面所有动画效果组合起来也很简单,将以上动画通过子组件进行嵌套即可,最终的子组件为我们动画所需的组件。
核心代码:

animation = Tween(begin: 0.0, end: 1.0).animate(cure);
animation2 = Tween<Offset>(begin: Offset(0.0, 0.0), end: Offset(1.0, 0.0)).animate(cure);
animation3 = Tween(begin: 0.0, end: 1.0).animate(cure);
animation4 = Tween(begin: 0.0, end: pi * 2).animate(cure);

// // 平移
SlideTransitionLogo(
  animation: animation2,
  // 渐变
  child: FadeTransition(
    opacity: animation3,
    // 二维旋转
    child:  RotationTransitionLogo(
     // 缩放
      child: ScaleTransition(
        // 3D旋转
        child: AnimatedBuilder(
            child:  FlutterLogo(),
            animation: animation4,
            builder: (context, child) {
              return Transform(
                  alignment: Alignment.center, //相对于坐标系原点的对齐方式
                  transform: Matrix4.identity()
                    ..rotateX(0)
                    ..rotateY(animation4.value),
                  child: Container(width: 100, height: 100, child: child));
            }),
        scale: animation,
      ),

      animation: animation3,
    ),将
  ),
),

其实系统内置的还有些其他现成的动画效果,有兴趣的小伙伴可以自己研究下,原理基本相同。

自定义动画效果

自定义动画一般和自绘制结合使用,根据绘制的组件和动画的运动曲线来达到我们想要的效果。下一篇有时间剖析下动画与绘制结合使用的方法与细节。

总结

动画归根结底是让数据不断变化来驱使UI产生变化,重点的就是我们如何处理这个变化过程中的数据,以上是对于Flutter动画使用方面的一些总结,希望对你有所帮助~ 如有疑问,欢迎指正。

作者:老李code
链接:https://juejin.cn/post/7154662336182583309

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容