為了表述方便,本書約定,將在 Widget 屬性發(fā)生變化時會執(zhí)行過渡動畫的組件統(tǒng)稱為”動畫過渡組件“,而動畫過渡組件最明顯的一個特征就是它會在內部自管理AnimationController
。我們知道,為了方便使用者可以自定義動畫的曲線、執(zhí)行時長、方向等,在前面介紹過的動畫封裝方法中,通常都需要使用者自己提供一個AnimationController
對象來自定義這些屬性值。但是,如此一來,使用者就必須得手動管理AnimationController
,這又會增加使用的復雜性。因此,如果也能將AnimationController
進行封裝,則會大大提高動畫組件的易用性。
我們要實現(xiàn)一個AnimatedDecoratedBox
,它可以在decoration
屬性發(fā)生變化時,從舊狀態(tài)變成新狀態(tài)的過程可以執(zhí)行一個過渡動畫。根據(jù)前面所學的知識,我們實現(xiàn)了一個AnimatedDecoratedBox1
組件:
class AnimatedDecoratedBox1 extends StatefulWidget {
AnimatedDecoratedBox1({
Key key,
@required this.decoration,
this.child,
this.curve = Curves.linear,
@required this.duration,
this.reverseDuration,
});
final BoxDecoration decoration;
final Widget child;
final Duration duration;
final Curve curve;
final Duration reverseDuration;
@override
_AnimatedDecoratedBox1State createState() => _AnimatedDecoratedBox1State();
}
class _AnimatedDecoratedBox1State extends State<AnimatedDecoratedBox1>
with SingleTickerProviderStateMixin {
@protected
AnimationController get controller => _controller;
AnimationController _controller;
Animation<double> get animation => _animation;
Animation<double> _animation;
DecorationTween _tween;
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animation,
builder: (context, child){
return DecoratedBox(
decoration: _tween.animate(_animation).value,
child: child,
);
},
child: widget.child,
);
}
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: widget.duration,
reverseDuration: widget.reverseDuration,
vsync: this,
);
_tween = DecorationTween(begin: widget.decoration);
_updateCurve();
}
void _updateCurve() {
if (widget.curve != null)
_animation = CurvedAnimation(parent: _controller, curve: widget.curve);
else
_animation = _controller;
}
@override
void didUpdateWidget(AnimatedDecoratedBox1 oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.curve != oldWidget.curve)
_updateCurve();
_controller.duration = widget.duration;
_controller.reverseDuration = widget.reverseDuration;
if(widget.decoration!= (_tween.end ?? _tween.begin)){
_tween
..begin = _tween.evaluate(_animation)
..end = widget.decoration;
_controller
..value = 0.0
..forward();
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
下面我們來使用AnimatedDecoratedBox1
來實現(xiàn)按鈕點擊后背景色從藍色過渡到紅色的效果:
Color _decorationColor = Colors.blue;
var duration = Duration(seconds: 1);
...//省略無關代碼
AnimatedDecoratedBox(
duration: duration,
decoration: BoxDecoration(color: _decorationColor),
child: FlatButton(
onPressed: () {
setState(() {
_decorationColor = Colors.red;
});
},
child: Text(
"AnimatedDecoratedBox",
style: TextStyle(color: Colors.white),
),
),
)
點擊前效果如圖9-8所示,點擊后截取了過渡過程的一幀如圖9-9所示: ![img] 點擊后,按鈕背景色會從藍色向紅色過渡,圖9-9是過渡過程中的一幀,有點偏紫色,整個過渡動畫結束后背景會變?yōu)榧t色。
上面的代碼雖然實現(xiàn)了我們期望的功能,但是代碼卻比較復雜。稍加思考后,我們就可以發(fā)現(xiàn),AnimationController
的管理以及 Tween 更新部分的代碼都是可以抽象出來的,如果我們這些通用邏輯封裝成基類,那么要實現(xiàn)動畫過渡組件只需要繼承這些基類,然后定制自身不同的代碼(比如動畫每一幀的構建方法)即可,這樣將會簡化代碼。
為了方便開發(fā)者來實現(xiàn)動畫過渡組件的封裝,F(xiàn)lutter 提供了一個ImplicitlyAnimatedWidget
抽象類,它繼承自 StatefulWidget,同時提供了一個對應的ImplicitlyAnimatedWidgetState
類,AnimationController
的管理就在ImplicitlyAnimatedWidgetState
類中。開發(fā)者如果要封裝動畫,只需要分別繼承ImplicitlyAnimatedWidget
和ImplicitlyAnimatedWidgetState
類即可,下面我們演示一下具體如何實現(xiàn)。
我們需要分兩步實現(xiàn):
ImplicitlyAnimatedWidget
類。 class AnimatedDecoratedBox extends ImplicitlyAnimatedWidget {
AnimatedDecoratedBox({
Key key,
@required this.decoration,
this.child,
Curve curve = Curves.linear, //動畫曲線
@required Duration duration, // 正向動畫執(zhí)行時長
Duration reverseDuration, // 反向動畫執(zhí)行時長
}) : super(
key: key,
curve: curve,
duration: duration,
reverseDuration: reverseDuration,
);
final BoxDecoration decoration;
final Widget child;
@override
_AnimatedDecoratedBoxState createState() {
return _AnimatedDecoratedBoxState();
}
}
其中curve
、duration
、reverseDuration
三個屬性在ImplicitlyAnimatedWidget
中已定義。 可以看到AnimatedDecoratedBox
類和普通繼承自StatefulWidget
的類沒有什么不同。
AnimatedWidgetBaseState
(該類繼承自ImplicitlyAnimatedWidgetState
類)。 class _AnimatedDecoratedBoxState
extends AnimatedWidgetBaseState<AnimatedDecoratedBox> {
DecorationTween _decoration; //定義一個Tween
@override
Widget build(BuildContext context) {
return DecoratedBox(
decoration: _decoration.evaluate(animation),
child: widget.child,
);
}
@override
void forEachTween(visitor) {
// 在需要更新Tween時,基類會調用此方法
_decoration = visitor(_decoration, widget.decoration,
(value) => DecorationTween(begin: value));
}
}
可以看到我們實現(xiàn)了build
和forEachTween
兩個方法。在動畫執(zhí)行過程中,每一幀都會調用build
方法(調用邏輯在ImplicitlyAnimatedWidgetState
中),所以在build
方法中我們需要構建每一幀的DecoratedBox
狀態(tài),因此得算出每一幀的decoration
狀態(tài),這個我們可以通過_decoration.evaluate(animation)
來算出,其中animation
是ImplicitlyAnimatedWidgetState
基類中定義的對象,_decoration
是我們自定義的一個DecorationTween
類型的對象,那么現(xiàn)在的問題就是它是在什么時候被賦值的呢?要回答這個問題,我們就得搞清楚什么時候需要對_decoration
賦值。我們知道_decoration
是一個 Tween,而 Tween的主要職責就是定義動畫的起始狀態(tài)(begin)和終止狀態(tài)(end)。對于AnimatedDecoratedBox
來說,decoration
的終止狀態(tài)就是用戶傳給它的值,而起始狀態(tài)是不確定的,有以下兩種情況:
AnimatedDecoratedBox
首次 build,此時直接將其decoration
值置為起始狀態(tài),即_decoration
值為DecorationTween(begin: decoration)
。AnimatedDecoratedBox
的decoration
更新時,則起始狀態(tài)為_decoration.animate(animation)
,即_decoration
值為DecorationTween(begin: _decoration.animate(animation),end:decoration)
。
現(xiàn)在forEachTween
的作用就很明顯了,它正是用于來更新 Tween 的初始值的,在上述兩種情況下會被調用,而開發(fā)者只需重寫此方法,并在此方法中更新 Tween 的起始狀態(tài)值即可。而一些更新的邏輯被屏蔽在了visitor
回調,我們只需要調用它并給它傳遞正確的參數(shù)即可,visitor
方法簽名如下:
Tween visitor(
Tween<dynamic> tween, //當前的tween,第一次調用為null
dynamic targetValue, // 終止狀態(tài)
TweenConstructor<dynamic> constructor,//Tween構造器,在上述三種情況下會被調用以更新tween
);
可以看到,通過繼承ImplicitlyAnimatedWidget
和ImplicitlyAnimatedWidgetState
類可以快速的實現(xiàn)動畫過渡組件的封裝,這和我們純手工實現(xiàn)相比,代碼簡化了很多。
如果讀者還有疑惑,建議查看
ImplicitlyAnimatedWidgetState
的源碼并結合本示例代碼對比理解。
在使用動畫過渡組件,我們只需要在改變一些屬性值后重新 build 組件即可,所以要實現(xiàn)狀態(tài)反向過渡,只需要將前后狀態(tài)值互換即可實現(xiàn),這本來是不需要再浪費筆墨的。但是ImplicitlyAnimatedWidget
構造函數(shù)中卻有一個reverseDuration
屬性用于設置反向動畫的執(zhí)行時長,這貌似在告訴讀者ImplicitlyAnimatedWidget
本身也提供了執(zhí)行反向動畫的接口,于是筆者查看了ImplicitlyAnimatedWidgetState
源碼并未發(fā)現(xiàn)有執(zhí)行反向動畫的接口,唯一有用的是它暴露了控制動畫的controller
。所以如果要讓reverseDuration
生效,我們只能先獲取controller
,然后再通過controller.reverse()
來啟動反向動畫,比如我們在上面示例的基礎上實現(xiàn)一個循環(huán)的點擊背景顏色變換效果,要求從藍色變?yōu)榧t色時動畫執(zhí)行時間為 400ms,從紅變藍為2s,如果要使reverseDuration
生效,我們需要這么做:
AnimatedDecoratedBox(
duration: Duration( milliseconds: 400),
decoration: BoxDecoration(color: _decorationColor),
reverseDuration: Duration(seconds: 2),
child: Builder(builder: (context) {
return FlatButton(
onPressed: () {
if (_decorationColor == Colors.red) {
ImplicitlyAnimatedWidgetState _state =
context.findAncestorStateOfType<ImplicitlyAnimatedWidgetState>();
// 通過controller來啟動反向動畫
_state.controller.reverse().then((e) {
// 經驗證必須調用setState來觸發(fā)rebuild,否則狀態(tài)同步會有問題
setState(() {
_decorationColor = Colors.blue;
});
});
} else {
setState(() {
_decorationColor = Colors.red;
});
}
},
child: Text(
"AnimatedDecoratedBox toggle",
style: TextStyle(color: Colors.white),
),
);
}),
)
上面的代碼實際上是非常糟糕且沒必要的,它需要我們了解ImplicitlyAnimatedWidgetState
內部實現(xiàn),并且要手動去啟動反向動畫。我們完全可以通過如下代碼實現(xiàn)相同的效果:
AnimatedDecoratedBox(
duration: Duration(
milliseconds: _decorationColor == Colors.red ? 400 : 2000),
decoration: BoxDecoration(color: _decorationColor),
child: Builder(builder: (context) {
return FlatButton(
onPressed: () {
setState(() {
_decorationColor = _decorationColor == Colors.blue
? Colors.red
: Colors.blue;
});
},
child: Text(
"AnimatedDecoratedBox toggle",
style: TextStyle(color: Colors.white),
),
);
}),
)
這樣的代碼是不是優(yōu)雅的多!那么現(xiàn)在問題來了,為什么ImplicitlyAnimatedWidgetState
要提供一個reverseDuration
參數(shù)呢?筆者仔細研究了ImplicitlyAnimatedWidgetState
的實現(xiàn),發(fā)現(xiàn)唯一的解釋就是該參數(shù)并非是給ImplicitlyAnimatedWidgetState
用的,而是給子類用的!原因正如我們前面說的,要使reverseDuration
有用就必須得獲取controller
屬性來手動啟動反向動畫,ImplicitlyAnimatedWidgetState
中的controller
屬性是一個保護屬性,定義如下:
@protected
AnimationController get controller => _controller;
而保護屬性原則上只應該在子類中使用,而不應該像上面示例代碼一樣在外部使用。綜上,我們可以得出兩條結論:
ImplicitlyAnimatedWidgetState
中controller
的方式。reverseDuration
,那么最好就不要暴露此參數(shù),比如我們上面自定義的AnimatedDecoratedBox
定義中就可以去除reverseDuration
可選參數(shù),如: class AnimatedDecoratedBox extends ImplicitlyAnimatedWidget {
AnimatedDecoratedBox({
Key key,
@required this.decoration,
this.child,
Curve curve = Curves.linear,
@required Duration duration,
}) : super(
key: key,
curve: curve,
duration: duration,
);
Flutter SDK 中也預置了很多動畫過渡組件,實現(xiàn)方式和大都和AnimatedDecoratedBox
差不多,如表9-1所示:
組件名 | 功能 |
---|---|
AnimatedPadding | 在 padding 發(fā)生變化時會執(zhí)行過渡動畫到新狀態(tài) |
AnimatedPositioned | 配合 Stack 一起使用,當定位狀態(tài)發(fā)生變化時會執(zhí)行過渡動畫到新的狀態(tài)。 |
AnimatedOpacity | 在透明度 opacity 發(fā)生變化時執(zhí)行過渡動畫到新狀態(tài) |
AnimatedAlign | 當alignment 發(fā)生變化時會執(zhí)行過渡動畫到新的狀態(tài)。 |
AnimatedContainer | 當 Container 屬性發(fā)生變化時會執(zhí)行過渡動畫到新的狀態(tài)。 |
AnimatedDefaultTextStyle | 當字體樣式發(fā)生變化時,子組件中繼承了該樣式的文本組件會動態(tài)過渡到新樣式。 |
表9-1:Flutter 預置的動畫過渡組件
下面我們通過一個示例來感受一下這些預置的動畫過渡組件效果:
import 'package:flutter/material.dart';
class AnimatedWidgetsTest extends StatefulWidget {
@override
_AnimatedWidgetsTestState createState() => _AnimatedWidgetsTestState();
}
class _AnimatedWidgetsTestState extends State<AnimatedWidgetsTest> {
double _padding = 10;
var _align = Alignment.topRight;
double _height = 100;
double _left = 0;
Color _color = Colors.red;
TextStyle _style = TextStyle(color: Colors.black);
Color _decorationColor = Colors.blue;
@override
Widget build(BuildContext context) {
var duration = Duration(seconds: 5);
return SingleChildScrollView(
child: Column(
children: <Widget>[
RaisedButton(
onPressed: () {
setState(() {
_padding = 20;
});
},
child: AnimatedPadding(
duration: duration,
padding: EdgeInsets.all(_padding),
child: Text("AnimatedPadding"),
),
),
SizedBox(
height: 50,
child: Stack(
children: <Widget>[
AnimatedPositioned(
duration: duration,
left: _left,
child: RaisedButton(
onPressed: () {
setState(() {
_left = 100;
});
},
child: Text("AnimatedPositioned"),
),
)
],
),
),
Container(
height: 100,
color: Colors.grey,
child: AnimatedAlign(
duration: duration,
alignment: _align,
child: RaisedButton(
onPressed: () {
setState(() {
_align = Alignment.center;
});
},
child: Text("AnimatedAlign"),
),
),
),
AnimatedContainer(
duration: duration,
height: _height,
color: _color,
child: FlatButton(
onPressed: () {
setState(() {
_height = 150;
_color = Colors.blue;
});
},
child: Text(
"AnimatedContainer",
style: TextStyle(color: Colors.white),
),
),
),
AnimatedDefaultTextStyle(
child: GestureDetector(
child: Text("hello world"),
onTap: () {
setState(() {
_style = TextStyle(
color: Colors.blue,
decorationStyle: TextDecorationStyle.solid,
decorationColor: Colors.blue,
);
});
},
),
style: _style,
duration: duration,
),
AnimatedDecoratedBox(
duration: duration,
decoration: BoxDecoration(color: _decorationColor),
child: FlatButton(
onPressed: () {
setState(() {
_decorationColor = Colors.red;
});
},
child: Text(
"AnimatedDecoratedBox",
style: TextStyle(color: Colors.white),
),
),
)
].map((e) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: e,
);
}).toList(),
),
);
}
}
運行后效果如圖9-10所示:
讀者可以點擊一下相應組件來查看一下實際的運行效果。
更多建議: