動畫本質是在一段時間內不斷改變屏幕上顯示的內容,從而產生視覺暫留現象。html
動畫通常可分爲兩類:web
「補間動畫」:補間動畫是一種預先定義物體運動的起點和終點,物體的運動方式,運動時間,時間曲線,而後從起點過渡到終點的動畫。markdown
「基於物理的動畫」:基於物理的動畫是一種模擬現實世界運動的動畫,經過創建運動模型來實現。例如一個籃球🏀從高處落下,須要根據其下落高度,重力加速度,地面反彈力等影響因素來創建運動模型。app
Flutter 中有多種類型的動畫,先從一個簡單的例子開始,使用一個 AnimatedContainer
控件,而後設置動畫時長 duration
,最後調用 setState
方法改變須要變化的屬性值,一個動畫就建立了。框架
代碼以下less
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 動畫是創建在如下的概念之上。ide
Animation
函數
Flutter 中的動畫系統基於 Animation
對象, 它是一個抽象類,保存了當前動畫的值和狀態(開始、暫停、前進、倒退),但不記錄屏幕上顯示的內容。UI 元素經過讀取 Animation
對象的值和監聽狀態變化運行 build
函數,而後渲染到屏幕上造成動畫效果。flex
一個 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
控件須要使用相同的 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 中多種類型的動畫,分別是
Flutter 動畫基於類型化的 Animation
對象,Widgets
經過讀取動畫對象的當前值和監聽狀態變化從新運行 build
函數,不斷變化 UI 造成動畫效果。
一個動畫的主要因素有
Animation
動畫對象
AnimationController
動畫控制器
Tween
動畫取值範圍
Curve
動畫運動曲線
Flutter animation basics with implicit animations
Directional animations with built-in explicit animations
本文使用 mdnice 排版