如果我們想要一些東西動畫颠放,我們必須改變大小或改變連續(xù)幀中對象的位置排惨。例如,在第1幀中碰凶,我們的對象位于位置x暮芭,在第2幀中,它將位于x + 1的位置欲低,在第3幀中辕宏,它位于x + 2的位置,依此類推砾莱。
創(chuàng)建動畫時(shí)的另一個(gè)概念是“每秒幀數(shù)”或FPS瑞筐。我們想要每秒更改對象的位置或大小多少次?電影通常每秒使用24幀腊瑟。這是人類眼睛看起來光滑自然的動畫的最小數(shù)量聚假。
FPS(圖片來源)
為了在Flutter中為小部件設(shè)置動畫,我們需要以下小部件:
-
Animation<T>:動畫對象由值(類型
T
)和狀態(tài)組成闰非。該值類似于當(dāng)前幀編號膘格。它會告訴您是否在第1,2,3等幀中。根據(jù)此值河胎,您可以決定窗口小部件的下一個(gè)位置或大小闯袒。狀態(tài)指示動畫在概念上是從開始到結(jié)束還是從結(jié)束回到開始。 - AnimationController:要創(chuàng)建動畫游岳,首先要創(chuàng)建一個(gè)AnimationController政敢。在給定的持續(xù)時(shí)間內(nèi),此小部件線性生成從0.0(下限)到1.0(上限)的值胚迫。只要運(yùn)行應(yīng)用程序的設(shè)備準(zhǔn)備好顯示新幀(通常喷户,此速率大約為每秒60個(gè)值),動畫控制器就會生成一個(gè)新值访锻。一個(gè)AnimationController當(dāng)不再需要它應(yīng)該被設(shè)置褪尝。這減少了泄漏的可能性闹获。當(dāng)與StatefulWidget一起使用時(shí),通常在State.initState方法中創(chuàng)建AnimationController 河哑,然后將其放置在State.dispose中避诽。方法。請注意璃谨,AnimationController繼承了Animation類沙庐,因此屬于Animation類型。
- Tween:這個(gè)類可用于將1AnimationController1的下界和上界(默認(rèn)值為0.0到1.0)從開始到結(jié)束轉(zhuǎn)換(或映射)到值佳吞。除非另有說明拱雏,否則吐Tween為double類型。補(bǔ)間的唯一工作是定義一個(gè)從輸入范圍到輸出范圍的映射底扳。輸入范圍通常是0.0到1.0铸抑,但這不是必需的。
-
TickerProvider:這是一個(gè)生成Ticker對象的工廠衷模。Ticker對象為每個(gè)新幀觸發(fā)一個(gè)事件鹊汛。AnimationController類使用Ticker來逐步調(diào)整它控制的動畫。我們可以通過使用
SingleTickerProviderStateMixin
實(shí)現(xiàn)TickerProvider功能算芯,將Ticker功能添加到我們的有狀態(tài)類中柒昏。如果您不確定mixins是什么,請閱讀本文熙揍。當(dāng)您只需要一個(gè)Ticker對象時(shí)(例如职祷,如果類在其整個(gè)生命周期內(nèi)僅創(chuàng)建一個(gè)AnimationController),此mixin非常有用届囚。 - AnimatedBuilder:很明顯有梆,每當(dāng)我們改變小部件的大小或位置時(shí),我們都想重新構(gòu)建它意系。但是我們怎么做到的泥耀?這是AnimatedBuilder小部件派上用場的地方。我們給這個(gè)小部件提供動畫蛔添,并告訴它在動畫前進(jìn)時(shí)要繪制什么痰催。
好吧,讓我們做一些實(shí)際的事迎瞧。
首先夸溶,我們將在屏幕中間創(chuàng)建一個(gè)圓圈,并使其周期性地變大凶硅。
我們的主要功能運(yùn)行應(yīng)用程序并顯示AnimatedCirclePage
:
main.dart
import 'package:animation/pages/animated_circle_page.dart';
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: AnimatedCirclePage(),
);
}
}
在AnimatedCirclePage
最初顯示在頁面中間的圓圈:
animated_circle_page.dart
import 'package:flutter/material.dart';
class AnimatedCirclePage extends StatefulWidget {
@override
_AnimatedCirclePageState createState() => _AnimatedCirclePageState();
}
class _AnimatedCirclePageState extends State<AnimatedCirclePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Animated Circle"),
),
body: Center(
child: Container(
width: 50,
height: 50,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(
Radius.circular(25),
),
color: Colors.red,
),
),
),
);
}
}
現(xiàn)在讓我們用動畫放大這個(gè)圓圈缝裁。
在下面的代碼中,我們添加了一個(gè)類型的成員AnimationController
并在initState
方法中實(shí)例化它足绅。AnimationController
需要兩個(gè)參數(shù):Duration
和TickerProvider
捷绑。
duration參數(shù)指定動畫將持續(xù)多長時(shí)間韩脑,在我們的示例中,將需要1秒鐘才能完成粹污。
第二個(gè)參數(shù)被命名vsync
為type TickerProvider
段多。由于我們TickerProvider
使用以下mixin為我們的類添加了功能:
with SingleTickerProviderStateMixin
該類的當(dāng)前實(shí)例可以vsync
作為TickerProvider
:傳遞給參數(shù):
vsync: this
因此我們將:
animated_circle_page.dart
class _AnimatedCirclePageState extends State<AnimatedCirclePage>
with SingleTickerProviderStateMixin {
AnimationController animationController;
@override
void initState() {
super.initState();
animationController = AnimationController(
duration: Duration(seconds: 1),
vsync: this,
);
animationController.forward();
}
調(diào)用該forward
方法將啟動動畫并生成從0.0(下限)到1.0(上限)的值。但是我們?nèi)绾蜗纳傻膬r(jià)值呢壮吩?
使用AnimatedBuilder小部件衩匣!
animated_circle_page.dart
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Animated Circle"),
),
body: AnimatedBuilder(
animation: animationController,
builder: (BuildContext context, Widget child) {
final size = 100 * (animationController.value+1);
return Center(
child: Container(
width: size,
height: size,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(
Radius.circular(size/2),
),
color: Colors.red,
),
),
);
},
),
);
}
AnimationBuilder的構(gòu)造函數(shù)有三個(gè)參數(shù):
-
animation
:我們在這里提供動畫對象。請記住粥航,AnimationController繼承自Animation類。因此生百,我們的AnimationController屬于Animation類型递雀,可以傳遞給此參數(shù)。 -
child
:此可選參數(shù)是一個(gè)小部件蚀浆,在動畫期間不會更改缀程,只創(chuàng)建一次(以提高性能)。它總是可以在構(gòu)建器函數(shù)(下一個(gè)參數(shù))中重用市俊。 -
builder
:這是為動畫的每個(gè)刻度調(diào)用的函數(shù)杨凑。在這里,我們可以決定在動畫的下一幀中繪制什么摆昧。我們可以通過animation.value
屬性訪問當(dāng)前幀號撩满。
一旦我們調(diào)用該animationController.forward()
方法,我們的動畫就會啟動绅你,并且將為動畫的每個(gè)幀調(diào)用AnimatedBuilder的構(gòu)建器方法伺帘。在每個(gè)幀中,值animationController.value
將逐漸從0.0增加到1.0忌锯。我們可以利用這個(gè)值并根據(jù)它改變圓的寬度和高度:
final size = 100 * (animationController.value+1);
如您所見伪嫁,當(dāng)動畫值為0.0時(shí),圓的大小將為100偶垮,當(dāng)值增加到1.0時(shí)张咳,大小將更改為200.因此,我們必須看到以下動畫似舵,其中圓的大小從100變?yōu)?00到200:
擴(kuò)大圈
在上面的示例中脚猾,我們?yōu)樗袆赢嬛堤砑恿?。實(shí)際上啄枕,我們需要將動畫的范圍從[0.0 ... 1.0]更改為[1.0 ... 2.0]婚陪。你還記得這Tween
堂課對什么有用嗎?它用于修改動畫值频祝。所以我們可以在這里使用Tween類將動畫值從[0.0 ... 1.0]映射到[1.0 ... 2.0]泌参。我們開始做吧:
animated_circle_page.dart
void initState() {
super.initState();
animationController = AnimationController(
duration: Duration(
seconds: 1,
),
vsync: this,
);
animation = Tween(begin: 1.0, end: 2.0).animate(animationController);
animationController.forward();
}
映射動畫值實(shí)際上發(fā)生在以下行中:
animation = Tween(begin:1.0, end:2.0).animate(animationController);
我們創(chuàng)建一個(gè)實(shí)例Tween
并指定begin
和end
值脆淹。然后我們調(diào)用該animate
方法并將動畫對象傳遞給它。animate函數(shù)會返回一個(gè)新的animation
對象沽一,其值是從begin
到end
盖溺。然后在AnimatedBuilder對象中,我們只需將animation屬性設(shè)置為我們的新animation
對象:
body: AnimatedBuilder(
animation: animation,
并將圓的大小設(shè)置為:
final size = 100 * (animation.value);
反轉(zhuǎn)動畫
現(xiàn)在讓我們做一些有趣的事情铣缠。一旦我們的動畫完成烘嘱,我們將反轉(zhuǎn)動畫(這次它的值從1變?yōu)?)。這將使圓圈再次變小蝗蛙。
我們怎么知道我們的動畫是完整的蝇庭?
通過監(jiān)聽AnimationStatus
!
我們可以為動畫添加一個(gè)監(jiān)聽器捡硅,這樣每次狀態(tài)改變時(shí)哮内,我們都會收到通知。
animated_circle_page.dart
@override
void initState() {
super.initState();
animationController = AnimationController(
duration: Duration(
seconds: 1,
),
vsync: this,
);
animation = Tween(begin: 1.0, end: 2.0).animate(animationController);
animationController.addStatusListener(animationStatusListener);
animationController.forward();
}
void animationStatusListener(AnimationStatus status) {
if (status == AnimationStatus.completed) {
animationController.reverse();
} else if (status == AnimationStatus.dismissed) {
animationController.forward();
}
}
只有在狀態(tài)發(fā)生變化時(shí)才會調(diào)用我們的偵聽器函數(shù)壮韭。動畫有四種可能的狀態(tài):
- dismissed:動畫在開始時(shí)停止
- forward:動畫從頭到尾運(yùn)行
- reverse:動畫從頭到尾向后運(yùn)行
- completed:動畫在結(jié)束時(shí)停止
在上面的代碼中北发,我們檢查了狀態(tài)。如果是completed
喷屋,那意味著我們剛剛到達(dá)動畫的末尾琳拨,所以我們調(diào)用reverse
函數(shù),向后播放動畫屯曹。當(dāng)狀態(tài)變?yōu)?code>dismissed狱庇,表示動畫已經(jīng)到達(dá)開頭,所以我們forward
再次打電話恶耽!這個(gè)循環(huán)將永遠(yuǎn)持續(xù)下去僵井!
生成的動畫是一個(gè)連續(xù)大小的圓圈:
現(xiàn)在讓我們做一些更有趣的事情。我們將圍繞屏幕中心旋轉(zhuǎn)這個(gè)圓圈!
rotating_circle_page.dart
使用以下代碼創(chuàng)建一個(gè)名為的新頁面:
import 'package:flutter/material.dart';
class RotatingCirclePage extends StatefulWidget {
@override
_RotatingCirclePageState createState() => _RotatingCirclePageState();
}
class _RotatingCirclePageState extends State<RotatingCirclePage>
with SingleTickerProviderStateMixin {
Widget _buildCircle(radius) {
return Container(
width: radius * 2,
height: radius * 2,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(
Radius.circular(radius),
),
color: Colors.red,
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Rotating Circle"),
),
body: Align(
alignment: Alignment(0, -0.1),
child: _buildCircle(30.0),
),
);
}
}
為了使代碼更具可讀性,我創(chuàng)建了一個(gè)輔助函數(shù)_buildCircle
拗小,用于繪制具有給定半徑的紅色圓圈蚪拦。而不是將圓圈居中,我將它對齊在頁面中心的上方。(如果您不熟悉,請觀看谷歌的這個(gè)簡短視頻以熟悉Align
小部件)。結(jié)果是:
現(xiàn)在讓我們像鐘擺一樣為這個(gè)圓圈制作動畫:
animated_circle_page.dart
import 'package:flutter/material.dart';
import 'dart:math' as math;
class RotatingCirclePage extends StatefulWidget {
@override
_RotatingCirclePageState createState() => _RotatingCirclePageState();
}
class _RotatingCirclePageState extends State<RotatingCirclePage>
with SingleTickerProviderStateMixin {
AnimationController animationController;
Animation animation;
@override
void initState() {
super.initState();
animationController = AnimationController(
duration: Duration(
seconds: 2,
),
vsync: this,
);
animation = CurvedAnimation(
parent: animationController,
curve: Curves.fastOutSlowIn,
);
animationController.addStatusListener(animationStatusListener);
animationController.forward();
}
void animationStatusListener(AnimationStatus status) {
if (status == AnimationStatus.completed) {
animationController.reverse();
} else if (status == AnimationStatus.dismissed) {
animationController.forward();
}
}
Widget _buildCircle(radius) {
return Container(
width: radius * 2,
height: radius * 2,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(
Radius.circular(radius),
),
color: Colors.red,
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Rotating Circle"),
),
body: Align(
alignment: Alignment(0, -0.1),
child: AnimatedBuilder(
child: _buildCircle(30.0),
animation: animationController,
builder: (BuildContext context, Widget child) {
return Transform.rotate(
child: child,
angle: math.pi * 2 * animation.value,
origin: Offset(0, 30),
);
},
),
),
);
}
}
讓我解釋一下上面代碼的重要部分合呐。
animation = CurvedAnimation(
parent: animationController,
curve: Curves.fastOutSlowIn,
);
默認(rèn)情況下,AnimationController
在給定的持續(xù)時(shí)間內(nèi)線性生成從0.0到1.0的數(shù)字笙以,因此動畫無任何速度播放淌实。如果我們想要改變動畫的速度和樣式,我們可以將它包裝在CurvedAnimation小部件中:
當(dāng)您想要將非線性曲線應(yīng)用于動畫對象時(shí),CurvedAnimation非常有用拆祈,特別是當(dāng)您想要動畫前進(jìn)時(shí)的曲線與后退時(shí)的曲線時(shí)恨闪。
請注意,可以首先在CurvedAnimation小部件中包裝動畫放坏,然后使用Tween小部件轉(zhuǎn)換其下限和上限咙咽,如下所示:
animation = Tween(begin: 5.0, end: 10.0).animate(
CurvedAnimation(
parent: animationController,
curve: Curves.fastOutSlowIn,
),
);
在這里,我使用了曲線淤年。fastOutSlowIn曲線钧敞,但您可以使用其他值并查看它們?nèi)绾斡绊憚赢嫷乃俣群退俣取?/p>
現(xiàn)在讓我解釋一下構(gòu)建方法:
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Rotating Circle"),
),
body: Align(
alignment: Alignment(0, -0.1),
child: AnimatedBuilder(
child: _buildCircle(30.0),
animation: animationController,
builder: (BuildContext context, Widget child) {
return Transform.rotate(
child: child,
angle: math.pi * 2 * animation.value,
origin: Offset(0, 30),
);
},
),
),
);
}
我們已將該child
屬性設(shè)置AnimatedBuilder
為圓圈。為什么麸粮?因?yàn)槲覀兿M粍?chuàng)建一次溉苛,而不是每一幀!(為了提高性能弄诲,我們不需要重建不隨時(shí)間變化的動畫部分炊昆。這里我們的圓圈大小在動畫期間保持不變,所以我們只構(gòu)建它一次并將它分配給孩子AnimatedBuilder
威根。builder
每次繪制新幀時(shí),該子項(xiàng)都可以在方法中重復(fù)使用视乐。
在builder
我們動畫的每一幀都會調(diào)用的方法中洛搀,我們習(xí)慣Transform.rotate
了旋轉(zhuǎn)圓圈。如果我們不指定origin
參數(shù)佑淀,圓圈將圍繞其自身的中心旋轉(zhuǎn)(在這種情況下留美,由于圓圈圍繞其自身的中心旋轉(zhuǎn),我們將看不到任何旋轉(zhuǎn)I烊小)谎砾。出于這個(gè)原因,我們將旋轉(zhuǎn)中心設(shè)置為偏移(0,30)表示的點(diǎn)捧颅,該點(diǎn)是距離窗口小部件中心的x距離為0景图,y距離為30的點(diǎn)。請查看以下內(nèi)容圖片碉哑。圓圈現(xiàn)在將圍繞標(biāo)有X的原點(diǎn)旋轉(zhuǎn):
旋轉(zhuǎn)角度已設(shè)置為:
angle: math.pi * 2 * animation.value,
由于動畫值從0變?yōu)?挚币,旋轉(zhuǎn)角度將從0變?yōu)?strong>2π,這等于完整的360°旋轉(zhuǎn)扣典。
重要說明:我沒有包含_buildCircle(30.0)
在Align小部件中妆毕。相反,我已經(jīng)在Align小部件中包裝了整個(gè)動畫贮尖,這是我們的AnimatedWidget對象笛粘。那是因?yàn)槲覀冎幌胄D(zhuǎn)圓圈,而不是旋轉(zhuǎn)它周圍的空間!如果我們在Align小部件中包裝了AnimatedBuilder的子節(jié)點(diǎn)薪前,那么我們的圓圈周圍會有一個(gè)額外的空間润努,這會導(dǎo)致我們的計(jì)算出錯(cuò)。我的全部觀點(diǎn)是以下代碼是錯(cuò)誤的:
animated_circle_page.dart
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Rotating Circle"),
),
body: AnimatedBuilder(
child: Align(
alignment: Alignment(0, -0.1),
child: _buildCircle(30.0),
),
animation: animationController,
builder: (BuildContext context, Widget child) {
return Transform.rotate(
child: child,
angle: math.pi * 2 * animation.value,
origin: Offset(0, 30),
);
},
),
);
}
與上一個(gè)動畫一樣序六,當(dāng)動畫完成時(shí)任连,我們將其反轉(zhuǎn),使其從2π旋轉(zhuǎn)回0 度例诀。結(jié)果是以下動畫:
圓周圍原點(diǎn)旋轉(zhuǎn)
在上一篇文章中随抠,我們學(xué)習(xí)了如何繪制彎曲的虛線。我告訴過你關(guān)于創(chuàng)建以下動畫的信息:
我將在github上的本文代碼中包含上述動畫的源代碼繁涂。但我建議你自己創(chuàng)作作為家庭作業(yè)拱她!請注意,我沒有在彎曲的路徑上移動碟子扔罪。我正在使用Transform.translate小部件在兩條獨(dú)立的直線上設(shè)置動畫秉沼。Transform.translate
可用于在繪制對象之前dx
和dy
之前偏移對象。
摘要矿酵,回顧和最終說明
而已唬复。你可以在這里停止閱讀!我只想強(qiáng)調(diào)以下注釋全肮,我從Flutter提供的關(guān)于動畫的三篇文章中抓取了這些文章(閱讀這些文章很好):
要創(chuàng)建動畫敞咧,首先要創(chuàng)建一個(gè)AnimationController。除了作為動畫本身辜腺,還AnimationController
可以控制動畫休建。例如,您可以告訴控制器向前播放動畫或停止動畫评疗。
AnimationController是一個(gè)特殊Animation
對象测砂,只要硬件準(zhǔn)備好新幀,它就會生成一個(gè)新值百匆。默認(rèn)情況下砌些,AnimationController
在給定的持續(xù)時(shí)間內(nèi)線性生成從0.0到1.0的數(shù)字。
AnimationController
派生自Animation<double>
加匈,因此它可以在需要Animation
對象的任何地方使用寄症。但是,AnimationController
還有其他控制動畫的方法矩动。例如有巧,您使用該.forward()
方法啟動動畫。數(shù)字的生成與屏幕刷新有關(guān)悲没,因此通常每秒生成60個(gè)數(shù)字篮迎。
的[Tween(https://api.flutter.dev/flutter/animation/Tween-class.html)抽象類中的類型值的范圍0.0-1.0映射名義上的雙精度值(例如Color
男图,或另一種雙)。這是一個(gè)Animatable
甜橱。要設(shè)置超過0.0到1.0間隔的動畫逊笆,可以使用a [Tween<T>](https://api.flutter.dev/flutter/animation/Tween-class.html)
,它在其開始值和結(jié)束值之間進(jìn)行插值岂傲。許多類型都有特定的Tween
子類难裆,它們提供特定于類型的插值。例如镊掖,ColorTween在顏色之間插值乃戈,RectTween在矩形之間插值。A Tween
繼承自Animatable<T>
而非繼承Animation<T>
亩进。像動畫一樣的Animatable不必輸出double症虑。例如,ColorTween
指定兩種顏色之間的進(jìn)展:
colorTween = ColorTween(begin: Colors.transparent, end: Colors.black54);
可以在Github上找到本文的源代碼归薛。
動畫菜的源代碼可以在這里找到谍憔。
謝謝閱讀!
轉(zhuǎn):https://medium.com/@meysam.mahfouzi/understanding-animations-in-flutter-b8ec789d94a4