交錯動畫
如何在 Flutter 中編寫交錯動畫。
交錯動畫的概念非常直觀:視覺變化以一系列操作的形式發生,而不是一次性全部完成。動畫可以是純粹的連續動畫(一個接一個地變化),也可以是部分或完全重疊的。它還可以包含間隙,即不發生任何變化的階段。
本指南介紹瞭如何在 Flutter 中構建交錯動畫。
以下影片演示了 basic_staggered_animation 執行的動畫效果。
在影片中,您可以看到單個元件的以下動畫效果:它最初是一個帶有圓角邊框的藍色正方形。該正方形按以下順序進行變換:
- 淡入
- 變寬
- 變高並向上移動
- 變形為帶邊框的圓形
- 顏色變為橙色
動畫向前執行結束後,會反向執行。
交錯動畫的基本結構
#下圖顯示了 basic_staggered_animation 示例中使用的 Interval。您可能會注意到以下特徵:
- 不透明度在時間軸的前 10% 期間發生變化。
- 不透明度的變化和寬度的變化之間存在一個微小的間隙。
- 在時間軸的最後 25% 期間,沒有任何動畫發生。
- 增加內邊距 (padding) 會使元件看起來向上升起。
- 將邊框半徑 (border radius) 增加到 0.5,可以將帶有圓角的正方形變為圓形。
- 內邊距和高度的變化發生在完全相同的時間間隔內,但這並非必須如此。
設定動畫步驟
- 建立一個
AnimationController來管理所有的Animations。 - 為每個要進行動畫的屬性建立一個
Tween。Tween定義了一個值的範圍。Tween的animate方法需要傳入parent控制器,併為該屬性生成一個Animation物件。
- 在
Animation的curve屬性上指定時間間隔 (Interval)。
當控制動畫的值發生變化時,新動畫的值也會隨之改變,從而觸發 UI 更新。
以下程式碼為 width 屬性建立了一個 Tween。它構建了一個 CurvedAnimation,指定了一條緩動曲線。請參閱 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 的值不一定是 double 型別。以下程式碼構建了 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,
),
),
),
完整的交錯動畫
#與所有互動式元件一樣,完整的動畫由一對元件組成:一個無狀態元件 (StatelessWidget) 和一個有狀態元件 (StatefulWidget)。
無狀態元件指定 Tween,定義 Animation 物件,並提供一個負責構建元件樹動畫部分的 build() 函式。
有狀態元件建立控制器,播放動畫,並構建元件樹的非動畫部分。當螢幕上的任何位置檢測到點選時,動畫就會開始。
basic_staggered_animation 的 main.dart 完整程式碼
無狀態元件:StaggerAnimation
#在無狀態元件 StaggerAnimation 中,build() 函式例項化了一個 AnimatedBuilder——這是一個用於構建動畫的通用元件。AnimatedBuilder 構建元件,並使用 Tween 的當前值對其進行配置。該示例建立了一個名為 _buildAnimation() 的函式(執行實際的 UI 更新),並將其分配給 builder 屬性。AnimatedBuilder 會監聽來自動畫控制器的通知,並在值發生變化時標記元件樹為髒 (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,
);
}
}
有狀態元件:StaggerDemo
#有狀態元件 StaggerDemo 建立了 AnimationController(統管一切),並指定了 2000 毫秒的持續時間。它播放動畫,並構建元件樹中非動畫的部分。當螢幕上檢測到點選時,動畫就會開始。動畫會向前執行,然後再反向執行。
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.withValues(alpha: 0.1),
border: Border.all(
color: Colors.black.withValues(alpha: 0.5),
),
),
child: StaggerAnimation(controller:_controller.view),
),
),
),
);
}
}