動(dòng)畫其實(shí)是我們通過(guò)某些方式(比如對(duì)象掺涛,Animation對(duì)象)給Flutter引擎提供不同的值纽匙,而Flutter可以根據(jù)我們提供的值厌丑,給對(duì)應(yīng)的Widget添加順滑的動(dòng)畫效果。
1. Animation
Animation是一個(gè)抽象類匹耕,它本身和UI渲染沒(méi)有任何關(guān)系聚请,而它主要的功能是保存動(dòng)畫的插值和狀態(tài);其中一個(gè)比較常用的Animation類是Animation<double>稳其。Animation對(duì)象是一個(gè)在一段時(shí)間內(nèi)依次生成一個(gè)區(qū)間(Tween)之間值的類良漱。Animation對(duì)象在整個(gè)動(dòng)畫執(zhí)行過(guò)程中輸出的值可以是線性的、曲線的欢际、一個(gè)步進(jìn)函數(shù)或者任何其他曲線函數(shù)等等母市,這由Curve來(lái)決定。 根據(jù)Animation對(duì)象的控制方式损趋,動(dòng)畫可以正向運(yùn)行(從起始狀態(tài)開(kāi)始患久,到終止?fàn)顟B(tài)結(jié)束),也可以反向運(yùn)行浑槽,甚至可以在中間切換方向蒋失。Animation還可以生成除double之外的其他類型值,如:Animation<Color> 或Animation<Size>桐玻。在動(dòng)畫的每一幀中篙挽,我們可以通過(guò)Animation對(duì)象的value屬性獲取動(dòng)畫的當(dāng)前狀態(tài)值
動(dòng)畫通知
addListener方法
它可以用于給Animation添加幀監(jiān)聽(tīng)器,在每一幀都會(huì)被調(diào)用镊靴。幀監(jiān)聽(tīng)器中最常見(jiàn)的行為是改變狀態(tài)后調(diào)用setState()來(lái)觸發(fā)UI重建
addStatusListener
當(dāng)動(dòng)畫的狀態(tài)發(fā)生變化時(shí)铣卡,會(huì)通知所有通過(guò) addStatusListener 添加的監(jiān)聽(tīng)器。
通常情況下偏竟,動(dòng)畫會(huì)從 dismissed 狀態(tài)開(kāi)始煮落,表示它處于變化區(qū)間的開(kāi)始點(diǎn)。
舉例來(lái)說(shuō)踊谋,從 0.0 到 1.0 的動(dòng)畫在 dismissed 狀態(tài)時(shí)的值應(yīng)該是 0.0蝉仇。
動(dòng)畫進(jìn)行的下一狀態(tài)可能是 forward(比如從 0.0 到 1.0)或者 reverse(比如從 1.0 到 0.0)。
最終,如果動(dòng)畫到達(dá)其區(qū)間的結(jié)束點(diǎn)(比如 1.0)轿衔,則動(dòng)畫會(huì)變成 completed 狀態(tài)沉迹。
動(dòng)畫狀態(tài) AnimationStatus
枚舉值 | 含義 |
---|---|
dismissed |
動(dòng)畫在起始點(diǎn)停止 |
forward |
動(dòng)畫正在正向執(zhí)行 |
reverse |
動(dòng)畫正在反向執(zhí)行 |
completed |
動(dòng)畫在終點(diǎn)停止 |
#
abstract class Animation<T> extends Listenable implements ValueListenable<T> {
const Animation();
// 添加動(dòng)畫監(jiān)聽(tīng)器
@override
void addListener(VoidCallback listener);
// 移除動(dòng)畫監(jiān)聽(tīng)器
@override
void removeListener(VoidCallback listener);
// 添加動(dòng)畫狀態(tài)監(jiān)聽(tīng)器
void addStatusListener(AnimationStatusListener listener);
// 移除動(dòng)畫狀態(tài)監(jiān)聽(tīng)器
void removeStatusListener(AnimationStatusListener listener);
// 獲取動(dòng)畫當(dāng)前狀態(tài)
AnimationStatus get status;
// 獲取動(dòng)畫當(dāng)前的值
@override
T get value;
2. AnimationController
Animation是一個(gè)抽象類,并不能用來(lái)直接創(chuàng)建對(duì)象實(shí)現(xiàn)動(dòng)畫的使用害驹。
AnimationController是Animation的一個(gè)子類胚股,實(shí)現(xiàn)動(dòng)畫通常我們需要?jiǎng)?chuàng)建AnimationController對(duì)象。
AnimationController會(huì)生成一系列的值裙秋,默認(rèn)情況下值是0.0到1.0區(qū)間的值;
除了上面的監(jiān)聽(tīng)缨伊,獲取動(dòng)畫的狀態(tài)摘刑、值之外,AnimationController還提供了對(duì)動(dòng)畫的控制:
- forward:向前執(zhí)行動(dòng)畫
- reverse:反向播放動(dòng)畫
- stop:停止動(dòng)畫
AnimationController的源碼:
AnimationController({
double? value, // 初始化值
this.duration, // 動(dòng)畫執(zhí)行的時(shí)間
this.reverseDuration,// 反向動(dòng)畫執(zhí)行的時(shí)間
this.debugLabel, // 標(biāo)識(shí) 調(diào)試使用
this.lowerBound = 0.0, // 最小值
this.upperBound = 1.0, // 最大值
this.animationBehavior = AnimationBehavior.normal,
required TickerProvider vsync, // 刷新率ticker的回調(diào)(看下面詳細(xì)解析)
})
final AnimationController controller = AnimationController(
duration: const Duration(milliseconds: 2000),
vsync: this,
);
AnimationController派生自Animation<double>刻坊,因此可以在需要Animation對(duì)象的任何地方使用枷恕。 但是,AnimationController具有控制動(dòng)畫的其他方法谭胚,例如forward()方法可以啟動(dòng)正向動(dòng)畫徐块,reverse()可以啟動(dòng)反向動(dòng)畫。
在動(dòng)畫開(kāi)始執(zhí)行后開(kāi)始生成動(dòng)畫幀灾而,屏幕每刷新一次就是一個(gè)動(dòng)畫幀胡控,在動(dòng)畫的每一幀,會(huì)隨著根據(jù)動(dòng)畫的曲線來(lái)生成當(dāng)前的動(dòng)畫值(Animation.value)旁趟,然后根據(jù)當(dāng)前的動(dòng)畫值去構(gòu)建UI昼激,當(dāng)所有動(dòng)畫幀依次觸發(fā)時(shí),動(dòng)畫值會(huì)依次改變锡搜,所以構(gòu)建的UI也會(huì)依次變化橙困,所以最終我們可以看到一個(gè)完成的動(dòng)畫。 另外在動(dòng)畫的每一幀耕餐,Animation對(duì)象會(huì)調(diào)用其幀監(jiān)聽(tīng)器凡傅,等動(dòng)畫狀態(tài)發(fā)生改變時(shí)(如動(dòng)畫結(jié)束)會(huì)調(diào)用狀態(tài)改變監(jiān)聽(tīng)器。
vsync
AnimationController有一個(gè)必傳的參數(shù)vsync肠缔,這是什么夏跷?
- Flutter有自己的渲染閉環(huán),F(xiàn)lutter每次渲染一幀畫面之前都需要等待一個(gè)vsync信號(hào)明未。
- 這里也是為了監(jiān)聽(tīng)vsync信號(hào)拓春,當(dāng)Flutter開(kāi)發(fā)的應(yīng)用程序不再接受同步信號(hào)時(shí)(比如鎖屏或退到后臺(tái)),那么繼續(xù)執(zhí)行動(dòng)畫會(huì)消耗性能亚隅。這個(gè)時(shí)候我們?cè)O(shè)置了Ticker硼莽,就不會(huì)再出發(fā)動(dòng)畫了。
- 開(kāi)發(fā)中比較常見(jiàn)的是將SingleTickerProviderStateMixin混入到State的定義中。
duration
duration表示動(dòng)畫執(zhí)行的時(shí)長(zhǎng)懂鸵,通過(guò)它我們可以控制動(dòng)畫的速度偏螺。
注意: 在某些情況下,動(dòng)畫值可能會(huì)超出
AnimationController
的[0.0匆光,1.0]的范圍套像,這取決于具體的曲線。例如终息,fling()
函數(shù)可以根據(jù)我們手指滑動(dòng)(甩出)的速度(velocity)夺巩、力量(force)等來(lái)模擬一個(gè)手指甩出動(dòng)畫,因此它的動(dòng)畫值可以在[0.0周崭,1.0]范圍之外 柳譬。也就是說(shuō),根據(jù)選擇的曲線续镇,CurvedAnimation
的輸出可以具有比輸入更大的范圍美澳。例如,Curves.elasticIn等彈性曲線會(huì)生成大于或小于默認(rèn)范圍的值摸航。
3. CurvedAnimation
動(dòng)畫過(guò)程可以是勻速的制跟、勻加速的或者先加速后減速等。Flutter中通過(guò)Curve(曲線)來(lái)描述動(dòng)畫過(guò)程酱虎,我們把勻速動(dòng)畫稱為線性的(Curves.linear)雨膨,而非勻速動(dòng)畫稱為非線性的。
CurvedAnimation也是Animation的一個(gè)實(shí)現(xiàn)類读串,它的目的是為了給AnimationController增加動(dòng)畫曲線
CurvedAnimation可以將AnimationController和Curve結(jié)合起來(lái)哥放,生成一個(gè)新的Animation對(duì)象
class CurvedAnimation extends Animation<double> with AnimationWithParentMixin<double> {
CurvedAnimation({
// 通常傳入一個(gè)AnimationController
@required this.parent,
// Curve類型的對(duì)象
@required this.curve,
this.reverseCurve,
});
}
final CurvedAnimation curve =
CurvedAnimation(parent: controller, curve: Curves.easeIn);
Curve類型的對(duì)象的有一些常量Curves(和Color類型有一些Colors是一樣的),可以供我們直接使用:
常見(jiàn)的幾種Curve
Curves曲線 | 動(dòng)畫過(guò)程 |
---|---|
linear | 勻速的 |
decelerate | 勻減速 |
ease | 開(kāi)始加速爹土,后面減速 |
easeIn | 開(kāi)始慢甥雕,后面快 |
easeOut | 開(kāi)始快,后面慢 |
easeInOut | 開(kāi)始慢胀茵,然后加速社露,最后再減速 |
Curve對(duì)應(yīng)值的效果,可以直接查看官網(wǎng)(有對(duì)應(yīng)的gif效果琼娘,一目了然)
https://api.flutter.dev/flutter/animation/Curves-class.html
自定義Curse
官方也給出了自定義Curse的一個(gè)示例:
正弦曲線 ShakeCurve
class ShakeCurve extends Curve {
@override
double transform(double t) {
return math.sin(t * math.PI * 2);
}
}
4. Ticker
當(dāng)創(chuàng)建一個(gè)AnimationController時(shí)峭弟,需要傳遞一個(gè)vsync參數(shù),它接收一個(gè)TickerProvider類型的對(duì)象脱拼,它的主要職責(zé)是創(chuàng)建Ticker瞒瘸,定義如下:
abstract class TickerProvider {
//通過(guò)一個(gè)回調(diào)創(chuàng)建一個(gè)Ticker
Ticker createTicker(TickerCallback onTick);
}
Flutter 應(yīng)用在啟動(dòng)時(shí)都會(huì)綁定一個(gè)SchedulerBinding,通過(guò)SchedulerBinding可以給每一次屏幕刷新添加回調(diào)熄浓,而Ticker就是通過(guò)SchedulerBinding來(lái)添加屏幕刷新回調(diào)情臭,這樣一來(lái),每次屏幕刷新都會(huì)調(diào)用TickerCallback。使用Ticker(而不是Timer)來(lái)驅(qū)動(dòng)動(dòng)畫會(huì)防止屏幕外動(dòng)畫(動(dòng)畫的UI不在當(dāng)前屏幕時(shí)俯在,如鎖屏?xí)r)消耗不必要的資源竟秫,因?yàn)镕lutter中屏幕刷新時(shí)會(huì)通知到綁定的SchedulerBinding,而Ticker是受SchedulerBinding驅(qū)動(dòng)的跷乐,由于鎖屏后屏幕會(huì)停止刷新肥败,所以Ticker就不會(huì)再觸發(fā)
通常我們會(huì)將SingleTickerProviderStateMixin
添加到State
的定義中,然后將State對(duì)象作為vsync
的值愕提。
class _MSBasicAnimationPageState extends State<MSBasicAnimationPage>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller =
AnimationController(vsync: this, duration: Duration(seconds: 2));
...
}
...
}
5. Tween
默認(rèn)情況下馒稍,AnimationController對(duì)象值的范圍是[0.0,1.0]浅侨。如果我們需要構(gòu)建UI的動(dòng)畫值在不同的范圍或不同的數(shù)據(jù)類型纽谒,則可以使用Tween來(lái)添加映射以生成不同的范圍或數(shù)據(jù)類型的值
class Tween<T extends dynamic> extends Animatable<T> {
Tween({ this.begin, this.end });
}
Tween構(gòu)造函數(shù)需要begin和end兩個(gè)參數(shù)。Tween的唯一職責(zé)就是定義從輸入范圍到輸出范圍的映射仗颈。輸入范圍通常為[0.0,1.0]椎例,但這不是必須的挨决,我們可以自定義需要的范圍。
例如订歪,像下面示例脖祈,Tween生成[-200.0,0.0]的值:
final Tween doubleTween = Tween<double>(begin: -200.0, end: 0.0);
Tween繼承自Animatable<T>刷晋,而不是繼承自Animation<T>盖高,Animatable中主要定義動(dòng)畫值的映射規(guī)則。
Tween也有一些子類眼虱,比如ColorTween喻奥、BorderTween,可以針對(duì)動(dòng)畫或者邊框來(lái)設(shè)置動(dòng)畫的值捏悬。
下面我們看一個(gè)ColorTween將動(dòng)畫輸入范圍映射為兩種顏色值之間過(guò)渡輸出的例子:
final Tween colorTween =
ColorTween(begin: Colors.transparent, end: Colors.black54);
Tween
對(duì)象不存儲(chǔ)任何狀態(tài)撞蚕,相反,它提供了evaluate(Animation<double> animation)
方法过牙,它可以獲取動(dòng)畫當(dāng)前映射值甥厦。 Animation
對(duì)象的當(dāng)前值可以通過(guò)value()
方法取到。evaluate
函數(shù)還執(zhí)行一些其它處理寇钉,例如分別確保在動(dòng)畫值為0.0和1.0時(shí)返回開(kāi)始和結(jié)束狀態(tài)刀疙。
注意:同時(shí)設(shè)置了Tween 和AnimationController的lowerBound、upperBound扫倡,可能會(huì)崩潰谦秧。
Tween.animate
要使用 Tween 對(duì)象,需要調(diào)用其animate()方法,然后傳入一個(gè)Animation對(duì)象油够。
注意 animate()返回的是一個(gè)Animation蚁袭,而不是一個(gè)Animatable。
例如石咬,以下代碼在 500 毫秒內(nèi)生成從 0 到 255 的整數(shù)值揩悄。
final AnimationController controller = AnimationController(
duration: const Duration(milliseconds: 500),
vsync: this,
);
Animation<int> alpha = IntTween(begin: 0, end: 255).animate(controller);
以下示例構(gòu)建了一個(gè)控制器、一條曲線和一個(gè) Tween:
final AnimationController controller = AnimationController(
duration: const Duration(milliseconds: 500),
vsync: this,
);
final Animation curve = CurvedAnimation(parent: controller, curve: Curves.easeOut);
Animation<int> alpha = IntTween(begin: 0, end: 255).animate(curve);
6. lerp
動(dòng)畫的原理其實(shí)就是每一幀繪制不同的內(nèi)容鬼悠,一般都是指定起始和結(jié)束狀態(tài)删性,然后在一段時(shí)間內(nèi)從起始狀態(tài)逐漸變?yōu)榻Y(jié)束狀態(tài),而具體某一幀的狀態(tài)值會(huì)根據(jù)動(dòng)畫的進(jìn)度來(lái)算出焕窝,因此蹬挺,F(xiàn)lutter 中給有可能會(huì)做動(dòng)畫的一些狀態(tài)屬性都定義了靜態(tài)的 lerp 方法(線性插值),比如:
//a 為起始顏色它掂,b為終止顏色巴帮,t為當(dāng)前動(dòng)畫的進(jìn)度[0,1]
Color.lerp(a, b, t);
lerp 的計(jì)算一般遵循: 返回值 = a + (b - a) * t,其它擁有 lerp 方法的類:
// Size.lerp(a, b, t)
// Rect.lerp(a, b, t)
// Offset.lerp(a, b, t)
// Decoration.lerp(a, b, t)
// Tween.lerp(t) //起始狀態(tài)和終止?fàn)顟B(tài)在構(gòu)建 Tween 的時(shí)候已經(jīng)指定了
...
需要注意虐秋,lerp 是線性插值榕茧,意思是返回值和動(dòng)畫進(jìn)度t是成一次函數(shù)(y = kx + b)關(guān)系,因?yàn)橐淮魏瘮?shù)的圖像是一條直線客给,所以叫線性插值用押。如果我們想讓動(dòng)畫按照一個(gè)曲線來(lái)執(zhí)行彩匕,我們可以對(duì) t 進(jìn)行映射垦页,比如要實(shí)現(xiàn)勻加速效果塔嬉,則 t' = at2+bt+c珍德,然后指定加速度 a 和 b 即可(大多數(shù)情況下需保證 t' 的取值范圍在[0,1]介褥,當(dāng)然也有一些情況可能會(huì)超出該取值范圍滚秩,比如彈簧(bounce)效果)澎语,而不同 Curve 可以按照不同曲線執(zhí)行動(dòng)畫的的原理本質(zhì)上就是對(duì) t 按照不同映射公式進(jìn)行映射蜗顽。
7. 動(dòng)畫基本使用 - 示例
class MSBasicAniamtionRouter extends StatefulWidget {
const MSBasicAniamtionRouter({Key? key}) : super(key: key);
@override
State<MSBasicAniamtionRouter> createState() => _MSBasicAniamtionRouterState();
}
class _MSBasicAniamtionRouterState extends State<MSBasicAniamtionRouter>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _curveAnimation;
late Animation<double> _tweenAnimation;
@override
void initState() {
super.initState();
_controller =
AnimationController(vsync: this, duration: Duration(seconds: 2));
// 動(dòng)畫速率曲線
_curveAnimation =
CurvedAnimation(parent: _controller, curve: Curves.linear);
// 動(dòng)畫執(zhí)行的value范圍
_tweenAnimation = Tween(begin: 50.0, end: 150.0).animate(_curveAnimation);
_controller.addListener(() {
setState(() {});
});
_controller.addStatusListener((status) {
if (status == AnimationStatus.dismissed) {
// 向前執(zhí)行動(dòng)畫
_controller.forward();
} else if (status == AnimationStatus.completed) {
// 反向執(zhí)行動(dòng)畫
_controller.reverse();
}
});
// 向前執(zhí)行動(dòng)畫
_controller.forward();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Icon(
Icons.favorite,
color: Colors.red,
size: _tweenAnimation.value,
),
),
);
}
}