动画曲线天天用,你能自己整一个吗?看完这篇你就会了!

简介: 一旦用到动画,曲线 Curve 类不可避免会用到,但是用了那么多,你真的了解怎么用吗?本篇为你详细解读 Curve 类的实现,看完你就会设计自己的动画曲线了!

前言

最近在写动画相关的篇章,经常会用到 Curve 这个动画曲线类,那这个类到底怎么实现的?如果想自己来一个自定义的动画曲线该怎么弄?本篇我们就来一探究竟。

曲线

Curve 类定义

查看源码, Curve 类定义如下:

abstract class Curve extends ParametricCurve<double> {
  const Curve();

  @override
  double transform(double t) {
    if (t == 0.0 || t == 1.0) {
      return t;
    }
    return super.transform(t);
  }
  
  Curve get flipped => FlippedCurve(this);
}

看上去好像没定义什么, 实际这里只是做了两个处理,一个是明确的数据类型为 double,另一个是对 transform 做了重载,也只是对参数 t 做了特殊处理,保证参数 t 的范围在0-1之间,且起点值0.0和终点值1.0不被转换函数转换。主要定义在上一层的ParametricCurve。文档是建议子类重载transformInternal方法,那我们就继续往上看ParametricCurve这个类的实现,代码如下:

abstract class ParametricCurve<T> {
  const ParametricCurve();

  T transform(double t) {
    assert(t != null);
    assert(t >= 0.0 && t <= 1.0, 'parametric value $t is outside of [0, 1] range.');
    return transformInternal(t);
  }

  @protected
  T transformInternal(double t) {
    throw UnimplementedError();
  }

  @override
  String toString() => objectRuntimeType(this, 'ParametricCurve');
}

可以看到,实际上 transform 方法除了做参数合法性验证以外,其实就是调用了transformInternal方法,因此子类必须要实现该方法,否则会抛出UnimplementedError异常。

实例解析

上面的源码可以看到,关键在于参数 t。这个参数 t 代表什么呢?注释里说的是:

Returns the value of the curve at point t. — 返回 t 点的曲线对应的值。

因此 t 可以认为是曲线的横坐标,而为了保证曲线的一致性,做了归一化处理,也就是t的取值都是在0-1之间。这么说可能有点抽象,我们来看2个例子来对比就明白了,先看最简单 Curves.linear 的实现。

class _Linear extends Curve {
  const _Linear._();

  @override
  double transformInternal(double t) => t;
}

超级简单吧,直接返回 t,其实对应我们的数学的函数就是:

y = f(t) = t

对应的曲线就是一条斜线。也就是说在设定的动画时间内,会完成从0-1的线性转变,也就是变化是均匀的。
线性这个很好理解,我们再来看一个减速曲线decelerate的实现。

class _DecelerateCurve extends Curve {
  const _DecelerateCurve._();

  @override
  double transformInternal(double t) {
    t = 1.0 - t;
    return 1.0 - t * t;
  }
}

我们先看一下_DecelerateCurve 的计算表达式是什么。
减速公式1

回忆一下我们高中物理学的匀减速运动,加速度为负(即减速)的距离计算公式:
减速公式2

上面的减速曲线其实就可以看做是初始速度是2,加速度也是2的减速运动。为什么要是2这个值呢,这是因为 t 的取值范围是0-1,这样计算完的结果的取值范围还是0-1。你肯定会问,为什么要保证曲线的计算结果要是0-1
我们来假设计算结果不为0-1会发生什么情况,比如我们要在屏幕上移动一个组件为60像素。假设动画曲线初始值不为0。那就意味着一开始的移动距离是跳变的。同样的,如果结束值不为1.0,意味着在最后一个点的距离值不是60.0,那么就意味着结束时需要从最后一个点跳到最终的60像素的位置(动画需要保证最终的移动距离是60像素)这样意味着动画会出现跳变的效果,绘制曲线的话会是下的样子(绿色是正常的,红线是异常的)。这样的动画体验是很糟糕的!因此,这是一个关键点,如果你的自定义曲线的 transformInternal 方法的返回值范围不是0-1,就意味着动画会出现跳变,导致动画缺帧的感觉。

image.png

有了这个基础,我们就可以解释动画曲线的基本机制了,实际上就是在给定的动画时间(Duration)范围内,完成组件的初始状态到结束状态的转变,这个转变是沿着设定的 Curve 类完成的,而其横坐标是0-1.0,曲线的初始值和结束值分别是0和1.0,而至于中间值是可以低于0或超过1的。我们可以想像是我们沿着设定的曲线运动,最终无论如何都会达到设定的目的地,而至于怎么走,拐多少道弯,速度怎么变化都是曲线控制的。但是,如果你的曲线初始值不为0或结束值不为1,就像是跳悬崖的那种感觉!

正弦动画曲线

我们来一个正弦曲线的动画验证一下上面的说法。

class SineCurve extends Curve {
  final int count;
  const SineCurve({this.count = 1}) : assert(count > 0);

