交織動畫
交織動畫是一個簡單的概念:視覺變化是隨著一系列的動作發生,而不是一次性的動作。動畫可能是純粹順序的,一個改變隨著一個改變發生,動畫也可能是部分或者全部重疊的。動畫也可能有間隙,沒有變化發生。
本指南展示如何在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: const Interval(
0.125,
0.250,
curve: Curves.ease,
),
),
),
begin
和 end
的值不一定是 doubles。
下面的程式碼為 borderRadius
屬性建立一個 tween(控制矩形的圓角半徑),使用 BorderRadius.circular()
。
borderRadius = BorderRadiusTween(
begin: BorderRadius.circular(4),
end: BorderRadius.circular(75),
).animate(
CurvedAnimation(
parent: controller,
curve: const 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({super.key, required 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: const Interval( 0.0, 0.100, curve: Curves.ease, ), ), ), // ... Other tween definitions ... ); 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, ), 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 State<StaggerDemo> createState() => _StaggerDemoState(); } class _StaggerDemoState extends State<StaggerDemo> with TickerProviderStateMixin { late 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, height: 300, decoration: BoxDecoration( color: Colors.black.withOpacity(0.1), border: Border.all( color: Colors.black.withOpacity(0.5), ), ), child: StaggerAnimation(controller:_controller.view), ), ), ), ); } }