交織動畫
交織動畫是一個簡單的概念:視覺變化是隨著一系列的動作發生,而不是一次性的動作。動畫可能是純粹順序的,一個改變隨著一個改變發生,動畫也可能是部分或者全部重疊的。動畫也可能有間隙,沒有變化發生。
本指南展示如何在Flutter中建構交織動畫。
以下影片示範了 basic_staggered_animation 所執行的動畫:
在這個影片中,你可以看到一個獨立的 widget 的以下動畫,以一個帶邊框的略微有圓角的藍色矩形開始,這個矩形會按照以下順序變化:
-
淡出
-
擴大
-
向上移動同時變得更高
-
變為一個有邊框的圓圈
-
顏色變為橙色
向前執行之後,動畫將反向執行。
一個交織動畫的基礎結構
下圖展示了在 basic_staggered_animation 使用間隔的例子。你會注意到有以下特點:
-
透明度在時間軸的前 10% 發生變化。
-
透明度的變化和寬度的變化之間有一個很小的間隔。
-
在時間軸的最後 25% 沒有動畫。
-
增加填充使 widget 看起來向上上升。
-
將圓角半徑增加到 0.5,將圓角正方形變成一個圓。
-
填充和高度的變化發生在相同的時間間隔內,但它們不必這麼做。
設定這個動畫:
-
建立一個
AnimationController
管理所有的Animations
。 -
為每一個有動畫的屬性建立一個 Tween
-
Tween 定義一個值的範圍。
-
Tween 的
animate
方法需要parent
控制器。同時產生一個動畫為這個屬性。
-
-
指定動畫的 “curve” 屬性的間隔
當控制動畫的值發生變化時,新動畫的值也隨之變化值更改,觸發 UI 更新。
下面的程式碼為 width
屬性建立了一個 tween。
它建立了一個 CurvedAnimation
, 指定一個 eased curve。其他更多的預定的動畫曲線請看 Curves
。
width = Tween<double>( begin: 50.0, end: 150.0, ).animate( CurvedAnimation( parent: controller, curve: Interval( 0.125, 0.250, curve: Curves.ease, ), ), ),
begin
和 end
的值不一定是 doubles。
下面的程式碼為 borderRadius
屬性建立一個 tween(控制矩形的圓角半徑),使用 BorderRadius.circular()
。
borderRadius = BorderRadiusTween( begin: BorderRadius.circular(4.0), end: BorderRadius.circular(75.0), ).animate( CurvedAnimation( parent: controller, curve: Interval( 0.375, 0.500, curve: Curves.ease, ), ), ),
完整的交織動畫
像所有可互動的 widgets 一樣,完整的動畫包括一對 widget:一個無狀態 widget 和一個有狀態的 widget。
無狀態 widget 指定 Tweens,定義動畫物件,提供一個 build()
方法,負責建構 widget 樹的動畫部分。
有狀態 widget 建立控制器,播放動畫,同時建構 widget 樹的非動畫部分。當在螢幕上檢測到一個點選時,動畫開始。
Full code for basic_staggered_animation’s main.dart
無狀態的 widget: StaggerAnimation
在無狀態 widget 中,StaggerAnimation
,the build()
函式例項化了一個
AnimatedBuilder
—一個用於建構動畫的通用 widget。
AnimatedBuilder
建構一個 widget 並使用 Tweens 的當前值配置它。這個例子建立一個名為 _buildAnimation()
(實際更新 UI)的方法,並將其分配給其 builder
屬性。AnimatedBuilder
監聽來自動畫控制器的通知,當值發生更改時,將 widget 樹標記為 dirty。對於動畫的每一個標記,值都會更新,導致呼叫 _buildAnimation()
。
class StaggerAnimation extends StatelessWidget { StaggerAnimation({ Key key, this.controller }) : // Each animation defined here transforms its value during the subset // of the controller's duration defined by the animation's interval. // For example the opacity animation transforms its value during // the first 10% of the controller's duration. opacity = Tween<double>( begin: 0.0, end: 1.0, ).animate( CurvedAnimation( parent: controller, curve: Interval( 0.0, 0.100, curve: Curves.ease, ), ), ), // ... Other tween definitions ... super(key: key); final AnimationController controller; final Animation<double> opacity; final Animation<double> width; final Animation<double> height; final Animation<EdgeInsets> padding; final Animation<BorderRadius> borderRadius; final Animation<Color> color; // This function is called each time the controller "ticks" a new frame. // When it runs, all of the animation's values will have been // updated to reflect the controller's current value. Widget _buildAnimation(BuildContext context, Widget child) { return Container( padding: padding.value, alignment: Alignment.bottomCenter, child: Opacity( opacity: opacity.value, child: Container( width: width.value, height: height.value, decoration: BoxDecoration( color: color.value, border: Border.all( color: Colors.indigo[300], width: 3.0, ), borderRadius: borderRadius.value, ), ), ), ); } @override Widget build(BuildContext context) { return AnimatedBuilder( builder: _buildAnimation, animation: controller, ); } }
有狀態的 widget: StaggerDemo
有狀態的 widget, StaggerDemo,建立 AnimationController(控制所有動畫的控制器),設定一個 2000 毫秒的週期。控制器播放一個動畫,然後在 widget 樹上建立一個無動畫的部分。當在螢幕上檢測到一個點選時,動畫開始。動畫向前執行,然後向後執行。
class StaggerDemo extends StatefulWidget { @override _StaggerDemoState createState() => _StaggerDemoState(); } class _StaggerDemoState extends State<StaggerDemo> with TickerProviderStateMixin { AnimationController _controller; @override void initState() { super.initState(); _controller = AnimationController( duration: const Duration(milliseconds: 2000), vsync: this ); } // ...Boilerplate... Future<void> _playAnimation() async { try { await _controller.forward().orCancel; await _controller.reverse().orCancel; } on TickerCanceled { // the animation got canceled, probably because it was disposed of } } @override Widget build(BuildContext context) { timeDilation = 10.0; // 1.0 is normal animation speed. return Scaffold( appBar: AppBar( title: const Text('Staggered Animation'), ), body: GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { _playAnimation(); }, child: Center( child: Container( width: 300.0, height: 300.0, decoration: BoxDecoration( color: Colors.black.withOpacity(0.1), border: Border.all( color: Colors.black.withOpacity(0.5), ), ), child: StaggerAnimation( controller: _controller.view ), ), ), ), ); } }