  @override
  double transformInternal(double t) {
    return sin(2 * count* pi * t);
  }
}

count 参数用于控制周期,即达到目的地之前可以多来几个来回。这里我们发现,初始值是0,但是一个周期(2π)结束值也是0,这样在动画结束前会出现跳变的结果。来看一下示例代码,这个示例是让圆形向下移动60像素。

AnimatedContainer(
  decoration: BoxDecoration(
    color: Colors.blue,
    borderRadius: BorderRadius.circular(30.0),
  ),
  transform: Matrix4.identity()..translate(0.0, up ? 60.0 : 0.0, 0.0),
  duration: Duration(milliseconds: 3000),
  curve: SineCurve(count: 1),
  child: ClipOval(
    child: Container(
      width: 60.0,
      height: 60.0,
      color: Colors.blue,
    ),
  ),
)

运行效果如下,注意看最后一帧从0的位置直接跳到了60的位置。

跳动动画

这个怎么调呢,我们来看一下正弦曲线的样子。

正弦曲线

如果我们要满足0-1范围的要求,那么要往后再移动90度才能够达到。但是,这样还有个问题,这样破坏了周期性,比如设置 count=2的时候结果又不对了。我们来看一下规律,实际上只有第一个周期需要多移动90度(途中箭头指向的点),后面的都是按360度(即2π)为周期了。也就是角度其实是按2.5π,4.5π,6.5π……规律来的,对应的角度公式其实就是:
调整后公式

所以调整后的正弦曲线代码为:

class SineCurve extends Curve {
  final int count;
  const SineCurve({this.count = 1}) : assert(count > 0);

  @override
  double transformInternal(double t) {
    // 需要补偿pi/2个角度,使得起始值是0.终止值是1,避免出现最后突然回到0
    return sin(2 * (count + 0.25) * pi * t);
  }
}

再看调整后的效果,是不是丝滑般地过渡了?
调整后动画

总结

本篇介绍了 Flutter 动画曲线类的原理和控制动画的机制,实际上 Curve 类就是在指定的时间内,沿曲线完成从起点到终点的过渡。但是为了保证动画平滑过渡,应该保证自定义曲线的transformInternal方法返回值的起始值和结束值分别是0和1。


欢迎关注个人公众号:岛上码农,个人微信:island-coder。

相关文章
|
5月前
AutoJs曲线滑动---贝塞尔曲线
AutoJs曲线滑动---贝塞尔曲线
159 0
|
2月前
|
前端开发
背景滑动,动感加倍:CSS动画对角线效果全解析!
背景滑动,动感加倍:CSS动画对角线效果全解析!
|
5月前
|
机器学习/深度学习 数据采集 算法
【传知代码】无监督动画中关节动画的运动表示-论文复现
本文探讨了数据驱动的无监督动画技术,尤其是针对关节动画的运动表示。研究提出三个主要贡献:1) 使用区域表示增强一阶运动稳定性;2) 明确建模背景运动以稳定点识别;3) 在无监督空间中解耦形状和姿态防止形状转移。通过这些改进,无监督运动转移的精度提升,特别是对关节对象的动画。作者还创建了一个新的TED演讲者数据集,证明了方法的有效性,其性能优于现有技术。文章总结了监督和无监督图像动画方法,并介绍了关节动画的基本原理,包括骨架、关节表示和姿势表示。核心逻辑涉及一阶运动模型、PCA-based运动估计和背景运动估计,以及图像生成过程。
【传知代码】无监督动画中关节动画的运动表示-论文复现
|
3月前
|
流计算 Python
Python实现动态银河系:模拟旋转的银河动画
Python实现动态银河系:模拟旋转的银河动画
40 0
|
4月前
|
编解码 算法 程序员
老程序员分享:OpenGL学习进程(10)第七课:四边形绘制与动画基础
老程序员分享:OpenGL学习进程(10)第七课:四边形绘制与动画基础
|
5月前
|
前端开发
【零基础入门前端系列】—旋转、缩放、倾斜、过渡(二十三)
【零基础入门前端系列】—旋转、缩放、倾斜、过渡(二十三)
|
11月前
|
图形学
不断涌出的爱意,使用SwiftUI搭建一个爱心粒子动画~
不断涌出的爱意,使用SwiftUI搭建一个爱心粒子动画~
138 0
|
前端开发 JavaScript
神奇的滤镜!巧妙实现内凹的平滑圆角
神奇的滤镜!巧妙实现内凹的平滑圆角
256 0
神奇的滤镜!巧妙实现内凹的平滑圆角
|
传感器 JSON 数据可视化
【视觉高级篇】22 # 如何用仿射变换来移动和旋转3D物体?
【视觉高级篇】22 # 如何用仿射变换来移动和旋转3D物体?
185 0
【视觉高级篇】22 # 如何用仿射变换来移动和旋转3D物体?
|
存储 算法
看动画学算法之:双向队列dequeue
看动画学算法之:双向队列dequeue
看动画学算法之:双向队列dequeue