注册

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,
);
}),
)

效果:

Oct-15-2022 14-27-43.gif

可以看到组件通过控制器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);


效果:

Oct-15-2022 14-53-06.gif
Oct-15-2022 15-29-28.gif
当然我们也可以自定义补间动画的过程,实现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);


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

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


自定义Curves


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


从源码中可以看到 Curve里有可以实现两个方法,官方建议实现transformInternal方法,因为transform方法内部直接返回了transformInternal这个方法。
image.png
那么实现就很简单了,定义一个类,继承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,
),将
),
),


效果:

Oct-15-2022 16-34-21.gif


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


自定义动画效果


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


总结


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


作者:老李code
链接:https://juejin.cn/post/7154662336182583309
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册