Flutter框架分析分析系列文章:bash
《Flutter框架分析(一)-- 总览和Window》框架
《Flutter框架分析(三)-- Widget,Element和RenderObject》函数
《Flutter框架分析(四)-- Flutter框架的运行》布局
前四篇文章介绍了Flutter框架的全貌,相信你们对Flutter框架有了个总体的了解。这一系列文章始终是围绕着渲染流水线的的运行的各个阶段加以说明。咱们知道在Vsync信号到来之后首先运行的是动画(Animate)阶段。而这个阶段是在从engine回调window
的onBeginFrame
函数开始运行的。那么这篇文章咱们就来介绍一下Flutter框架的动画基本原理。this
所谓动画其实就是一系列连续变化的图片在极短的时间逐帧显示,在人眼看来就是动画了。这里咱们举一个简单的例子先说明一下在Flutter中怎么运行一个动画:lua
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(home: LogoAnim()));
}
class LogoAnim extends StatefulWidget {
_LogoAnimState createState() => _LogoAnimState();
}
class _LogoAnimState extends State<LogoAnim> with SingleTickerProviderStateMixin {
Animation<double> animation;
AnimationController controller;
@override
void initState() {
super.initState();
controller =
AnimationController(duration: const Duration(seconds: 2), vsync: this);
animation = Tween<double>(begin: 0, end: 300).animate(controller)
..addListener(() {
setState(() {
});
});
controller.forward(from: 0);
}
@override
Widget build(BuildContext context) {
return Center(
child: Container(
margin: EdgeInsets.symmetric(vertical: 10),
height: animation.value,
width: animation.value,
child: FlutterLogo(),
),
);
}
void dispose() {
controller.dispose();
super.dispose();
}
}
复制代码
这个动画是在手机屏幕上由小到大渐变的显示一个Flutter标志。从上述代码中咱们能够看到在Flutter中实现一个动画要作这么几件事。
Widget
是个StatefulWidget
。其State
要混入(mixin
) SingleTickerProviderStateMixin
。initState()
里要加入和动画相关的初始化,这里咱们实例化了两个类AnimationController
和Animation
。实例化AnimationController
的时候咱们传入了两个参数,一个是动画的时长,另外一个是State
本身,这里实际上是利用到了混入的SingleTickerProviderStateMixin
。实例化另外一个Animation
的时候,咱们首先实例化的是一个Tween
。这个类其实表明了从最小值到最大值的一个线性变化。因此实例化的时候要传入开始和结束值。而后调用animate()
并传入以前的controller
。这个调用会返回咱们须要的Animation
实例。显然咱们须要知道动画的属性变化的时候的消息,因此这里会经过..addListener()
给Animation
实例注册回调。这个回调只作一件事,那就是调用setState()
来更新UI。最后就是调用controller.forward()
来启动动画。build()
函数里咱们构建widget
的时候用到了animation.value
。因此这里的链条就是动画在收到回调后会调用setState()
,而从咱们上篇文章知道setState
以后在渲染流水线的构建阶段会走到build()
来重建Widget
。重建的时候就用到了发生变化之后的animation.value
。这个一帧一帧的循环,咱们的动画就动起来了。dispose()
的时候要记得调用controller.dispose()
释放资源。接下来咱们就深刻Flutter源码来看一下动画是如何运行的。
首先咱们来看一下混入到State
中的SingleTickerProviderStateMixin
。
mixin SingleTickerProviderStateMixin<T extends StatefulWidget> on State<T> implements TickerProvider {
Ticker _ticker;
@override
Ticker createTicker(TickerCallback onTick) {
_ticker = Ticker(onTick, debugLabel: 'created by $this');
return _ticker;
}
@override
void didChangeDependencies() {
if (_ticker != null)
_ticker.muted = !TickerMode.of(context);
super.didChangeDependencies();
}
}
复制代码
这个混入其实就作了一件事,实现createTicker()
来实例化一个Ticker
类。在另外一个函数didChangeDependencies()
里,有这样一行代码_ticker.muted = !TickerMode.of(context);
。这行代码的意思是在这个带有动画的State
的在element tree中的依赖发生变化的时候是否mute
本身的_ticker
。一个场景就是当前页的动画还在播放的时候,用户导航到另一个页面,当前页的动画就没有必要再播放了,反之在页面切换回来的时候动画有可能还要继续播放,控制的地方就在这里,注意TickerMode.of(context)
这种方式,咱们在Flutter框架中不少地方都会见到,基本上就是从element tree的祖先里找到对应那个InheritedWidget
的方式。
Ticker
顾名思义,就是给动画提供vsync信号的吧。咱们来看下源码一探究竟。
class Ticker {
TickerFuture _future;
bool get muted => _muted;
bool _muted = false;
set muted(bool value) {
if (value == muted)
return;
_muted = value;
if (value) {
unscheduleTick();
} else if (shouldScheduleTick) {
scheduleTick();
}
}
bool get isTicking {
if (_future == null)
return false;
if (muted)
return false;
if (SchedulerBinding.instance.framesEnabled)
return true;
if (SchedulerBinding.instance.schedulerPhase != SchedulerPhase.idle)
return true;
return false;
}
bool get isActive => _future != null;
Duration _startTime;
TickerFuture start() {
_future = TickerFuture._();
if (shouldScheduleTick) {
scheduleTick();
}
if (SchedulerBinding.instance.schedulerPhase.index > SchedulerPhase.idle.index &&
SchedulerBinding.instance.schedulerPhase.index < SchedulerPhase.postFrameCallbacks.index)
_startTime = SchedulerBinding.instance.currentFrameTimeStamp;
return _future;
}
void stop({ bool canceled = false }) {
if (!isActive)
return;
final TickerFuture localFuture = _future;
_future = null;
_startTime = null;
unscheduleTick();
if (canceled) {
localFuture._cancel(this);
} else {
localFuture._complete();
}
}
final TickerCallback _onTick;
int _animationId;
@protected
bool get scheduled => _animationId != null;
@protected
bool get shouldScheduleTick => !muted && isActive && !scheduled;
void _tick(Duration timeStamp) {
_animationId = null;
_startTime ??= timeStamp;
_onTick(timeStamp - _startTime);
if (shouldScheduleTick)
scheduleTick(rescheduling: true);
}
@protected
void scheduleTick({ bool rescheduling = false }) {
_animationId = SchedulerBinding.instance.scheduleFrameCallback(_tick, rescheduling: rescheduling);
}
@protected
void unscheduleTick() {
if (scheduled) {
SchedulerBinding.instance.cancelFrameCallbackWithId(_animationId);
_animationId = null;
}
}
}
复制代码
能够看到Ticker
主要在作的有点像控制一个计时器,有start()
和stop()
和mute
。还记录当前本身的状态isTicking
。咱们须要关注的的是scheduleTick()
这个函数:
@protected
void scheduleTick({ bool rescheduling = false }) {
_animationId = SchedulerBinding.instance.scheduleFrameCallback(_tick, rescheduling: rescheduling);
}
复制代码
你看,这里就跑到了咱们以前文章说的SchedulerBinding
里面去了。这里调度的时候会传入Ticker
的回调函数_tick
。
int scheduleFrameCallback(FrameCallback callback, { bool rescheduling = false }) {
scheduleFrame();
_nextFrameCallbackId += 1;
_transientCallbacks[_nextFrameCallbackId] = _FrameCallbackEntry(callback, rescheduling: rescheduling);
return _nextFrameCallbackId;
}
复制代码
在调度一帧的时候Ticker
的回调函数_tick
被加入了transientCallbacks
。从以前对渲染流水线的分析,咱们知道transientCallbacks
会在vsync信号到来之后window
的onBeginFrame
回调里被执行一次。也就是说此时就进入到渲染流水线的动画Animate
阶段了。
接着咱们就看下Ticker
的回调函数_tick
作了什么:
void _tick(Duration timeStamp) {
_animationId = null;
_startTime ??= timeStamp;
_onTick(timeStamp - _startTime);
if (shouldScheduleTick)
scheduleTick(rescheduling: true);
}
复制代码
这里的_onTick
是在实例化Ticker
时候传入的。_onTick
被调用以后,Ticker
若是发现本身的任务尚未完成,还要接着跳动,那就再来调度新一帧。因此你看动画的动力其实仍是来自vsync信号的。
那么这个_onTick
又是啥样的呢?这个函数是在实例化Ticker
的时候传入的。而从上述分析咱们又知道,Ticker
的实例化是在调用TickerProvider.createTicker()
的时候完成的。谁来调用这个函数呢?是AnimationController
。
AnimationController({
double value,
this.duration,
this.debugLabel,
this.lowerBound = 0.0,
this.upperBound = 1.0,
this.animationBehavior = AnimationBehavior.normal,
@required TickerProvider vsync,
}) : _direction = _AnimationDirection.forward {
_ticker = vsync.createTicker(_tick);
_internalSetValue(value ?? lowerBound);
}
复制代码
可见在其构造函数里就调用createTicker()
了,传入的参数是_ticker
。 接着看_ticker
。
void _tick(Duration elapsed) {
_lastElapsedDuration = elapsed;
final double elapsedInSeconds = elapsed.inMicroseconds.toDouble() / Duration.microsecondsPerSecond;
_value = _simulation.x(elapsedInSeconds).clamp(lowerBound, upperBound);
if (_simulation.isDone(elapsedInSeconds)) {
_status = (_direction == _AnimationDirection.forward) ?
AnimationStatus.completed :
AnimationStatus.dismissed;
stop(canceled: false);
}
notifyListeners();
_checkStatusChanged();
}
复制代码
这个回调里作这几件事,根据vsync到来之后的时间戳来计算更新一下新的值,这里计算用的是个_simulation
。为啥叫这名?由于这是用来模拟一个物体在外力做用下在不一样的时间点的运动状态的变化,这也算是动画的本质吧。
算出来新的值之后就调用notifyListeners()
来通知各位观察者。还记的在开始的例子里咱们实例化animation
之后会经过..addListener()
添加的回调吗?在这里这个回调就会被调用,也就是setState()
会被调用了。接下来就是渲染流水线的构建(build)阶段了。
看到这里你可能会有疑问,事情都让AnimationController
作了,那那个例子里的Tween
是用来干啥的?
从AnimationController
的构造函数里咱们能够看出来,它只管[0.0, 1.0]之间的模拟,也就是说无论动画怎么动,它任什么时候候只输出0.0到1.0之间的值,可是咱们的动画有旋转角度,颜色渐变,图形变化以及更复杂的组合,显然咱们得想办法把0.0到1.0之间的值转换为咱们须要的角度,位置,颜色,透明度等等,这个转化就是由各类Animation
来完成的,像例子里说的Tween
,它的任务在动画期间把值从0渐变到300。怎么作呢?在实例化Tween
之后咱们会调用animate()
,传入AnimationController
实例。
Animation<T> animate(Animation<double> parent) {
return _AnimatedEvaluation<T>(parent, this);
}
复制代码
你看,入参是个Animation<double>
,这里也就是AnimationController
。出参则是个Animation<T>
。这样就完成了从[0.0, 1.0]到任意类型的变化。
具体怎么变呢?这个变化实际上是在用到这个值得时候发生的,上面的例子里在State.build()
函数里构造widget
的时候会调用到animation.value
这个getter
。这其实调用的是_AnimatedEvaluation.value
。
@override
T get value => _evaluatable.evaluate(parent);
复制代码
_evaluatable
就是Tween
了,parent
就是AnimationController
了。因此呢,这个转换是Tween
本身完成的,也是,只有它本身知道须要什么样的输出。
T evaluate(Animation<double> animation) => transform(animation.value);
复制代码
又到了transform()
里了
@override
T transform(double t) {
if (t == 0.0)
return begin;
if (t == 1.0)
return end;
return lerp(t);
}
复制代码
看到范围限制了吗?真正的转换又是在lerp()
里完成的。
@protected
T lerp(double t) {
return begin + (end - begin) * t;
}
复制代码
很简单的线性插值。
所里你要理解Flutter中的Tween
动画是干什么的只要把握住它在本身的transform()
函数中作了什么事情就知道了,从上可知Tween
其实就是在作线性插值的动画而已。Tween
是线性插值的,那若是我想搞非线性插值的动画呢?那就用CurvedAnimation
。Flutter里有一大票各类各样的线性插值动画和非线性插值的动画,你甚至能够本身定义本身的非线性动画,只要重写变换函数就好了:
import 'dart:math';
class ShakeCurve extends Curve {
@override
double transform(double t) => sin(t * pi * 2);
}
复制代码
好了,关于Flutter框架里的动画就先分析到这里。
本篇文章是Flutter框架分析系列文章的第五篇,本系列文章主要是以Flutter的渲染流水线为线索来分析其运行的。本篇主要针对的是渲染流水线的动画阶段,从底层的角度对Flutter的动画机制作了一个简要的分析。指望你们对Flutter的动画有一个基础的认识。在此之上的各类眼花缭乱的动画相关widgets
都是在此基础上衍生出来的,所谓道生一,一辈子二,二生三,三生万物。掌握了道,就不会被万物所迷惑。