
老孟導讀:影片系統是任何一個UI框架的核心功能,也是開發者學習一個UI框架的重中之重,同時也是比較難掌握的一部分,下面我們就一層一層的揭開 Flutter 影片的面紗,
任何程式的影片原理都是一樣的,即:視覺暫留,視覺暫留又叫視覺暫停,人眼在觀察景物時,光信號傳入大腦神經,需經過一段短暫的時間,光的作用結束后,視覺形象并不立即消失,這種殘留的視覺稱“后像”,視覺的這一現象則被稱為“視覺暫留”,
人眼能保留0.1-0.4秒左右的影像,所以在 1 秒內看到連續的25張影像,人就會感到畫面流暢,而 1 秒內看到連續的多少張影像稱為 幀率,即 FPS,理論上 達到 24 FPS 畫面比較流暢,而Flutter,理論上可以達到 60 FPS,
AnimationController
介紹完了影片系統的基本原理,實作一個藍色盒子大小從 100 變為 200影片效果:
class AnimationBaseDemo extends StatefulWidget {
@override
_AnimationBaseDemoState createState() => _AnimationBaseDemoState();
}
class _AnimationBaseDemoState extends State<AnimationBaseDemo> {
double _size = 100;
@override
Widget build(BuildContext context) {
return Center(
child: GestureDetector(
onTap: () {
setState(() {
_size = 200;
});
},
child: Container(
height: _size,
width: _size,
color: Colors.blue,
alignment: Alignment.center,
child: Text('點我變大',style: TextStyle(color: Colors.white,fontSize: 18),),
),
),
);
}
}

雖然變大了,但并沒有影片效果,而是直接變大的,想要使其一點點放大需要引入 AnimationController,它是影片控制器,控制影片的啟動、停止,還可以獲取影片的運行狀態,AnimationController 通常在 initState 方法中初始化:
class _AnimationBaseDemoState extends State<AnimationBaseDemo> with SingleTickerProviderStateMixin{
double _size = 100;
AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this,duration: Duration(milliseconds: 500));
}
...
}
這里有兩個引數需要設定:
- vsync:當創建 AnimationController 時,需要傳遞一個
vsync引數,存在vsync時會防止螢屏外影片消耗不必要的資源,單個 AnimationController 的時候使用 SingleTickerProviderStateMixin,多個 AnimationController 使用 TickerProviderStateMixin, - duration:表示影片執行的時間,
修改如下:
class _AnimationBaseDemoState extends State<AnimationBaseDemo> with SingleTickerProviderStateMixin{
double _size = 100;
AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this,duration: Duration(milliseconds: 500));
}
@override
Widget build(BuildContext context) {
return Center(
child: GestureDetector(
onTap: () {
_controller.forward();
},
child: Container(
height: _size,
width: _size,
color: Colors.blue,
alignment: Alignment.center,
child: Text('點我變大',style: TextStyle(color: Colors.white,fontSize: 18),),
),
),
);
}
@override
void dispose() {
super.dispose();
_controller.dispose();
}
}
點擊藍色盒子的時候不再直接更改大小,而是執行影片_controller.forward(),
另外在State dispose 生命周期中釋放 AnimationController,
此時點擊藍色盒子發現并不會變大,StatefulWidget 組件改變外觀需要呼叫 setState,因此給 AnimationController 添加監聽:
_controller = AnimationController(vsync: this,duration: Duration(milliseconds: 500))
..addListener(() {
setState(() {
_size = 100+100*_controller.value;
});
});
每一幀都會回呼addListener ,在此回呼中設定藍色盒子大小,藍色的大小是由 100 變到 200,而 AnimationController 的值默認是 0 到 1,所以藍色大小等于 _size = 100+100*_controller.value,運行效果:

