前言
動畫本質是在一段時間內不斷改變螢幕上顯示的內容,從而產生視覺暫留現象。
動畫一般可分為兩類:
「補間動畫」:補間動畫是一種預先定義物體運動的起點和終點,物體的運動方式,運動時間,時間曲線,然後從起點過渡到終點的動畫。
「基於物理的動畫」:基於物理的動畫是一種模擬現實世界運動的動畫,通過建立運動模型來實現。例如一個籃球?從高處落下,需要根據其下落高度,重力加速度,地面反彈力等影響因素來建立運動模型。
Flutter 中的動畫
Flutter 中有多種型別的動畫,先從一個簡單的例子開始,使用一個 AnimatedContainer
控制元件,然後設定動畫時長 duration
,最後呼叫 setState
方法改變需要變化的屬性值,一個動畫就建立了。
程式碼如下
import 'package:flutter/material.dart';
class AnimatedContainerPage extends StatefulWidget {
@override
_AnimatedContainerPageState createState() => _AnimatedContainerPageState();
}
class _AnimatedContainerPageState extends State<AnimatedContainerPage> {
// 初始的屬性值
double size = 100;
double raidus = 25;
Color color = Colors.yellow;
void _animate() {
// 改變屬性值
setState(() {
size = size == 100 ? 200 : 100;
raidus = raidus == 25 ? 100 : 25;
color = color == Colors.yellow ? Colors.greenAccent : Colors.yellow;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Animated Container')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 在 AnimatedContainer 上應用屬性值
AnimatedContainer(
width: size,
height: size,
curve: Curves.easeIn,
padding: const EdgeInsets.all(20.0),
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(raidus),
),
duration: Duration(seconds: 1),
child: FlutterLogo(),
)
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _animate,
child: Icon(Icons.refresh),
),
);
}
}
複製程式碼
這是一個隱式動畫,除此之外還有顯式動畫,Hreo 動畫,交織動畫。
基礎概念
Flutter 動畫是建立在以下的概念之上。
Animation
Flutter 中的動畫系統基於 Animation
物件, 它是一個抽象類,儲存了當前動畫的值和狀態(開始、暫停、前進、倒退),但不記錄螢幕上顯示的內容。UI 元素通過讀取 Animation
物件的值和監聽狀態變化執行 build
函式,然後渲染到螢幕上形成動畫效果。
一個 Animation
物件在一段時間內會持續生成介於兩個值之間的值,比較常見的型別是 Animation<double>
,除 double
型別之外還有 Animation<Color>
或者 Animation<Size>
等。
abstract class Animation<T> extends Listenable implements ValueListenable<T> {
/// ...
}
複製程式碼
AnimationController
帶有控制方法的 Animation
物件,用來控制動畫的啟動,暫停,結束,設定動畫執行時間等。
class AnimationController extends Animation<double>
with AnimationEagerListenerMixin, AnimationLocalListenersMixin, AnimationLocalStatusListenersMixin {
/// ...
}
AnimationController controller = AnimationController(
vsync: this,
duration: Duration(seconds: 10),
);
複製程式碼
Tween
用來生成不同型別和範圍的動畫取值。
class Tween<T extends dynamic> extends Animatable<T> {
Tween({ this.begin, this.end });
/// ...
}
// double 型別
Tween<double> tween = Tween<double>(begin: -200, end: 200);
// color 型別
ColorTween colorTween = ColorTween(begin: Colors.blue, end: Colors.yellow);
// border radius 型別
BorderRadiusTween radiusTween = BorderRadiusTween(
begin: BorderRadius.circular(0.0),
end: BorderRadius.circular(150.0),
);
複製程式碼
Curve
Flutter 動畫的預設動畫過程是勻速的,使用 CurvedAnimation
可以將時間曲線定義為非線性曲線。
class CurvedAnimation extends Animation<double> with AnimationWithParentMixin<double> {
/// ...
}
Animation animation = CurvedAnimation(parent: controller, curve: Curves.easeIn);
複製程式碼
Ticker
Ticker
用來新增每次螢幕重新整理的回撥函式 TickerCallback
,每次螢幕重新整理都會呼叫。類似於 Web 裡面的 requestAnimationFrame
方法。
class Ticker {
/// ...
}
Ticker ticker = Ticker(callback);
複製程式碼
隱式動畫
隱式動畫使用 Flutter 框架內建的動畫部件建立,通過設定動畫的起始值和最終值來觸發。當使用 setState
方法改變部件的動畫屬性值時,框架會自動計算出一個從舊值過渡到新值的動畫。
比如 AnimatedOpacity
部件,改變它的 opacity
值就可以觸發動畫。
import 'package:flutter/material.dart';
class OpacityChangePage extends StatefulWidget {
@override
_OpacityChangePageState createState() => _OpacityChangePageState();
}
class _OpacityChangePageState extends State<OpacityChangePage> {
double _opacity = 1.0;
// 改變目標值
void _toggle() {
_opacity = _opacity > 0 ? 0.0 : 1.0;
setState(() {});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('隱式動畫')),
body: Center(
child: AnimatedOpacity(
// 傳入目標值
opacity: _opacity,
duration: Duration(seconds: 1),
child: Container(
width: 200,
height: 200,
color: Colors.blue,
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: _toggle,
child: Icon(Icons.play_arrow),
),
);
}
}
複製程式碼
除了 AnimatedOpacity
外,還有其他的內建隱式動畫部件如:AnimatedContainer
, AnimatedPadding
, AnimatedPositioned
, AnimatedSwitcher
, AnimatedAlign
等。
顯式動畫
顯式動畫指的是需要手動設定動畫的時間,運動曲線,取值範圍的動畫。將值傳遞給動畫部件如: RotationTransition
,最後使用一個AnimationController
控制動畫的開始和結束。
import 'dart:math';
import 'package:flutter/material.dart';
class RotationAinmationPage extends StatefulWidget {
@override
_RotationAinmationPageState createState() => _RotationAinmationPageState();
}
class _RotationAinmationPageState extends State<RotationAinmationPage>
with SingleTickerProviderStateMixin {
AnimationController _controller;
Animation<double> _turns;
bool _playing = false;
// 控制動畫執行狀態
void _toggle() {
if (_playing) {
_playing = false;
_controller.stop();
} else {
_controller.forward()..whenComplete(() => _controller.reverse());
_playing = true;
}
setState(() {});
}
@override
void initState() {
super.initState();
// 初始化動畫控制器,設定動畫時間
_controller = AnimationController(
vsync: this,
duration: Duration(seconds: 10),
);
// 設定動畫取值範圍和時間曲線
_turns = Tween(begin: 0.0, end: pi * 2).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeIn),
);
}
@override
void dispose() {
super.dispose();
_controller.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('顯示動畫')),
body: Center(
child: RotationTransition(
// 傳入動畫值
turns: _turns,
child: Container(
width: 200,
height: 200,
child: Image.asset(
'assets/images/fan.png',
fit: BoxFit.cover,
),
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: _toggle,
child: Icon(_playing ? Icons.pause : Icons.play_arrow),
),
);
}
}
複製程式碼
除了 RotationTransition
外,還有其他的顯示動畫部件如:FadeTransition
, ScaleTransition
, SizeTransition
, SlideTransition
等。
Hero 動畫
Hero 動畫指的是在頁面切換時一個元素從舊頁面運動到新頁面的動畫。Hero 動畫需要使用兩個 Hero
控制元件實現:一個用來在舊頁面中,另一個在新頁面。兩個 Hero
控制元件需要使用相同的 tag
屬性,並且不能與其他tag
重複。
// 頁面 1
import 'package:flutter/material.dart';
import 'hero_animation_page2.dart';
String cake1 = 'assets/images/cake01.jpg';
String cake2 = 'assets/images/cake02.jpg';
class HeroAnimationPage1 extends StatelessWidget {
GestureDetector buildRowItem(context, String image) {
return GestureDetector(
onTap: () {
// 跳轉到頁面 2
Navigator.of(context).push(
MaterialPageRoute(builder: (ctx) {
return HeroAnimationPage2(image: image);
}),
);
},
child: Container(
width: 100,
height: 100,
child: Hero(
// 設定 Hero 的 tag 屬性
tag: image,
child: ClipOval(child: Image.asset(image)),
),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('頁面 1')),
body: Column(
children: <Widget>[
SizedBox(height: 40.0),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
buildRowItem(context, cake1),
buildRowItem(context, cake2),
],
),
],
),
);
}
}
// 頁面 2
import 'package:flutter/material.dart';
class HeroAnimationPage2 extends StatelessWidget {
final String image;
const HeroAnimationPage2({@required this.image});
@override
Widget build(BuildContext context) {
return Scaffold(
body: CustomScrollView(
slivers: <Widget>[
SliverAppBar(
expandedHeight: 400.0,
title: Text('頁面 2'),
backgroundColor: Colors.grey[200],
flexibleSpace: FlexibleSpaceBar(
collapseMode: CollapseMode.parallax,
background: Hero(
// 使用從頁面 1 傳入的 tag 值
tag: image,
child: Container(
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage(image),
fit: BoxFit.cover,
),
),
),
),
),
),
SliverList(
delegate: SliverChildListDelegate(
<Widget>[
Container(height: 600.0, color: Colors.grey[200]),
],
),
),
],
),
);
}
}
複製程式碼
交織動畫
交織動畫是由一系列的小動畫組成的動畫。每個小動畫可以是連續或間斷的,也可以相互重疊。其關鍵點在於使用 Interval
部件給每個小動畫設定一個時間間隔,以及為每個動畫的設定一個取值範圍 Tween
,最後使用一個 AnimationController
控制總體的動畫狀態。
Interval
繼承至 Curve
類,通過設定屬性 begin
和 end
來確定這個小動畫的執行範圍。
class Interval extends Curve {
/// 動畫起始點
final double begin;
/// 動畫結束點
final double end;
/// 動畫緩動曲線
final Curve curve;
/// ...
}
複製程式碼
這是一個由 5 個小動畫組成的交織動畫,寬度,高度,顏色,圓角,邊框,每個動畫都有自己的動畫區間。
import 'package:flutter/material.dart';
class StaggeredAnimationPage extends StatefulWidget {
@override
_StaggeredAnimationPageState createState() => _StaggeredAnimationPageState();
}
class _StaggeredAnimationPageState extends State<StaggeredAnimationPage>
with SingleTickerProviderStateMixin {
AnimationController _controller;
Animation<double> _width;
Animation<double> _height;
Animation<Color> _color;
Animation<double> _border;
Animation<BorderRadius> _borderRadius;
void _play() {
if (_controller.isCompleted) {
_controller.reverse();
} else {
_controller.forward();
}
}
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: Duration(seconds: 5),
);
_width = Tween<double>(
begin: 100,
end: 300,
).animate(
CurvedAnimation(
parent: _controller,
curve: Interval(
0.0,
0.2,
curve: Curves.ease,
),
),
);
_height = Tween<double>(
begin: 100,
end: 300,
).animate(
CurvedAnimation(
parent: _controller,
curve: Interval(
0.2,
0.4,
curve: Curves.ease,
),
),
);
_color = ColorTween(
begin: Colors.blue,
end: Colors.yellow,
).animate(
CurvedAnimation(
parent: _controller,
curve: Interval(
0.4,
0.6,
curve: Curves.ease,
),
),
);
_borderRadius = BorderRadiusTween(
begin: BorderRadius.circular(0.0),
end: BorderRadius.circular(150.0),
).animate(
CurvedAnimation(
parent: _controller,
curve: Interval(
0.6,
0.8,
curve: Curves.ease,
),
),
);
_border = Tween<double>(
begin: 0,
end: 25,
).animate(
CurvedAnimation(
parent: _controller,
curve: Interval(0.8, 1.0),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('交織動畫')),
body: Center(
child: AnimatedBuilder(
animation: _controller,
builder: (BuildContext context, Widget child) {
return Container(
width: _width.value,
height: _height.value,
decoration: BoxDecoration(
color: _color.value,
borderRadius: _borderRadius.value,
border: Border.all(
width: _border.value,
color: Colors.orange,
),
),
);
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: _play,
child: Icon(Icons.refresh),
),
);
}
}
複製程式碼
物理動畫
物理動畫是一種模擬現實世界物體運動的動畫。需要建立物體的運動模型,以一個物體下落為例,這個運動受到物體的下落高度,重力加速度,地面的反作用力等因素的影響。
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
class ThrowAnimationPage extends StatefulWidget {
@override
_ThrowAnimationPageState createState() => _ThrowAnimationPageState();
}
class _ThrowAnimationPageState extends State<ThrowAnimationPage> {
// 球心高度
double y = 70.0;
// Y 軸速度
double vy = -10.0;
// 重力
double gravity = 0.1;
// 地面反彈力
double bounce = -0.5;
// 球的半徑
double radius = 50.0;
// 地面高度
final double height = 700;
// 下落方法
void _fall(_) {
y += vy;
vy += gravity;
// 如果球體接觸到地面,根據地面反彈力改變球體的 Y 軸速度
if (y + radius > height) {
y = height - radius;
vy *= bounce;
} else if (y - radius < 0) {
y = 0 + radius;
vy *= bounce;
}
setState(() {});
}
@override
void initState() {
super.initState();
// 使用一個 Ticker 在每次更新介面時執行球體下落方法
Ticker(_fall)..start();
}
@override
Widget build(BuildContext context) {
double screenWidth = MediaQuery.of(context).size.width;
return Scaffold(
appBar: AppBar(title: Text('物理動畫')),
body: Column(
children: <Widget>[
Container(
height: height,
child: Stack(
children: <Widget>[
Positioned(
top: y - radius,
left: screenWidth / 2 - radius,
child: Container(
width: radius * 2,
height: radius * 2,
decoration: BoxDecoration(
color: Colors.blue,
shape: BoxShape.circle,
),
),
),
],
),
),
Expanded(child: Container(color: Colors.blue)),
],
),
);
}
}
複製程式碼
總結
本文介紹了 Flutter 中多種型別的動畫,分別是
隱式動畫 顯式動畫 Hero 動畫 交織動畫 基於物理的動畫
Flutter 動畫基於型別化的 Animation
物件,Widgets
通過讀取動畫物件的當前值和監聽狀態變化重新執行 build
函式,不斷變化 UI 形成動畫效果。
一個動畫的主要因素有
Animation
動畫物件AnimationController
動畫控制器Tween
動畫取值範圍Curve
動畫運動曲線
參考
Flutter animation basics with implicit animations
Directional animations with built-in explicit animations
本文使用 mdnice 排版