例子
所謂動(dòng)畫其實(shí)就是一系列連續(xù)變化的圖片在極短的時(shí)間逐幀顯示,在人眼看來(lái)就是動(dòng)畫了。這里我們舉一個(gè)簡(jiǎn)單的例子先說(shuō)明一下在Flutter中怎么運(yùn)行一個(gè)動(dòng)畫:
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();
}
}
這個(gè)動(dòng)畫是在手機(jī)屏幕上由小到大漸變的顯示一個(gè)Flutter標(biāo)志永乌。從上述代碼中我們可以看到在Flutter中實(shí)現(xiàn)一個(gè)動(dòng)畫要做這么幾件事吨述。
- 首先施加動(dòng)畫的Widget是個(gè)StatefulWidget。其State要混入(mixin) SingleTickerProviderStateMixin歧寺。
- 在initState()里要加入和動(dòng)畫相關(guān)的初始化燥狰,這里我們實(shí)例化了兩個(gè)類AnimationController和Animation棘脐。實(shí)例化AnimationController的時(shí)候我們傳入了兩個(gè)參數(shù),一個(gè)是動(dòng)畫的時(shí)長(zhǎng)龙致,另一個(gè)是State自己蛀缝,這里其實(shí)是利用到了混入的SingleTickerProviderStateMixin。實(shí)例化另一個(gè)Animation的時(shí)候目代,我們首先實(shí)例化的是一個(gè)Tween屈梁。這個(gè)類其實(shí)代表了從最小值到最大值的一個(gè)線性變化。所以實(shí)例化的時(shí)候要傳入開始和結(jié)束值榛了。然后調(diào)用animate()并傳入之前的controller在讶。這個(gè)調(diào)用會(huì)返回我們需要的Animation實(shí)例。顯然我們需要知道動(dòng)畫的屬性變化的時(shí)候的消息霜大,所以這里會(huì)通過(guò)..addListener()給Animation實(shí)例注冊(cè)回調(diào)构哺。這個(gè)回調(diào)只做一件事,那就是調(diào)用setState()來(lái)更新UI战坤。最后就是調(diào)用controller.forward()來(lái)啟動(dòng)動(dòng)畫曙强。
- 注意在build()函數(shù)里我們構(gòu)建widget的時(shí)候用到了animation.value。所以這里的鏈條就是動(dòng)畫在收到回調(diào)后會(huì)調(diào)用setState()途茫,而從我們上篇文章知道setState之后在渲染流水線的構(gòu)建階段會(huì)走到build()來(lái)重建Widget旗扑。重建的時(shí)候就用到了發(fā)生變化以后的animation.value。這個(gè)一幀一幀的循環(huán)慈省,我們的動(dòng)畫就動(dòng)起來(lái)了臀防。
- 最后在dispose()的時(shí)候要記得調(diào)用controller.dispose()釋放資源。
接下來(lái)我們就深入Flutter源碼來(lái)看一下動(dòng)畫是如何運(yùn)行的边败。
分析
首先我們來(lái)看一下混入到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();
}
}
這個(gè)混入其實(shí)就做了一件事,實(shí)現(xiàn)createTicker()來(lái)實(shí)例化一個(gè)Ticker類笑窜。在另一個(gè)函數(shù)didChangeDependencies()里致燥,有這樣一行代碼_ticker.muted = !TickerMode.of(context);。這行代碼的意思是在這個(gè)帶有動(dòng)畫的State的在element tree中的依賴發(fā)生變化的時(shí)候是否mute自己的_ticker排截。一個(gè)場(chǎng)景就是當(dāng)前頁(yè)的動(dòng)畫還在播放的時(shí)候嫌蚤,用戶導(dǎo)航到另外一個(gè)頁(yè)面,當(dāng)前頁(yè)的動(dòng)畫就沒(méi)有必要再播放了断傲,反之在頁(yè)面切換回來(lái)的時(shí)候動(dòng)畫有可能還要繼續(xù)播放脱吱,控制的地方就在這里,注意TickerMode.of(context)這種方式认罩,我們?cè)贔lutter框架中很多地方都會(huì)見到箱蝠,基本上就是從element tree的祖先里找到對(duì)應(yīng)那個(gè)InheritedWidget的方式。
Ticker顧名思義,就是給動(dòng)畫提供vsync信號(hào)的吧宦搬。我們來(lái)看下源碼一探究竟牙瓢。
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主要在做的有點(diǎn)像控制一個(gè)計(jì)時(shí)器,有start()和stop()和mute间校。還記錄當(dāng)前自己的狀態(tài)isTicking矾克。我們需要關(guān)注的的是scheduleTick()這個(gè)函數(shù):
@protected
void scheduleTick({ bool rescheduling = false }) {
_animationId = SchedulerBinding.instance.scheduleFrameCallback(_tick, rescheduling: rescheduling);
}
你看,這里就跑到了我們之前文章說(shuō)的SchedulerBinding里面去了憔足。這里調(diào)度的時(shí)候會(huì)傳入Ticker的回調(diào)函數(shù)_tick胁附。
int scheduleFrameCallback(FrameCallback callback, { bool rescheduling = false }) {
scheduleFrame();
_nextFrameCallbackId += 1;
_transientCallbacks[_nextFrameCallbackId] = _FrameCallbackEntry(callback, rescheduling: rescheduling);
return _nextFrameCallbackId;
}
在調(diào)度一幀的時(shí)候Ticker的回調(diào)函數(shù)_tick被加入了transientCallbacks。從之前對(duì)渲染流水線的分析四瘫,我們知道transientCallbacks會(huì)在vsync信號(hào)到來(lái)以后window的onBeginFrame回調(diào)里被執(zhí)行一次。也就是說(shuō)此時(shí)就進(jìn)入到渲染流水線的動(dòng)畫Animate階段了欲逃。
接著我們就看下Ticker的回調(diào)函數(shù)_tick做了什么:
void _tick(Duration timeStamp) {
_animationId = null;
_startTime ??= timeStamp;
_onTick(timeStamp - _startTime);
if (shouldScheduleTick)
scheduleTick(rescheduling: true);
}
這里的_onTick是在實(shí)例化Ticker時(shí)候傳入的找蜜。_onTick被調(diào)用之后,Ticker如果發(fā)現(xiàn)自己的任務(wù)還沒(méi)有完成稳析,還要接著跳動(dòng)洗做,那就再來(lái)調(diào)度新一幀。所以你看動(dòng)畫的動(dòng)力其實(shí)還是來(lái)自vsync信號(hào)的彰居。
那么這個(gè)_onTick又是啥樣的呢诚纸?這個(gè)函數(shù)是在實(shí)例化Ticker的時(shí)候傳入的。而從上述分析我們又知道陈惰,Ticker的實(shí)例化是在調(diào)用TickerProvider.createTicker()的時(shí)候完成的畦徘。誰(shuí)來(lái)調(diào)用這個(gè)函數(shù)呢?是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);
}
可見在其構(gòu)造函數(shù)里就調(diào)用createTicker()了井辆,傳入的參數(shù)是_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();
}
這個(gè)回調(diào)里做這幾件事杯缺,根據(jù)vsync到來(lái)以后的時(shí)間戳來(lái)計(jì)算更新一下新的值,這里計(jì)算用的是個(gè)_simulation睡榆。為啥叫這名萍肆?因?yàn)檫@是用來(lái)模擬一個(gè)物體在外力作用下在不同的時(shí)間點(diǎn)的運(yùn)動(dòng)狀態(tài)的變化,這也算是動(dòng)畫的本質(zhì)吧胀屿。
算出來(lái)新的值以后就調(diào)用notifyListeners()來(lái)通知各位觀察者塘揣。還記的在開始的例子里我們實(shí)例化animation以后會(huì)通過(guò)..addListener()添加的回調(diào)嗎?在這里這個(gè)回調(diào)就會(huì)被調(diào)用宿崭,也就是setState()會(huì)被調(diào)用了勿负。接下來(lái)就是渲染流水線的構(gòu)建(build)階段了。
看到這里你可能會(huì)有疑問(wèn),事情都讓AnimationController做了奴愉,那那個(gè)例子里的Tween是用來(lái)干啥的琅摩?
從AnimationController的構(gòu)造函數(shù)里我們可以看出來(lái),它只管[0.0, 1.0]之間的模擬锭硼,也就是說(shuō)不管動(dòng)畫怎么動(dòng)房资,它任何時(shí)候只輸出0.0到1.0之間的值,但是我們的動(dòng)畫有旋轉(zhuǎn)角度檀头,顏色漸變轰异,圖形變化以及更復(fù)雜的組合,顯然我們得想辦法把0.0到1.0之間的值轉(zhuǎn)換為我們需要的角度暑始,位置搭独,顏色,透明度等等廊镜,這個(gè)轉(zhuǎn)化就是由各種Animation來(lái)完成的牙肝,像例子里說(shuō)的Tween,它的任務(wù)在動(dòng)畫期間把值從0漸變到300嗤朴。怎么做呢配椭?在實(shí)例化Tween以后我們會(huì)調(diào)用animate(),傳入AnimationController實(shí)例雹姊。
Animation<T> animate(Animation<double> parent) {
return _AnimatedEvaluation<T>(parent, this);
}
你看股缸,入?yún)⑹莻€(gè)Animation<double>,這里也就是AnimationController吱雏。出參則是個(gè)Animation<T>敦姻。這樣就完成了從[0.0, 1.0]到任意類型的變化。
具體怎么變呢歧杏?這個(gè)變化其實(shí)是在用到這個(gè)值得時(shí)候發(fā)生的替劈,上面的例子里在State.build()函數(shù)里構(gòu)造widget的時(shí)候會(huì)調(diào)用到animation.value這個(gè)getter。這其實(shí)調(diào)用的是_AnimatedEvaluation.value得滤。
@override
T get value => _evaluatable.evaluate(parent);
_evaluatable就是Tween了陨献,parent就是AnimationController了。所以呢懂更,這個(gè)轉(zhuǎn)換是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);
}
看到范圍限制了嗎龄捡?真正的轉(zhuǎn)換又是在lerp()里完成的。
@protected
T lerp(double t) {
return begin + (end - begin) * t;
}
很簡(jiǎn)單的線性插值慷暂。
所里你要理解Flutter中的Tween動(dòng)畫是干什么的只要把握住它在自己的transform()函數(shù)中做了什么事情就知道了聘殖,從上可知Tween其實(shí)就是在做線性插值的動(dòng)畫而已晨雳。Tween是線性插值的,那如果我想搞非線性插值的動(dòng)畫呢奸腺?那就用CurvedAnimation餐禁。Flutter里有一大票各種各樣的線性插值動(dòng)畫和非線性插值的動(dòng)畫,你甚至可以自己定義自己的非線性動(dòng)畫突照,只要重寫變換函數(shù)就行了:
import 'dart:math';
class ShakeCurve extends Curve {
@override
double transform(double t) => sin(t * pi * 2);
}
好了帮非,關(guān)于Flutter框架里的動(dòng)畫就先分析到這里。