這就是 Flutter 中最簡單影片的實作方式,其中最重要的就是 AnimationController,_controller.value 是當前影片的值,默認從 0 到 1,也可以通過引數形式設定最大值和最小值:
_controller = AnimationController(vsync: this,duration: Duration(milliseconds: 500),lowerBound: 100,upperBound: 200)
..addListener(() {
setState(() {
_size = _controller.value;
});
})
此時 _controller.value 的值就是從 100變化到 200,
除了使用 addListener 監聽每一幀,還可以監聽影片狀態的變化:
_controller = AnimationController(
vsync: this,
duration: Duration(milliseconds: 500),
lowerBound: 100,
upperBound: 200)
..addStatusListener((status) {
print('status:$status');
})
影片的狀態分為四種:
- dismissed:影片停止在開始處,
- forward:影片正在從開始處運行到結束處(正向運行),
- reverse:影片正在從結束處運行到開始處(反向運行),
- completed:影片停止在結束處,
再來看下影片的控制方法:
- forward:正向執行影片,
- reverse:反向執行影片,
- repeat:反復執行影片,
- reset:重置影片,
forward 和 reverse 方法中都有 from 引數,這個引數的意義是一樣的,表示影片從此值開始執行,而不再是從lowerBound 到 upperBound,比如上面的例子中 from 引數設定 150,那么執行影片時,藍色盒子瞬間變為 150,然后再慢慢變大到200,
讓藍色盒子大小從 100 到 200,然后再變到 100,再到 200,如此反復:
_controller = AnimationController(
vsync: this,
duration: Duration(milliseconds: 500),
lowerBound: 100,
upperBound: 200)
..addStatusListener((AnimationStatus status) {
if(status == AnimationStatus.completed){
_controller.reverse();
}else if(status == AnimationStatus.dismissed){
_controller.forward();
}
})
..addListener(() {
setState(() {
_size = _controller.value;
});
});
只需監聽影片狀態變化,在影片結束后再正向/反向再次執行影片,

雖然上面講了很多,但只有一個重點 AnimationController,
AnimationController 設定的最小/大值型別是 double,如果影片的變化是顏色要如何處理?
AnimationController 在執行影片期間回傳的值是 0 到 1,顏色從藍色變為紅色方法如下:
_controller =
AnimationController(vsync: this, duration: Duration(milliseconds: 500))
..addListener(() {
setState(() {
_color = Color.lerp(_startColor, _endColor, _controller.value);
});
});
重點是 Color.lerp 方法,此方法是在兩種顏色之間線性插值,
完整代碼如下:
class TweenDemo extends StatefulWidget {
@override
_TweenDemoState createState() => _TweenDemoState();
}
class _TweenDemoState extends State<TweenDemo>
with SingleTickerProviderStateMixin {
AnimationController _controller;
Color _startColor = Colors.blue;
Color _endColor = Colors.red;
Color _color = Colors.blue;
@override
void initState() {
super.initState();
_controller =
AnimationController(vsync: this, duration: Duration(milliseconds: 500))
..addListener(() {
setState(() {
_color = Color.lerp(_startColor, _endColor, _controller.value);
});
});
}
@override
Widget build(BuildContext context) {
return Center(
child: GestureDetector(
onTap: () {
_controller.forward();
},
child: Container(
height: 100,
width: 100,
color: _color,
alignment: Alignment.center,
child: Text(
'點我變色',
style: TextStyle(color: Colors.white, fontSize: 18),
),
),
),
);
}
@override
void dispose() {
super.dispose();
_controller.dispose();
}
}

Flutter 中把這種從 0 -> 1 轉換為 藍色 -> 紅色 行為稱之為 Tween(映射),
使用 Tween 完成影片 藍色 -> 紅色:
class _TweenDemoState extends State<TweenDemo>
with SingleTickerProviderStateMixin {
AnimationController _controller;
Animation<Color> _animation;
@override
void initState() {
super.initState();
_controller =
AnimationController(vsync: this, duration: Duration(milliseconds: 500))
..addListener(() {
setState(() {});
});
_animation =
ColorTween(begin: Colors.blue, end: Colors.red).animate(_controller);
}
@override
Widget build(BuildContext context) {
return Center(
child: GestureDetector(
onTap: () {
_controller.forward();
},
child: Container(
height: 100,
width: 100,
color: _animation.value,
alignment: Alignment.center,
child: Text(
'點我變色',
style: TextStyle(color: Colors.white, fontSize: 18),
),
),
),
);
}
@override
void dispose() {
super.dispose();
_controller.dispose();
}
}
效果和上面是一樣的,
Tween 僅僅是映射,影片的控制依然由 AnimationController 控制,因此需要 Tween.animate(_controller) 將控制器傳遞給Tween,
系統提供了大量的 Tween:

基本上常用的屬性都包含了其對應的 Tween,看一下 ColorTween 的源代碼實作:

