動畫從原理上可以分爲兩類:補間動畫和基於物理動畫。
補間動畫顧名思義就是介於兩點之間,兩點也就是起點和終點。在補間動畫中,定義了起點和終點以及時間軸,再定義過渡時間和速度的曲線。然後框架會計算如何從起點過渡到終點。
物理動畫是基於對真實世界的行爲模擬來進行建模的。像乒乓球的落地和彈起等,
在flutter中,動畫又被區分隱式動畫、顯式動畫、hexo動畫、交織動畫,物理動畫等。下面詳細解釋。
隱式動畫的使用
先看效果:
實現一個盒子縮放,點擊按鈕放大:
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("隱式動畫"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
RaisedButton(
onPressed: () {
_updateState();
},
child: Text('Animate'),
),
Container(
width: _bigger ? 400 : 100,
height: _bigger ? 400 : 100,
color: Colors.lightBlue[200],
child: Center(
child: Text(
'Animatiaon',
style: Theme.of(context).textTheme.subtitle1,
),
),
),
],
),
),
);
}
沒有任何動畫,畫面突兀生硬,下面我們用隱式動畫實現一個柔和的效果:
- 把Container替換爲AnimatedContainer;
- 設置動畫時長爲400毫秒;
- 設置動畫曲線;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("隱式動畫"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
RaisedButton(
onPressed: () {
_updateState();
},
child: Text('Animate'),
),
AnimatedContainer(
duration: Duration(
milliseconds: 400,
),
width: _bigger ? 400 : 100,
height: _bigger ? 400 : 100,
curve: Curves.bounceOut,
color: Colors.lightBlue[200],
child: Center(
child: Text(
'Animatiaon',
style: Theme.of(context).textTheme.subtitle1,
),
),
),
],
),
),
);
}
簡單的3步,就實現了一個縮放動畫。能這麼簡單,是因爲flutter幫我媽實現了動畫細節。查看AnimatedContainer,可以看到它繼承自ImplicitlyAnimatedWidget:
class AnimatedContainer extends ImplicitlyAnimatedWidget
ImplicitlyAnimatedWidgets:AnimatedContainer是Flutter的動畫庫爲我們實現的管理動畫的小部件。這些小部件統稱爲隱式動畫或隱式動畫小部件,它們的名稱來自於ImplicitlyAnimatedWidget,也就是它們實現的父類,下面列舉下常用的小部件:
ALign->AnimatedAlign
Container->AnimatedContainer
DefaultTextStyle->AnimatedDefaultTextStyle
Opacity->AnimatedOpacity
Padding->AnimatedPadding
PhysicalModel->AnimatedPhysicalModel
Positioned->AnimatedPositioned
PositionedDirectional->AnimatedPositionedDirectional
Theme->AnimatedThemeSize->AnimatedSize
這些小部件在首次添加到widget樹時將不進行動畫處理,也就是我們進入頁面的時候,是沒有動畫的。但是當我們更改其屬性時,它們將通過對指定持續時間內的變化自動進行動畫處理來響應這些變化。怎麼實現自動呢,是因爲ImplicitlyAnimatedWidgetState在內部創建並管理AnimationController來爲動畫提供動力。
當然實現起來簡單也就意味着動畫效果簡單,ImplicitlyAnimatedWidgets及其子類受到一些限制:除了動畫屬性之外,開發人員只能爲動畫選擇持續時間和曲線。如果需要對動畫進行更多控制(例如,將其停在中間的某個位置),ImplicitlyAnimatedWidgets並不能辦到,這時候我們就需要使用顯式動畫。
Tween動畫的使用
上面瞭解了基本的隱式動畫,但是一些widget沒有的屬性,比如顏色變化等,我們就需要用Tween動畫實現,它相當於簡單自定義的隱式動畫。
下面用一個案例實現P圖軟件的調色濾鏡效果,給我的女朋友調個色。我相信你學會這一招,一定能討得女朋友歡心,前提是你先有個女朋友。
@override
Widget build(BuildContext context) {
// TODO: implement build
return Scaffold(
body: Stack(
children: <Widget>[
Image.asset(
R.bg,
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
fit: BoxFit.fitWidth,
),
Column(
children: <Widget>[
Center(
child: Image.asset(
R.lihuili,
),
),
Slider.adaptive(
value: _sliderValue,
onChanged: (double value) {
setState(() {
_sliderValue = value;
_newColor =
Color.lerp(Colors.white, Colors.blue, _sliderValue);
});
})
],
),
],
),
);
}
先實現佈局,然後加入TweenAnimationBuilder:
Center(
child: TweenAnimationBuilder(
tween: ColorTween(begin: Colors.white,end: Colors.green),
duration: Duration(milliseconds: 300),
child: Image.asset(
R.lihuili,
),
),
)
因爲是濾鏡,所以使用ColorTween實現。然後把end顏色改爲拖動手柄產生的值:
tween: ColorTween(begin: Colors.white, end: _newColor),
給圖片加上顏色過濾:
ColorFiltered(
child: Image.asset(
R.lihuili,
),
colorFilter: ColorFilter.mode(color, BlendMode.modulate),
);
見證奇蹟的時刻來了:
沒幾行代碼,就實現了一個濾鏡效果。嗯,加雞腿。。。
Animation
瞭解了一些動畫,下面介紹下動畫的核心類:
Animation,Flutter 動畫庫中的核心類,插入用於指導動畫的值。
Animation 對象知道動畫目前的狀態(例如,是否開始,暫停,前進或倒退),但是對屏幕上顯示的內容一無所知。AnimationController 管理 Animation。
AnimationController 是個特殊的 Animation 對象,每當硬件準備新幀時,他都會生成一個新值。默認情況下,AnimationController 在給定期間內會線性生成從 0.0 到 1.0 的數字。CurvedAnimation 定義動畫在開始值和結束值之間如何變化的路徑或曲線。
Duration 動畫花費的時間。
Tween 爲動畫對象插入一個範圍值。例如,Tween 可以定義插入值由紅到藍,或從 0 到 255。
在默認情況下,AnimationController 對象的範圍是 0.0-0.1。如果需要不同的範圍或者不同的數據類型,可以使用 Tween 配置動畫來插入不同的範圍或數據類型。Tween.animate,要使用 Tween 對象,需要 Tween 調用 animate(),傳入控制器對象。
使用 Listeners 和 StatusListeners 監視動畫狀態變化。
一個 Animation 對象可以有不止一個 Listener 和 StatusListener,用 addListener() 和 addStatusListener() 來定義。當動畫值改變時調用 Listener。Listener 最常用的操作是調用 setState() 進行重建。當一個動畫開始,結束,前進或後退時,會調用 StatusListener,用 AnimationStatus 來定義。Ticker 動畫定時器。
AnimationController 的vsync對象會綁定一個ticker,當widget不顯示時,動畫定時器將會暫停,當widget再次顯示時,動畫定時器重新恢復執行,這樣就可以避免動畫相關UI不在前臺顯示時依然運行消耗資源。 如果要使用自定義的State對象作爲vsync時,混入TickerProviderStateMixin。就不需要我們自己釋放資源了。
顯式動畫
上面簡單的動畫不滿足我們的時候,就需要自己控制動畫了。
flutter 爲我們提供的switch 不能改變大小,滿足不了我們的需要,下面我們自己實現一個,首先分析都需要哪些屬性:寬高、打開的顏色、關閉的顏色、按鈕的顏色、打開關閉的事件。
class CustomSwitch extends StatefulWidget {
CustomSwitch({
Key key,
this.width = 120,
this.height = 50,
this.activeColor = Colors.blue,
this.inactiveColor = Colors.grey,
this.buttonColor = Colors.white,
this.onChanged,
this.value = false,
}) : super(key: key);
final double width;
final double height;
/// 打開時的顏色
final Color activeColor;
/// 關閉時的顏色
final Color inactiveColor;
/// 按鈕顏色
final Color buttonColor;
final ValueChanged<bool> onChanged;
final bool value;
@override
_CustomSwitchState createState() {
return _CustomSwitchState();
}
}
class _CustomSwitchState extends State<CustomSwitch> {
bool value;
double paddingValue ;
double diameter;
@override
void initState() {
super.initState();
value = widget.value;
paddingValue=widget.height/12;
diameter = widget.height - 2 * paddingValue;
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
width: widget.width,
height: widget.height,
decoration: BoxDecoration(
color: value ? widget.activeColor : widget.inactiveColor,
borderRadius: BorderRadius.circular(widget.height / 2),
),
padding: EdgeInsets.all(paddingValue),
child: Align(
alignment: value?Alignment.centerRight:Alignment.centerLeft,
child: Container(
width: diameter,
height: diameter,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: widget.buttonColor,
),
),
),
);
}
}
先實現ui,效果如下:
- 我們要創建一個動畫,當點擊的時候,滑塊會從左邊滑動到右邊,所以首先混入 SingleTickerProviderStateMixin ,然後聲明動畫:
Animation<Alignment> _animation;
AnimationController _animationController;
- 初始化:
// 設置動畫取值範圍和時間曲線
_animation = Tween<Alignment>(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
).animate(
CurvedAnimation(
parent: _animationController,
curve: Curves.linear,
)
);
- 在我們的佈局外面添加AnimatedBuilder:
return AnimatedBuilder(
animation: _animationController,
builder: (animation,child){
return Container(
...
- 修改滑塊位置爲動畫的值:
child: Align(
alignment: _animation.value,
child: Container(
...
- 萬事俱備,下面就是點擊滑塊了,如果動畫結束了,也就是從左邊滑到了右邊(初始爲左邊),或者從右邊滑到了左邊(初始爲右),那麼要反向運動,也就是reverse,否則就是開始動畫,也就是forward:
child: GestureDetector(
onTap: () {
if (_animationController.isCompleted) {
_animationController.reverse();
} else {
_animationController.forward();
}
_value = !_value;
widget.onChanged?.call(_value);
},
...
看下效果:
完整代碼:
import 'package:flutter/material.dart';
class CustomSwitch extends StatefulWidget {
CustomSwitch({
Key key,
this.width = 120,
this.height = 50,
this.activeColor = Colors.blue,
this.inactiveColor = Colors.grey,
this.buttonColor = Colors.white,
this.onChanged,
this.value = false,
}) : super(key: key);
final double width;
final double height;
/// 打開時的顏色
final Color activeColor;
/// 關閉時的顏色
final Color inactiveColor;
/// 按鈕顏色
final Color buttonColor;
final ValueChanged<bool> onChanged;
final bool value;
@override
_CustomSwitchState createState() {
return _CustomSwitchState();
}
}
class _CustomSwitchState extends State<CustomSwitch>
with SingleTickerProviderStateMixin {
bool _value;
double _paddingValue;
double _diameter;
Animation<Alignment> _animation;
AnimationController _animationController;
@override
void initState() {
super.initState();
_value = widget.value;
_paddingValue = widget.height / 12;
_diameter = widget.height - 2 * _paddingValue;
// 初始化動畫控制器,設置動畫時間
_animationController = AnimationController(
vsync: this,
duration: Duration(milliseconds: 500),
);
// 設置動畫取值範圍和時間曲線
_animation = Tween<Alignment>(
begin: widget.value ? Alignment.centerRight : Alignment.centerLeft,
end: widget.value ? Alignment.centerLeft : Alignment.centerRight,
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.linear,
));
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animationController,
builder: (animation, child) {
return Container(
width: widget.width,
height: widget.height,
decoration: BoxDecoration(
color: _value ? widget.activeColor : widget.inactiveColor,
borderRadius: BorderRadius.circular(widget.height / 2),
),
padding: EdgeInsets.all(_paddingValue),
child: Align(
alignment: _animation.value,
child: GestureDetector(
onTap: () {
if (_animationController.isCompleted) {
_animationController.reverse();
} else {
_animationController.forward();
}
_value = !_value;
widget.onChanged?.call(_value);
},
child: Container(
width: _diameter,
height: _diameter,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: widget.buttonColor,
),
),
),
),
);
},
);
}
}
Hero動畫
Hero 指的是在屏幕間轉換的 widget。我們可以使用 Flutter’s Hero widget 創建 hero 動畫。使 hero 從原頁面過渡到新頁面。
所以Flutter 中的 Hero widget 實現的動畫類型也稱爲 共享元素過渡 或 共享元素動畫。
創建 hero 的步驟
- 定義一個起始 Hero widget,被稱爲 source hero。也就是要過度的widget,通常是圖片。
- 定義一個終點 Hero widget,被稱爲 destination hero。該 hero 與 source hero 使用一樣的 tag 標籤,hero 通過 tag 來匹配。 爲了獲得最佳效果,heroes 應該有幾乎完全相同的 widget 樹。
- 創建一個含有 destination hero 的頁面。目標頁面定義了動畫結束時應有的 widget 樹。
- 通過 Navigator 導航來觸發動畫。 Navigator 推送並彈出操作觸發原頁面和目標頁面中含有配對標籤 heroes 的 hero 動畫。
下面我們按照套路實現一個:
第一步,定義一個起始hero;
Hero(
tag: 'flippers',
child: Image.asset(
R.flippers,
),
)
第二部,定義一個終點hero:
Hero(
tag: 'flippers',
child: SizedBox(
width: 100.0,
child: Image.asset(
R.flippers,
),
),
)
第三部,創建個頁面裝載終點hero:
Scaffold(
appBar: AppBar(
title: const Text('Flippers Page'),
),
body: Container(
padding: const EdgeInsets.all(8.0),
alignment: Alignment.topLeft,
// Use background color to emphasize that it's a new route.
color: Colors.lightBlueAccent,
child: Hero(
tag: 'flippers',
child: SizedBox(
width: 100.0,
child: Image.asset(
R.flippers,
),
),
),
),
);
第四部,路由導航:
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (BuildContext context) {
...
看下效果:
頁面過度動畫
hero是2個widget之間的過度,頁面的過度需要PageTransitionsBuilder,flutter 給我們實現了4種:
- FadeUpwardsPageTransitionsBuilder — 淡入淡出
- OpenUpwardsPageTransitionsBuilder — 從下往上
- ZoomPageTransitionsBuilder — 從小到大縮放
- CupertinoPageTransitionsBuilder — 蘋果左右滑入風格
怎麼使用呢?一般我們應用都是一個統一的過度風格,淡然flutter是包含安卓和ios的,所以區分不同的平臺對應不同的風格。在全局設置:
MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
pageTransitionsTheme: PageTransitionsTheme(builders: {
TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
TargetPlatform.android: ZoomPageTransitionsBuilder(),
})),
routes: Routes.routes,
home: MyHomePage(title: '動畫'),
);
看下效果:
當然,如果你們的ui特別牛逼,要實現自己的風格,比如旋轉並且淡入淡出的過度動畫,我們就要自己實現了。
首先實現PageTransitionsBuilder:
class RotationFadeTransitionBuilder extends PageTransitionsBuilder {
const RotationFadeTransitionBuilder();
@override
Widget buildTransitions<T>(
PageRoute<T> route,
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
return _RotationFadeTransitionBuilder(
routeAnimation: animation, child: child);
}
}
buildTransitions
的返回值是widget,我們創建一個widget:
class _RotationFadeTransitionBuilder extends StatelessWidget {
_RotationFadeTransitionBuilder({
Key key,
@required Animation<double> routeAnimation,
@required this.child,
}) ;
final Widget child;
@override
Widget build(BuildContext context) {
}
}
因爲我們需要一個旋轉動畫和一個淡入淡出,所以我們實現動畫:
final Animation<double> _turnsAnimation;
final Animation<double> _opacityAnimation;
https://api.flutter.dev/flutter/animation/Curves-class.html
這是動畫對應的curve,我們選擇淡入淡出的和旋轉的,並通過Animation.drive加到過渡動畫上:
_RotationFadeTransitionBuilder({
Key key,
@required Animation<double> routeAnimation,
@required this.child,
}) : _turnsAnimation = routeAnimation.drive(CurveTween(curve: Curves.linearToEaseOut)),
_opacityAnimation = routeAnimation.drive( CurveTween(curve: Curves.easeIn)),
super(key: key);
實現我們的動畫:
@override
Widget build(BuildContext context) {
return RotationTransition(
turns: _turnsAnimation,
child: FadeTransition(
opacity: _opacityAnimation,
child: child,
),
);
}
添加到ThemeData中:
TargetPlatform.android: RotationFadeTransitionBuilder(),
動畫全部代碼:
import 'package:flutter/material.dart';
class RotationFadeTransitionBuilder extends PageTransitionsBuilder {
const RotationFadeTransitionBuilder();
@override
Widget buildTransitions<T>(
PageRoute<T> route,
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
return _RotationFadeTransitionBuilder(
routeAnimation: animation, child: child);
}
}
class _RotationFadeTransitionBuilder extends StatelessWidget {
_RotationFadeTransitionBuilder({
Key key,
@required Animation<double> routeAnimation,
@required this.child,
}) : _turnsAnimation = routeAnimation.drive(CurveTween(curve: Curves.linearToEaseOut)),
_opacityAnimation = routeAnimation.drive( CurveTween(curve: Curves.easeIn)),
super(key: key);
final Animation<double> _turnsAnimation;
final Animation<double> _opacityAnimation;
final Widget child;
@override
Widget build(BuildContext context) {
return RotationTransition(
turns: _turnsAnimation,
child: FadeTransition(
opacity: _opacityAnimation,
child: child,
),
);
}
}
看下效果: