Flutter 中動畫的建立有很多種, 需要根據具體的需求選擇不同的動畫。如果只是簡單的佈局等的動畫直接使用最簡單的隱式動畫就可以了,因為隱式動畫是由框架控制的,所以僅僅只需要更改變需要變化屬性就可以了。如果你想自己控制動畫的變換則需要使用顯示動畫,如果需要控制一些列動畫組合時使用交織動畫去控制。如果內建的滿足不了需求的時候,還可以結合畫布自繪動畫。
動畫基礎
Flutter動畫和其他平臺動畫原理也是一樣的,都是在快速更改UI實現動畫效果。在一個Flutter動畫中主要包含Animation(動畫)、AnimationController(控制器)、Curve(速度曲線)、Animatable(動畫取值範圍)、Listeners (監聽事件)、Ticker(幀)。
- Animation 一個抽象類是Flutter動畫的核心類,主用於儲存動畫當前插值的和狀態,在動畫執行時會持續生成介於兩個值之間的插入值。例如當寬從100變成200,會在動畫第一幀到最後一幀都會生成100-200區間的一個值,如果速度是勻速的,這個值就是勻速增加到200。
- AnimationController 用來控制動畫的狀態啟動、暫停、反向執行等, 是Animation的一個子類
- Curve 用來定義動畫運動的是勻速運動還是勻加速等,和 css 中 animation-timing-function 類似
- Animatable 用於表明動畫值範圍值。可以通過呼叫animate方法,返回一個Animation,常見的Tween系列的類都是對他的實現
- Listener 監聽動畫狀態的變化
- Ticker 幀回撥,在動畫執行時候每一幀都會呼叫其回撥,類似與 js 中的 requestAnimationFrame
動畫組成結構
動畫選擇
隱式動畫
隱式動畫簡單來說就是我們只需要修改對應的屬性,Flutter就是自己幫我們過渡動畫,和css中過渡有點類似,當我們設定後transition後只需要更改對應的css屬性就會自動過渡到新的值。Flutter 內建了一些常用的隱式動畫,可以看到原始碼裡都是對ImplicitlyAnimatedWidget的實現,如果需要我們也可以自己實現ImplicitlyAnimatedWidget來自定義隱式動畫。
內建隱式動畫
看個使用例子
// 首先我們在一個StatefulWidget定義 一個height和color
double heihgt = 100;
Color color = Colors.yellow[800];
// 在build 怎加一個隱式動畫組建 AnimatedContainer,需要個Duration(動畫執行時間),其他的引數和Container的基本一致
AnimatedContainer(
duration: Duration(milliseconds: 500),
height: heihgt, // 使用我們定義好的值
color: color,
margin: EdgeInsets.all(8),
child: Center(
child: Text('AnimatedContainer',
style: TextStyle(color: Colors.white, fontSize: 20),
),
),
)
// 在需要執行動畫時候我們修改 height 和 color 值,就會看到 上邊的組建會一邊變高邊過渡到藍色上
setState(() {
heihgt = 200;
color = Colors.blue;
});
在Flutter內建的隱式動畫元件中,一般都是AnimatedXxxxxx類似的,後面的Xxxxxx都能找到對應的元件。內建的有下面這些 AnimatedContainer、AnimatedPadding、AnimatedAlign、AnimatedPositioned、AnimatedOpacity、SliverAnimatedOpacity、AnimatedDefaultTextStyle、AnimatedPhysicalModel。這些隱式動畫的使用和其Xxxxxx對應的屬性基本一致,只需要額外的指定 duration 就可以了,當然也可以為動畫指定動畫曲線 curve。
自定義隱式動畫
當這內建的滿足不了你的時候,你也可以去實現一個隱式動畫,只需要實現抽象類 ImplicitlyAnimatedWidget。實現自定義隱式動畫僅需要重寫build 和 forEachTween 就可以簡單實現了。
// 直接繼承ImplicitlyAnimatedWidget
class AnimatedDemo extends ImplicitlyAnimatedWidget {
final Color color;
final Widget child;
final double height;
AnimatedDemo({
this.color,
this.height,
Curve curve = Curves.linear,
this.child,
@required Duration duration,
}) : super(curve: curve, duration: duration);
@override
_AnimatedDemo createState() => _AnimatedDemo();
}
//因為ImplicitlyAnimatedWidget是繼承 StatefulWidget 的,所以還需要繼承他的狀態類 (AnimatedWidgetBaseState 繼承自 ImplicitlyAnimatedWidgetState)
class _AnimatedDemo extends AnimatedWidgetBaseState<AnimatedDemo> {
ColorTween _color;
Tween<double> _height;
// 在動畫執行時候會每一幀都呼叫 build
@override
Widget build(BuildContext context) {
return Container(
color: _color.evaluate(animation), //使用evaluate可以獲取Tween當前幀的狀態值
height: _height.evaluate(animation),
child: widget.child,
);
}
//首次build和更新時候會呼叫,在這裡設定動畫需要的Tween的開始值和結束值
@override
void forEachTween(visitor) {
//visitor 有三個引數(當前的tween,動畫終止狀態,一個回撥函式(將第一次給定的值設定為Tween的開始值))
_color = visitor(_color, widget.color, (value) => ColorTween(begin: value));// 這裡value==首次widget.color的值
_height = visitor(_height, widget.height, (value) => Tween<double>(begin: value));
}
}
我們可以去看 ImplicitlyAnimatedWidget 是如何控制動畫的,在 ImplicitlyAnimatedWidgetState 中會看到其實裡面定義了 AnimationController 控制動畫。然後可以看到 didUpdateWidget 鉤子函式中呼叫了 _controller.forward() 執行動畫,當父 Widget 呼叫 setState 時候就會觸發這個鉤子函式的呼叫。
顯示動畫
有時候有些動畫需要們自己去控制動畫的狀態,而不是交給框架去處理,這時就需要我們自己去定義前面簡介裡提到的那幾個動畫要素了。
內建顯示動畫
在Flutter中內建的顯示動畫大部分都是XxxxxxTransition名稱的,我們看個內建顯示動畫使用例子,RotationTransition元件需要一個 turns(Animation<double>)引數,我們可以給它個AnimationController
// RotationTransition 引數
RotationTransition(
turns: Animation<double>,
child: ChildWidget(),
)
// AnimationController 引數
AnimationController(
double? value, // 初始值
this.duration, //動畫時間
this.reverseDuration, // 反向動畫執行的時間
this.debugLabel,
this.lowerBound = 0.0, //動畫開始值
this.upperBound = 1.0, //動畫結束值
this.animationBehavior = AnimationBehavior.normal,
required TickerProvider vsync, //垂直同步,需要一個 Ticker ,Flutter 給我們提供了
)
使用 RotationTransition,可以看到一個紅藍漸變色方塊旋轉一週。
class RotationTransitionDemo extends StatefulWidget {
@override
_RotationTransitionDemoState createState() => _RotationTransitionDemoState();
}
class _RotationTransitionDemoState extends State<RotationTransitionDemo> with SingleTickerProviderStateMixin {
AnimationController _controller;
@override
void initState() {
super.initState();
// 設定動畫時間為1秒
_controller = AnimationController(duration: Duration(milliseconds: 1000), vsync: this)
..addListener(() { // 監聽動畫的狀態值發生變化
print(_controller.value);
})
..addStatusListener((status) { //監聽動畫狀態
// dismissed 動畫在起始點停止
// forward 動畫正在正向執行
// reverse 動畫正在反向執行
// completed 動畫在終點停止
print(status);
})
..forward(); // 執行動畫
// 常用方法
// forward() // 正向執行動畫
// reverse() 反向執行動畫
// repeat() 重複執行 可以傳個引數 是否會反向運動
// stop() 停止動畫
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('RotationTransition'),
),
body: Center(
child: RotationTransition(
turns: _controller, // 設定 Animation
child: Container(
height: 300,
width: 300,
decoration: BoxDecoration(
gradient: LinearGradient(colors: [Colors.red, Colors.blue]),
),
),
),
),
);
}
}
控制器補間和曲線
在控制器中我們可以看的動畫開始值和結束值預設是0.0到1.0,而且是double型別的。而實際動畫中不可能只是double型別的,需要我們自己使用Animatable來指定補間範圍值。
修改一下上面的程式碼
// 通過控制器的drive方法新增
_controller = AnimationController(duration: Duration(milliseconds: 1000),vsync: this)
..drive(Tween(begin: 1, end: 4)) //使用Tween(Animatable的子類)指定補間範圍
// 我也也可以是使用Animatable的animate方法新增到控制器
Tween(begin: 1, end: 4).animate(_controller);
// 這樣寫我們可以使用 chain() 疊加多個 Tween
Tween(begin: 1, end: 4)
.chain(CurveTween(curve: Curves.ease)) //疊加個曲線
.animate(_controller);
Flutter已經內建幫我們實現了很多Animatable,ColorTween、SizeTween、IntTween、StepTween等等。
自定義顯示動畫
檢視 RotationTransition 的原始碼,我們可以看到它是對的抽象類 AnimatedWidget 的實現,當內建的滿足不了我們的時候,可以直接自己實現 AnimatedWidget 自定義顯示動畫。先來看看 AnimatedWidget 裡面都有些啥。
// 只摘取主要的部分
abstract class AnimatedWidget extends StatefulWidget {
const AnimatedWidget({ Key key,@required this.listenable, }) : assert(listenable != null), super(key: key);
@override
_AnimatedState createState() => _AnimatedState();
}
class _AnimatedState extends State<AnimatedWidget> {
@override
void initState() {
super.initState();
widget.listenable.addListener(_handleChange);
}
void _handleChange() {
setState(() {
// 我們可以看到顯示動畫是通過控制器監聽插值更改 setState 進行重繪。
});
}
}
接下來我自己繼承 AnimatedWidget 實現一個自定義顯示動畫
// 繼承 AnimatedWidget
class OpacityAnimatedWidget extends AnimatedWidget {
final Widget child;
Animation<Color> colorAnimation;
// AnimatedWidget 需要可傳遞一個 listenable 進去,我們可以傳遞個 AnimationController
OpacityAnimatedWidget(listenable, {this.colorAnimation, this.child}) : super(listenable: listenable);
@override
Widget build(BuildContext context) {
Animation<double> animation = listenable;
return Opacity(
opacity: animation.value,
child: Container(
color: colorAnimation.value,
child: child,
),
);
}
}
// 使用 需要在狀態類上 混入一個 SingleTickerProviderStateMixin
AnimationController _controller = AnimationController(duration: Duration(milliseconds: 1000), vsync: this);
OpacityAnimatedWidget(
Tween(begin: 1.0, end: .8).animate(_controller),
colorAnimation: ColorTween(begin: Colors.red, end: Colors.blue).animate(_controller),
child: Container(
height: 300,
width: 300,
),
)
Flutter 內部還提供了一個 AnimatedBuilder 幫助我們簡化自定義動畫。
// 只需要三個三引數
AnimatedBuilder(
animation, // 一個listenable
child,// 傳入個子元件,非必填
builder,// (BuildContext context, Widget child){} 這裡的第二個引數 child ,就是上面傳入的 child
// 這麼做的好處就是,動畫執行的時候只會執行 builder ,如果一個動畫只是包裹層需要執行動畫,這個時候就可以把包裹的子元件 放到外面傳進去
// 這樣就每次只需要 執行 builder 而方法第二個引數是傳遞進來的引用,所以可以避免每次都更新,減少開銷
)
交織動畫
官方是這麼介紹的:交織動畫是一個簡單的概念:視覺變化是隨著一系列的動作發生,而不是一次性的動作。動畫可能是純粹順序的,一個改變隨著一個改變發生,動畫也可能是部分或者全部重疊的。動畫也可能有間隙,沒有變化發生。
簡單點說就是一個動畫可以分割成很多片段,每個片段都有不同的Tween,看個使用示例
class StaggeredAnimationDemo extends StatefulWidget {
@override
_StaggeredAnimationDemoState createState() => _StaggeredAnimationDemoState();
}
class _StaggeredAnimationDemoState extends State<StaggeredAnimationDemo> with SingleTickerProviderStateMixin {
AnimationController _controller;
Animation<double> _height;
Animation<Color> _color;
Animation<double> _borderRadius;
@override
void initState() {
super.initState();
_controller = AnimationController(duration: Duration(milliseconds: 5000), vsync: this);
_height = Tween(begin: 50.0, end: 300.0).animate(
CurvedAnimation(
parent: _controller,
curve: Interval(0, 0.15), // Interval 範圍必須是0-1 指定Tween在哪一段時間執行
),
);
_color = ColorTween(begin: Colors.red, end: Colors.blue).animate(
CurvedAnimation(
parent: _controller,
curve: Interval(0.1, 0.2),
),
);
_borderRadius = Tween(begin: 10.0, end: 150.0).animate(
CurvedAnimation(
parent: _controller,
curve: Interval(0.1, 0.25),
),
);
_controller.forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BasiceAppLayout(
title: '交織動畫',
body: Center(
child: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Container(
height: _height.value,
width: _height.value,
decoration: BoxDecoration(
color: _color.value,
borderRadius: BorderRadius.circular(_borderRadius.value),
),
);
},
),
),
);
}
}
Hero動畫
Flutter叫它主動畫,用於不同頁面之間切換時候動畫,比如有一個商品列表,點選後跳到一個新的頁面檢視原圖,就可以這個動畫。使用也很簡單,在不同頁面使用Hero包裹需要動畫元件,兩個頁面的 tag 需要甚至成一直,但是同一個頁面需要保持唯一。
Hero(
tag: "avatar", //唯一標記,前後兩個路由頁Hero的tag必須相同
child: ChildWidget(),
)