本質上也是使用 Color.lerp 實作的,
Curve
影片中還有一個重要的概念就是 Curve,即影片執行曲線,Curve 的作用和 Android 中的 Interpolator(差值器)是一樣的,負責控制影片變化的速率,通俗地講就是使影片的效果能夠以勻速、加速、減速、拋物線等各種速率變化,
藍色盒子大小 100 變大到 200,影片曲線設定為 bounceIn(彈簧效果) :
class CurveDemo extends StatefulWidget {
@override
_CurveDemoState createState() => _CurveDemoState();
}
class _CurveDemoState extends State<CurveDemo>
with SingleTickerProviderStateMixin {
AnimationController _controller;
Animation _animation;
@override
void initState() {
super.initState();
_controller =
AnimationController(vsync: this, duration: Duration(milliseconds: 1000))
..addListener(() {
setState(() {});
});
_animation = Tween(begin: 100.0, end: 200.0)
.chain(CurveTween(curve: Curves.bounceIn))
.animate(_controller);
}
@override
Widget build(BuildContext context) {
return Center(
child: GestureDetector(
onTap: () {
_controller.forward();
},
child: Container(
height: _animation.value,
width: _animation.value,
color: Colors.blue,
alignment: Alignment.center,
child: Text(
'點我變大',
style: TextStyle(color: Colors.white, fontSize: 18),
),
),
),
);
}
@override
void dispose() {
super.dispose();
_controller.dispose();
}
}

影片加上Curve 后,AnimationController 的最小/大值必須是 [0,1]之間,例如下面的寫法就是錯誤的:
_controller =
AnimationController(vsync: this, duration: Duration(milliseconds: 1000),lowerBound: 100.0,upperBound: 200.0)
..addListener(() {
setState(() {});
});
_animation = CurveTween(curve: Curves.bounceIn).animate(_controller);
拋出如下例外:

正確寫法:
_controller =
AnimationController(vsync: this, duration: Duration(milliseconds: 1000))
..addListener(() {
setState(() {});
});
_animation = Tween(begin: 100.0, end: 200.0)
.chain(CurveTween(curve: Curves.bounceIn))
.animate(_controller);
系統已經提供了38種常用到影片曲線:
linear

decelerate

bounceIn

bounceOut

elasticIn

其余影片效果可以官方檔案查看,
通常情況下,這些曲線能夠滿足 99.99% 的需求,很多時候設計也就是告訴你影片 先快后慢 或者 先慢后快,所以選個類似的就可以了,但有一些 特別 的設計非要一個系統沒有的影片曲線,要怎么辦?
那就自定義一個影片曲線
其實自定義一個影片曲線難點在 數學 上,怎么把數學公式用代碼實作才是難點,
下面是一個 樓梯效果 的影片曲線:

自定義影片曲線需要繼承 Curve 重寫 transformInternal 方法即可:
class _StairsCurve extends Curve {
@override
double transformInternal(double t) {
return t;
}
}
直接回傳 t 其實就是線性影片,即 Curves.linear,實作樓梯效果影片代碼如下:
class _StairsCurve extends Curve {
//階梯的數量
final int num;
double _perStairY;
double _perStairX;
_StairsCurve(this.num) {
_perStairY = 1.0 / (num - 1);
_perStairX = 1.0 / num;
}
@override
double transformInternal(double t) {
return _perStairY * (t / _perStairX).floor();
}
}
修改開始處的案例,使用此曲線:
_animation = Tween(begin: 100.0, end: 200.0)
.chain(CurveTween(curve: _StairsCurve(5)))
.animate(_controller);

