一個App中如果能有優秀的動畫效果,能讓App看起來顯得更加高大上。此篇我們就來介紹一下Flutter中Animation體系。
-
我們先來一個簡單的例子,來實現透明度漸變動畫:
class FadeInDemo extends StatefulWidget { @override State createState() { return _FadeInDemoState(); } } class _FadeInDemoState extends State { double opacity = 0.0; @override Widget build(BuildContext context) { return Column( children: <Widget>[ MaterialButton( child: Text( "Click Me", style: TextStyle(color: Colors.blueAccent), ), onPressed: () => setState(() { opacity = 1; }), ), AnimatedOpacity( duration: const Duration(seconds: 2), opacity: opacity, child: Text('Flutter Animation Demo01') ) ], ); } }
這裏我們藉助於AnimatedOpacity來實現漸變:我們通過點擊按鈕來觸發動畫效果,如下圖所示。
我們來總結一下實現步驟:
- 使用AnimatedOpacity來包裹需要實現透明度漸變動畫的Widget,並指定duration和opacity參數。這倆參數也好理解:duration自然是動畫時間,opacity表示透明度(取值範圍爲0~1,0表示透明)
- 觸發動畫:通過setState()方法,我們可以直接指定opacity的最終值(爲1,即完全顯示)。因爲所謂的動畫,肯定是有起始狀態和結束狀態,然後在指定的動畫時間內慢慢發生變化。
2.使用AnimatedContainer來實現其他屬性變化的動畫.
class AnimatedContainerDemo extends StatefulWidget { @override State createState() { return _AnimatedContainerDemoState(); } } class _AnimatedContainerDemoState extends State<AnimatedContainerDemo> { Color color; double borderRadius; double margin; double randomBorderRadius() { return Random().nextDouble() * 64; } double randomMargin() { return Random().nextDouble() * 64; } Color randomColor() { return Color(0xFFFFFFFF & Random().nextInt(0xFFFFFFFF)); } void change() { setState(() { initState(); }); } @override void initState() { color = randomColor(); borderRadius = randomBorderRadius(); margin = randomMargin(); } @override Widget build(BuildContext context) { return Scaffold( body: Center( child: Column( children: <Widget>[ SizedBox( width: 128, height: 128, child: AnimatedContainer( curve: Curves.easeInOutBack, duration: const Duration(milliseconds: 400), margin: EdgeInsets.all(margin), decoration: BoxDecoration( color: color, borderRadius: BorderRadius.circular(borderRadius), ), ), ), MaterialButton( color: Theme.of(context).primaryColor, child: Text( 'change', style: TextStyle(color: Colors.white), ), onPressed: () => change(), ) ], ), ), ); } }
運行效果如下:
我們這次同時修改了margin、color以及borderRadius三個屬性。AnimatedContainer的使用思路和AnimatedOpacity類似:
- 包裹子widget,指定duration及相關屬性參數
- 在setState方法中指定屬性的動畫終止狀態
實際上我們剛剛介紹的兩種實現方式被稱之爲隱式動畫(implicit animation),可以理解成對於Animation子系統進行了一層封裝,方便我們開發者使用。下面我們正式來介紹Animation子系統的重要組成類:
3.Animation類:通過這個類,我們可以知道當前的動畫值(animation.value)以及當前的狀態(通過設置對應的監聽listener),但是對於屏幕上如何顯示、widget如何渲染,它是不知道的,換句話說也不是它所關心的,這點從軟件設計上耦合性也更低。其次,從代碼角度上,它是一個抽象類:
4.AnimationController類。
從類本身上看,它是繼承自Animation類的,並且泛型類型爲double。從作用上來看,我們可以通過AnimationController來指定動畫時長,以及它提供的forward()、reverse()方法來觸發動畫的執行。
5.CurvedAnimation類:同樣繼承自Animation類,並且泛型類型爲double。它主要用來描述非線性變化的動畫,有點類似Android中的屬性動畫的插值器。
6.Tween類.
從類的層次結構上,它有所不同,不再是繼承自Animation,而是繼承自Animatable。它主要用來指定起始狀態和終止狀態的。
好,我們已經對這四個類有了一定的瞭解,下面我們就從實例來看看他們是如何結合在一起使用的。
7.實例一:
class LogoDemo extends StatefulWidget {
@override
State createState() => _LogoState();
}
class _LogoState extends State with SingleTickerProviderStateMixin {
// 註釋1:這裏已經出現了我們前面提到的Animation和AnimationController類
Animation<double> animation;
AnimationController controller;
@override
void initState() {
super.initState();
// 註釋2:
// 在構造一個AnimationController對象時,我們需要傳遞兩個參數
// vsync:主要爲了防止一些超出屏幕之外的動畫而導致的不必要的資源消耗。我們這裏就傳遞
// this,除此之外,我們還需要使用with關鍵字來接入SingleTickerProviderStateMixin類型
// duration:指定動畫時長
controller =
AnimationController(vsync: this, duration: const Duration(seconds: 2));
// 註釋3: 通過Tween對象來指定起始值和終止值,並且通過animate方法返回一個Animation對象,
// 並且設置了監聽,最後在監聽回調中調用setState(),從而刷新頁面
animation = Tween<double>(begin: 0, end: 300).animate(controller)
..addListener(() {
setState(() {});
});
// 註釋4: 啓動動畫
controller.forward();
}
@override
Widget build(BuildContext context) {
return Center(
child: Container(
margin: EdgeInsets.symmetric(vertical: 10),
// 註釋5:通過Animation對象類獲取對應的變化值
height: animation.value,
width: animation.value,
child: FlutterLogo(),
),
);
}
@override
void dispose() {
// 註釋6:對controller解註冊
controller.dispose();
super.dispose();
}
}
運行效果如圖所示:
這裏涉及到了兩個Dart語言本身的知識點:mixin和..。mixin這裏推薦一篇medium上的文章:https://medium.com/flutter-community/dart-what-are-mixins-3a72344011f3;而..很簡單,它就是爲了方便鏈式調用。我們可以看下前面addListener()方法,方法本身返回的void類型,但是我們最終卻返回了一個Animation類型的對象。這其中就是..起到的作用,它可以使得方法返回調用這個方法的對象本身。
8.使用AnimatedWidget來重構上面的代碼:
class LogoDemo extends StatefulWidget {
@override
State createState() => _LogoState();
}
class _LogoState extends State with SingleTickerProviderStateMixin {
Animation<double> animation;
AnimationController controller;
@override
void initState() {
super.initState();
controller =
AnimationController(vsync: this, duration: const Duration(seconds: 2));
animation = Tween<double>(begin: 0, end: 300).animate(controller)
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
controller.reverse();
} else if (status == AnimationStatus.dismissed) {
controller.forward();
}
print('$status');
});
controller.forward();
}
@override
Widget build(BuildContext context) {
return AnimatedLogo(animation: animation);
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
}
class AnimatedLogo extends AnimatedWidget {
AnimatedLogo({Key key, Animation<double> animation})
: super(key: key, listenable: animation);
@override
Widget build(BuildContext context) {
final animation = listenable as Animation;
return Center(
child: Container(
margin: EdgeInsets.symmetric(vertical: 10),
height: animation.value,
width: animation.value,
child: FlutterLogo(),
),
);
}
}
先看效果:
大部分代碼是和之前的例子是一樣的,不同的是:
- 使用AnimatedWidget,並且Animation對象作爲參數傳遞進來
- 省略了在addListener的回調裏調用setState方法來觸發頁面刷新
這樣寫的好處:
- 省去了需要調用setState的重複代碼
- 使得程序耦合性更低。試想一下,我們的App中有多處都需要實現Logo的resize動畫,這個時候我們只需要在使用處定義Animation的描述,最後都使用這裏的AnimatedLogo。這樣做就使得Widget和Animation的描述進行分離。
我們可以去看一下AnimatedWidget類的源碼:
abstract class AnimatedWidget extends StatefulWidget {
/// Creates a widget that rebuilds when the given listenable changes.
///
/// The [listenable] argument is required.
const AnimatedWidget({
Key key,
@required this.listenable,
}) : assert(listenable != null),
super(key: key);
/// The [Listenable] to which this widget is listening.
///
/// Commonly an [Animation] or a [ChangeNotifier].
final Listenable listenable;
/// Override this method to build widgets that depend on the state of the
/// listenable (e.g., the current value of the animation).
@protected
Widget build(BuildContext context);
/// Subclasses typically do not override this method.
@override
_AnimatedState createState() => _AnimatedState();
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<Listenable>('animation', listenable));
}
}
class _AnimatedState extends State<AnimatedWidget> {
@override
void initState() {
super.initState();
widget.listenable.addListener(_handleChange);
}
@override
void didUpdateWidget(AnimatedWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.listenable != oldWidget.listenable) {
oldWidget.listenable.removeListener(_handleChange);
widget.listenable.addListener(_handleChange);
}
}
@override
void dispose() {
widget.listenable.removeListener(_handleChange);
super.dispose();
}
void _handleChange() {
setState(() {
// The listenable's state is our build state, and it changed already.
});
}
@override
Widget build(BuildContext context) => widget.build(context);
}
可以看到在initState方法裏,添加了動畫監聽,回調的執行邏輯爲_handleChange()方法,而_handleChange()的實現就是調用了setState方法,這點和我們之前在第7條中例子的寫法一樣。也就是說,AnimatedWidget只是做了一層封裝而已。
9.使用AnimatedBuilder進一步重構上面的代碼:
class LogoDemo extends StatefulWidget {
@override
State createState() => _LogoState();
}
class _LogoState extends State with SingleTickerProviderStateMixin {
Animation<double> animation;
AnimationController controller;
@override
void initState() {
super.initState();
controller =
AnimationController(vsync: this, duration: const Duration(seconds: 2));
animation = Tween<double>(begin: 0, end: 300).animate(controller);
controller.forward();
}
@override
Widget build(BuildContext context) {
return GrowTransition(child: LogoWidget(), animation: animation);
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
}
class LogoWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.symmetric(vertical: 10),
child: FlutterLogo(),
);
}
}
class GrowTransition extends StatelessWidget {
GrowTransition({this.child, this.animation});
final Widget child;
final Animation<double> animation;
@override
Widget build(BuildContext context) {
return Center(
child: AnimatedBuilder(
animation: animation,
builder: (context, child) => Container(
height: animation.value,
width: animation.value,
child: child,
),
child: child),
);
}
}
我們可以在任意地方使用這裏GrowTransition,代碼進行進一步分離。
10.使用Transition。
Flutter還爲我們提供一些封裝好的Transition,方便我們實現動畫效果,下面我們就以ScaleTransition爲例,說明如何去使用這些Transition。
class ScaleTransitionDemo extends StatefulWidget {
@override
State createState() => _LogoState();
}
class _LogoState extends State with SingleTickerProviderStateMixin {
Animation<double> animation;
AnimationController controller;
@override
void initState() {
super.initState();
controller =
AnimationController(vsync: this, duration: const Duration(seconds: 2));
animation = Tween<double>(begin: 0, end: 10).animate(controller);
controller.forward();
}
@override
Widget build(BuildContext context) {
return Center(
child: ScaleTransition(
scale: animation,
child: FlutterLogo(),
));
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
}
ScaleTransition在使用時需要指定兩個參數:
- scale: 就是一個Animation對象
- child: 需要實現縮放動畫的widget
最後再注意一下Tween中指定的值所表示的含義,它表示的倍數。比如我們這裏end填入了10,表示動畫結束狀態爲放大10倍。我們可以通過ScaleTransition的源碼來說服大家:
class ScaleTransition extends AnimatedWidget {
/// Creates a scale transition.
///
/// The [scale] argument must not be null. The [alignment] argument defaults
/// to [Alignment.center].
const ScaleTransition({
Key key,
@required Animation<double> scale,
this.alignment = Alignment.center,
this.child,
}) : assert(scale != null),
super(key: key, listenable: scale);
/// The animation that controls the scale of the child.
///
/// If the current value of the scale animation is v, the child will be
/// painted v times its normal size.
Animation<double> get scale => listenable;
/// The alignment of the origin of the coordinate system in which the scale
/// takes place, relative to the size of the box.
///
/// For example, to set the origin of the scale to bottom middle, you can use
/// an alignment of (0.0, 1.0).
final Alignment alignment;
/// The widget below this widget in the tree.
///
/// {@macro flutter.widgets.child}
final Widget child;
@override
Widget build(BuildContext context) {
final double scaleValue = scale.value;
final Matrix4 transform = Matrix4.identity()
..scale(scaleValue, scaleValue, 1.0);
return Transform(
transform: transform,
alignment: alignment,
child: child,
);
}
}
可以看到,ScaleTransition也是繼承自我們前面已經介紹過的AnimatedWidget,然後重點關注build()方法裏,用到了Matrix4矩陣,這裏的scale.value實際上就是Animation.value,而Matrix4.identity()..scale(),它的三個參數分別表示在x軸、y軸以及z軸上縮放的倍數。
與ScaleTransition類似的還有SlideTransition、RotationTransition等等,讀者可以自己去嘗試一下,這裏就不在一一贅述了。