作為一個移動端UI框架,Flutter 也擁有自己的動畫體系。
分類
Flutter 動畫分為兩類:補間動畫(Tween)和 基於物理的動畫。
本文主要介紹第一類動畫。
動畫的基本類
Animation<T>
Animation是一個抽象的類,主要儲存動畫的狀態和當前值。最常用的Animation類是Animation<double>
T 有很多型別,如Color、Offset。後面會詳細介紹
可以通過Animation中的 value 屬性獲得當前動畫的值。
動畫的監聽:
- addListener() 每一幀動畫執行的監聽
- addStatusListener() 動畫狀態改變的監聽。有下面四種狀態
AnimationController
AnimationController 繼承Animation<double>,負責控制動畫的執行,停止等。
AnimationController 會在動畫的每一幀,就會生成一個新的值。預設情況下,AnimationController在給定的時間段內線性的生成從0.0到1.0(預設區間)的數字。
建立 AnimationController,則需要傳入一個 vsync 引數。
Tween
預設情況下,AnimationController物件的範圍從0.0到1.0。
如果你需要不同範圍或者不同的資料型別,就需要tween來配置動畫以生成不同的範圍或資料型別的值
Tween的子類如下圖所示:
例子
class PageState extends State<HomePage> with SingleTickerProviderStateMixin {
AnimationController controller;
//doubler型別動畫
Animation<double> doubleAnimation;
//顏色動畫
Animation<Color> colorAnimation;
//位置動畫
Animation<Offset> offsetAnimation;
//圓角動畫
Animation<BorderRadius> radiusAnimation;
//裝飾動畫
Animation<Decoration> decorationAnimation;
//字型動畫
Animation<TextStyle> textStyleAnimation;
@override
void initState() {
// TODO: implement initState
super.initState();
//建立AnimationController
controller = new AnimationController(
vsync: this, duration: Duration(milliseconds: 2000));
//animation第一種建立方式:
doubleAnimation = new Tween<double>(begin: 0.0, end: 200.0).animate(controller)
..addListener(() {
setState(() {});
})
..addStatusListener((AnimationStatus status) {
//執行完成後反向執行
if (status == AnimationStatus.completed) {
controller.reverse();
} else if (status == AnimationStatus.dismissed) {
//反向執行完成,正向執行
controller.forward();
}
});
//animation第二種建立方式:
offsetAnimation = controller.drive(
Tween<Offset>(begin: Offset(0.0, 0.0),end: Offset(400.0, 200.0))
);
colorAnimation = ColorTween(begin: Colors.yellow,end: Colors.red).animate(controller);
radiusAnimation = BorderRadiusTween(begin: BorderRadius.circular(0),end: BorderRadius.circular(50)).animate(controller);
decorationAnimation = DecorationTween(begin: BoxDecoration(color: Colors.purple,borderRadius: BorderRadius.circular(0),),
end: BoxDecoration(color: Colors.lightBlueAccent,borderRadius: BorderRadius.circular(40))).animate(controller);
textStyleAnimation = TextStyleTween(begin: TextStyle(color: Colors.black,fontSize: 20,fontWeight: FontWeight.w100),
end: TextStyle(color: Colors.purple,fontSize: 30,fontWeight: FontWeight.w700)).animate(controller);
//啟動動畫
controller.forward();
}
@override
Widget build(BuildContext context) {
// TODO: implement build
return Scaffold(
appBar: AppBar(title: Text("Tween動畫"),),
body: Container(
alignment: Alignment.center,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
SizedBox(
height: 200,
child: Container(
height: doubleAnimation.value,
width: doubleAnimation.value,
child: FlutterLogo(),
),
),
Container(
margin: EdgeInsets.only(left: offsetAnimation.value.dx),
width: 50,
height: 50,
color: Colors.green,
),
Container(
height: 100,
width: 100,
color: colorAnimation.value,
),
SizedBox(height: 10,),
Container(
height: 100,
width: 100,
decoration: BoxDecoration(borderRadius: radiusAnimation.value,color: Colors.blue),
),
Container(
height: 60,
width: 200,
decoration: decorationAnimation.value,
),
Container(
height: 100,
child: Text("TestStyleTween",style: textStyleAnimation.value,),
),
],
),
)
);
}
@override
void dispose() {
// TODO: implement dispose
super.dispose();
controller.dispose();
}
}
複製程式碼
tween動畫執行效果圖
這裡列舉裡幾種比較簡單的Tween動畫。在上面的程式碼中我們通過對animation設定addListener()對每一幀的變化進行監聽,當animation 中的value插值發生改變時呼叫 setState(() {});重新整理佈局,從而達到動畫過度的效果。當我們state中的佈局複雜的時候,我們在每一幀變化的時候都呼叫setState來重新整理widget樹,會把state中所有的widget都重新繪製,這樣就會造成不必要的效能消耗,我們只需要重新整理執行動畫的那個widget就行了。Flutter為我們提供了AnimatedWidget。
AnimatedWidget
原始碼
abstract class AnimatedWidget extends StatefulWidget {
//建立一個widget,當listenable 發生改變時重構
const AnimatedWidget({
Key key,
@required this.listenable,
}) : assert(listenable != null),
super(key: key);
//宣告一個Listenable ,幀動畫監聽
final Listenable listenable;
@protected
Widget build(BuildContext context);
/// Subclasses typically do not override this method.
@override
_AnimatedState createState() => _AnimatedState();
...
}
複製程式碼
AnimatedWidget繼承自StatefulWidget,擁有自己的狀態,並且例項一個listenable用來監聽幀動畫,當動畫方法變化時,重新整理AnimatedWidget。
__AnimatedState
方法原始碼如下:
class _AnimatedState extends State<AnimatedWidget> {
@override
void initState() {
super.initState();
//新增監聽的回撥
widget.listenable.addListener(_handleChange);
}
@override
void didUpdateWidget(AnimatedWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.listenable != oldWidget.listenable) {
oldWidget.listenable.removeListener(_handleChange);
widget.listenable.addListener(_handleChange);
}
}
@override
void dispose() {
widget.listenable.removeListener(_handleChange);
super.dispose();
}
//當幀動畫發生改變時觸發重新整理
void _handleChange() {
setState(() {
// The listenable's state is our build state, and it changed already.
});
}
//呼叫build()方法,重構AnimatedWidget
@override
Widget build(BuildContext context) => widget.build(context);
}
複製程式碼
當listenable觸發重新整理的時候,呼叫 setState
重構AnimatedWidget,雖然到最後還是呼叫setState,但是重新整理的物件是不同的。
Listenable
從原始碼中可以看到在AnimatedWidget宣告瞭一個Listenable,用來監聽每一幀的變化。那麼Listenable又是啥呢。我們可以看一小截Animation的原始碼:
可以看到Animation也是繼承自Listenable,Listenable 原始碼如下:abstract class Listenable {
const Listenable();
factory Listenable.merge(List<Listenable> listenables) = _MergingListenable;
void addListener(VoidCallback listener);
void removeListener(VoidCallback listener);
}
複製程式碼
所以Animation也是一個Listenable 實現類。原始碼看完,具體用法如下:
ColorAnimationWidget
宣告一個ColorAnimationWidget類,繼承自AnimatedWidget。程式碼如下:
class ColorAnimationWidget extends AnimatedWidget{
ColorAnimationWidget({Key key, Animation<Color> animation})
: super(key: key, listenable: animation);
@override
Widget build(BuildContext context) {
final Animation<Color> animation = listenable;
// TODO: implement build
return Center(
child: Container(
width: 200,
height: 200,
color: animation.value,
),
);
}
}
複製程式碼
使用ColorAnimationWidget
class PageState extends State<HomePage> with SingleTickerProviderStateMixin{
AnimationController _controller;
Animation<Color> _animation;
@override
void initState() {
// TODO: implement initState
super.initState();
_controller = AnimationController(vsync: this,duration: Duration(seconds: 2));
_animation = ColorTween(begin: Colors.lightBlueAccent,end: Colors.red).animate(_controller)
..addStatusListener((AnimationStatus status){
if(status == AnimationStatus.completed){
_controller.reverse();
}else if(status == AnimationStatus.dismissed){
_controller.forward();
}
});
_controller.forward();
}
@override
Widget build(BuildContext context) {
// TODO: implement build
return Scaffold(
body: ColorAnimationWidget(animation: _animation,),
);
}
}
class ColorAnimationWidget extends AnimatedWidget{
ColorAnimationWidget({Key key, Animation<Color> animation})
: super(key: key, listenable: animation);
@override
Widget build(BuildContext context) {
final Animation<Color> animation = listenable;
// TODO: implement build
return Center(
child: Container(
width: 200,
height: 200,
color: animation.value,
),
);
}
}
複製程式碼
可以看到用法基本和不使用AnimatedWidget一樣,唯一的區別就是使用AnimatedWidget時setState
是在AnimatedWidget內呼叫的,只重新整理一個widget。
ColorAnimationWidget效果圖:
CurvedAnimation
Tween動畫預設為我們提供了區間內線性變化,如果我們需要曲線變化,則需要配合使用CurvedAnimation。如下圖彈性效果:
用法如下:class HomePageState extends State<HomePage> with TickerProviderStateMixin{
Animation animation;
AnimationController controller;
@override
void initState() {
// TODO: implement initState
super.initState();
controller = new AnimationController(vsync: this,duration: Duration(seconds: 2));
CurvedAnimation curve = new CurvedAnimation(parent: controller, curve: Curves.bounceOut);
animation = Tween<double>(begin: 0.0,end: 500).animate(curve)
..addListener((){
setState(() {
});
});
controller.forward();
}
@override
Widget build(BuildContext context) {
// TODO: implement build
return Container(
alignment: Alignment.topCenter,
child: Container(
margin: EdgeInsets.only(top: animation.value),
width: 100,
height: 100,
child: FlutterLogo(),
),
);
}
}
複製程式碼
CurvedAnimation curve = new CurvedAnimation(parent: controller, curve: Curves.bounceOut);
這裡我們使用的是bounceOut
效果
更多的曲線效果可以檢視官網的效果圖:Curves
我們看一下bounceOut
是如何實現的:
class _BounceOutCurve extends Curve {
const _BounceOutCurve._();
@override
double transformInternal(double t) {
return _bounce(t);
}
}
double _bounce(double t) {
if (t < 1.0 / 2.75) {
return 7.5625 * t * t;
} else if (t < 2 / 2.75) {
t -= 1.5 / 2.75;
return 7.5625 * t * t + 0.75;
} else if (t < 2.5 / 2.75) {
t -= 2.25 / 2.75;
return 7.5625 * t * t + 0.9375;
}
t -= 2.625 / 2.75;
return 7.5625 * t * t + 0.984375;
}
複製程式碼
可以看到bounceOut 是繼承 Curve類,實現它的transformInternal方法,在transformInternal實現它的軌跡。
AnimatedBuilder
上面我們實現了一個顏色變化的例子,假如我們現在需要實現一個大小變化的widget呢?是不是要在宣告一個SizeAnimationWidget繼承AnimationWidget ?
顯然這樣做是可以的。當然我們也有更好的做法,就是使用AnimatedBuilder來重構我們的widget。
什麼是AnimatedBuilder
AnimatedBuilder繼承自抽象的AnimationWidget ,目的為了構建通用的AnimationWidget 實現類,不用每次使用AnimationWidget 都要建立一個實現類。
AnimatedBuilder原始碼:
class AnimatedBuilder extends AnimatedWidget {
/// Creates an animated builder.
const AnimatedBuilder({
Key key,
@required Listenable animation,
@required this.builder,
this.child,
}) : assert(animation != null),
assert(builder != null),
super(key: key, listenable: animation);
final TransitionBuilder builder;
final Widget child;
@override
Widget build(BuildContext context) {
return builder(context, child);
}
}
複製程式碼
使用的時候我們只需要傳入animation和builder就行了
用法
class _LogoAppState extends State<LogoApp> with TickerProviderStateMixin {
Animation animation;
AnimationController controller;
initState() {
super.initState();
controller = new AnimationController(
duration: const Duration(milliseconds: 2000), vsync: this);
final CurvedAnimation curve =
new CurvedAnimation(parent: controller, curve: Curves.bounceOut);
animation = new Tween(begin: 0.0, end: 300.0).animate(curve);
controller.forward();
}
Widget build(BuildContext context) {
return new GrowTransition(child: new LogoWidget(), animation: animation);
}
dispose() {
controller.dispose();
super.dispose();
}
}
class GrowTransition extends StatelessWidget {
GrowTransition({this.child, this.animation});
final Widget child;
final Animation<double> animation;
Widget build(BuildContext context) {
return new Center(
child: new AnimatedBuilder(
animation: animation,
builder: (BuildContext context, Widget child) {
return new Container(
height: animation.value, width: animation.value, child: child);
},
child: child),
);
}
}
class LogoWidget extends StatelessWidget {
// Leave out the height and width so it fills the animating parent
build(BuildContext context) {
return new Container(
margin: new EdgeInsets.symmetric(vertical: 10.0),
child: new FlutterLogo(),
);
}
}
複製程式碼
效果圖
總結
tween動畫到這裡就結束了,Hero動畫留下一次在補充吧
參考:Flutter動畫教程