總結
影片系統的核心是 AnimationController,而且是不可或缺的,影片中必須有 AnimationController,而 Tween 和 Curve 則是對 AnimationController 的補充, Tween 實作了將 AnimationController [0,1]的值映射為其他型別的值,比如顏色、樣式等,Curve 是 AnimationController 影片執行曲線,默認是線性運行,
將 AnimationController 、 Tween 、Curve 進行關聯的方式:
AnimationController _controller;
Animation _animation;
@override
void initState() {
super.initState();
_controller =
AnimationController(vsync: this, duration: Duration(milliseconds: 1000))
..addListener(() {
setState(() {});
});
_animation = Tween(begin: 100.0, end: 200.0)
.animate(_controller);
}
或者:
_animation = _controller.drive(Tween(begin: 100.0, end: 200.0));
加入 Curve :
_animation = Tween(begin: 100.0, end: 200.0)
.chain(CurveTween(curve: Curves.linear))
.animate(_controller);
或者:
_animation = _controller
.drive(CurveTween(curve: Curves.linear))
.drive(Tween(begin: 100.0, end: 200.0));
只需要 Curve :
_animation = CurveTween(curve: Curves.linear)
.animate(_controller);
或者
_animation = _controller.drive(CurveTween(curve: Curves.linear));
一個 AnimationController 可以對應多個 Animation(Tween 或者 Curve),StatefulWidget 組件可以包含多個 AnimationController ,但 SingleTickerProviderStateMixin 需要修改為 TickerProviderStateMixin,改變顏色和大小,由兩個 AnimationController 控制:
class MultiControllerDemo extends StatefulWidget {
@override
_MultiControllerDemoState createState() => _MultiControllerDemoState();
}
class _MultiControllerDemoState extends State<MultiControllerDemo>
with TickerProviderStateMixin {
AnimationController _sizeController;
AnimationController _colorController;
Animation<double> _sizeAnimation;
Animation<Color> _colorAnimation;
@override
void initState() {
super.initState();
_sizeController =
AnimationController(vsync: this, duration: Duration(milliseconds: 2000))
..addListener(() {
setState(() {});
});
_sizeAnimation = _sizeController
.drive(CurveTween(curve: Curves.linear))
.drive(Tween(begin: 100.0, end: 200.0));
_colorController =
AnimationController(vsync: this, duration: Duration(milliseconds: 1000))
..addListener(() {
setState(() {});
});
_colorAnimation = _colorController
.drive(CurveTween(curve: Curves.bounceIn))
.drive(ColorTween(begin: Colors.blue, end: Colors.red));
}
@override
Widget build(BuildContext context) {
return Center(
child: GestureDetector(
onTap: () {
_sizeController.forward();
_colorController.forward();
},
child: Container(
height: _sizeAnimation.value,
width: _sizeAnimation.value,
color: _colorAnimation.value,
alignment: Alignment.center,
child: Text(
'點我變化',
style: TextStyle(color: Colors.white, fontSize: 18),
),
),
),
);
}
@override
void dispose() {
super.dispose();
_sizeController.dispose();
_colorController.dispose();
}
}

AnimationController 、Tween 、Curve 是整個影片的基礎,Flutter 系統封裝了大量了影片組件,但這些組件也是基于此封裝的,因為深入了解這三部分比學習使用影片組件更重要,再次對這3個進行總結:
- AnimationController:影片控制器,控制影片的播放、停止等,繼承自Animation< double >,是一個特殊的Animation物件,默認情況下它會線性的生成一個0.0到1.0的值,型別只能是 double 型別,不設定影片曲線的情況下,可以設定輸出的最小值和最大值,
- Curve:影片曲線,作用和Android中的Interpolator(差值器)類似,負責控制影片變化的速率,通俗地講就是使影片的效果能夠以勻速、加速、減速、拋物線等各種速率變化,
- Tween:將 AnimationController 生成的 [0,1]值映射成其他屬性的值,比如顏色、樣式等,
完成一個影片效果的程序如下:

- 創建 AnimationController ,
- 如果需要 Tween 或者 Curve,將 AnimationController 與其關聯,Tween 和 Curve 并不是必須的,當然大部分情況都需要,
- 將影片值作用于組件,當沒有Tween 和 Curve 時,影片值來源于AnimationController,如果有 Tween 和 Curve,影片值來源于 Tween 或者Curve 的 Animation,
如果你發現閱讀完此篇文章還是感覺不會寫影片,不要灰心,這是正常的,第一次想了解這些抽象的概念還是比較困難的,如果你有其他平臺的相關經驗,那會好很多,對于影片,想要掌握個人認為只有一個方法就是 多寫,
后面會介紹影片組件基礎使用、實作原理、高級影片以及自定義影片,把每一個影片組件的用法都親自手寫一遍(而不是復制黏貼),回過頭來在看這篇文章,會有不一樣的感覺,
交流
老孟Flutter博客地址(330個控制元件用法):http://laomengit.com
歡迎加入Flutter交流群(微信:laomengit)、關注公眾號【老孟Flutter】:
![]() |
![]() |
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/292.html
標籤:Dart
上一篇:Flutter 中漸變的高級用法


