每一個平臺對於動畫的實現大同小異,手段大部分都是在60幀(部分Android機型90FPS,部分iPad是120FPS)的刷新頻率下實現UI的屢次變化,利用人眼視覺殘留實現「流暢」的動做。html
在理想狀態下,Flutter 可以實現 60 FPS。(這裏的刷新頻率是否跟隨硬件,找到資料再更新)markdown
可是每種UI框架對動畫的抽象方式都不同,而 Flutter 中實現一個動畫須要涉及到 Animation、Curve、Controller、Tween這四個角色。框架
Animation是一個抽象類,從下面的Animation的部分源碼能夠看出,class Animation 僅定義了動畫當前的值和狀態,以及監聽方法等內容,與UI展現樣式等相關定義和屬性無關(color, width 等)。less
abstract class Animation<T> extends Listenable implements ValueListenable<T> {
const Animation();
@override
void addListener(VoidCallback listener);
@override
void removeListener(VoidCallback listener);
void addStatusListener(AnimationStatusListener listener);
void removeStatusListener(AnimationStatusListener listener);
AnimationStatus get status;
@override
T get value;
bool get isDismissed => status == AnimationStatus.dismissed;
bool get isCompleted => status == AnimationStatus.completed;
// 代碼省略
// ...
}
複製代碼
繼承自 abstract class Animation
類,用於控制動畫的 進行(forward
)、中止(stop
)、反向播放(reverse
)。ide
默認狀況下,AnimationController
對象會在動畫的每一幀,按照動畫曲線的規律生成 0 - 1 區間內的值。以下代碼:函數
final AnimationController controller = new AnimationController(duration: const Duration(milliseconds: 2000), vsync: this);
複製代碼
這段代碼的意義是:在2000毫秒(秒)內,隨着幀刷新的頻率,線性的生成 0 - 1 區間內的值。動畫
其中 vsync 參數須要傳入 TickerProvider 對象,用於屏幕刷新時的回調。ui
傳入 TickerProvider 對象除了提供屏幕刷新回調之外,還能夠防止 屏外動畫的問題出現。當動畫UI不在當前屏幕時,或者手機鎖屏時,動畫刷新會中止,避免消耗沒必要要的資源。this
固然,動畫如此經常使用的操做,Flutter毫不會讓你花費很大的代價去實現,一般咱們只要在 State 中 mixin SingleTickerProviderStateMixin 便可。 以下代碼:spa
class _StaggerDemoState extends State<StaggerDemo> with TickerProviderStateMixin {
//...
}
複製代碼
動畫的實現一般伴隨着 AnimationControlelr
對象的 dispose
,勢必須要藉助 StatefullWidget
的聲明週期方法dispose
。天然也會有對應的State
對象,因此這裏不須要去糾結 StatelessWidget
是否可使用 TickerProviderStateMixin
的疑問。
以上所講的內容均是在 線性動畫 下,若是想要執行非線性動畫,則須要藉助 Curve
的協助。
動畫中使用 Curve
能夠改變更畫曲線,Curve
已經提供經常使用的動畫曲線,下面列出部分枚舉類:
Curves曲線 | 動畫過程 - | - linear | 勻速的 decelerate | 勻減速 ease | 開始加速,後面減速 easeIn | 開始慢,後面快 easeOut | 開始快,後面慢 easeInOut | 開始慢,而後加速,最後再減速
除了已經提供好的 Curve
曲線,也能夠自定義動畫曲線,實現起來也很簡單。下面是一個正弦曲線的實現:
class ShakeCurve extends Curve {
@override
double transform(double t) {
return math.sin(t * math.PI * 2);
}
}
複製代碼
從上面的代碼能夠看出,要實現一個自定義動畫曲線,只須要重寫 Curve
的 transform
方法便可。
若是要實現的動畫效果不知足於0-1的區間的話,還能夠藉助 Tween
對象實現自定義動畫區間。
提供開發自定義動畫區間的能力。以下示例所示:
Tween<double>(begin: -200.0, end: 0.0);
Tween<Color>(begin: Colors.transparent, end: Colors.black54);
Tween<EdgeInsets>(begin: const EdgeInsets.only(left: .0), end: const EdgeInsets.only(left: 100.0)
複製代碼
這些均不是0 - 1 的區間。Color
的區間能夠實現一個Color
到另外一個Color
的漸變過渡。EdgeInsets
能夠實現間距的漸變。
如此看起來,貌似Tween就已經能夠實現0-1到任意區間的映射了,然而,事情毫不會如此順利。
讓咱們來看一下 Tween的定義:
class Tween<T extends dynamic> extends Animatable<T> {
Tween({ this.begin, this.end });
T begin;
T end;
/// Returns the value this variable has at the given animation clock value.
///
/// The default implementation of this method uses the [+], [-], and [*]
/// operators on `T`. The [begin] and [end] properties must therefore be
/// non-null by the time this method is called.
@protected
T lerp(double t) {
assert(begin != null);
assert(end != null);
return begin + (end - begin) * t;
}
@override
T transform(double t) {
if (t == 0.0)
return begin;
if (t == 1.0)
return end;
return lerp(t);
}
@override
String toString() => '$runtimeType($begin \u2192 $end)';
}
複製代碼
從上面的源碼能夠看出,Tween
的定義十分的簡單,毫不會支持全部類型的區間映射。lerp
函數已經告訴咱們,類型 T
的對象,須要進行 + - *
三則運算,所以,可以運用 Tween
自動實現區間映射的對象,只能是實現了三則運算的對象,如上面例子中的 double
、EdgeInsets
,而Color
則不能直接使用Tween
實現區間映射。這時候就須要開發者自行實現double -> Color
類型的區間映射。
那咱們就本身實現一個 ColorTween
:
class ColorTween extends Tween<Color> {
ColorTween({ Color begin, Color end }) : super(begin: begin, end: end);
@override
Color lerp(double t) => Color.lerp(begin, end, t);
}
複製代碼
咱們利用 Color
對象的 lerp
方法很輕鬆的實現了 ColorTween
,只是重寫了 Tween
類的 lerp
方法,返回區間映射的計算方法而已。類比ColorTween
的實現,其餘類型的區間映射,也能夠如此寫。只要你喜歡,也能夠實現正弦函數的區間映射關係。
Flutter 已經提供了一些線程的Tween子類給開發者使用:
動畫的當前值,動畫的執行,動畫曲線,動畫區間 都已經實現了,那下一步就是結合這四個部分,實現動畫效果。
先來看一個完整的動畫定義:
// 1. 建立動畫控制類 AnimationController,用於執行動畫
final AnimationController _controller = AnimationController(duration: const Duration(milliseconds: 2000), vsync: this);
// 2. 使用 CurvedAnimation 結合 動畫控制類 和 動畫曲線類,返回一個 「具備指定動畫曲線的」「動畫」 對象
fianl CurvedAnimation curvedAnimation = CurvedAnimation(
parent: _controller,
curve: Curves.ease,
);
// 3. 自定義區間 再次結合 CurvedAnimation,生成一個「具備指定區間值」和「指定動畫曲線」的「動畫」對象
final Animation<double> height = Tween<double>(begin: 0, end: 300.0).animate(curvedAnimation);
複製代碼
上述就是一個完整的動畫建立過程,其中使用了一個新的 CurvedAnimation
類,從名字就能夠看出,CurvedAnimation
和Curve
、Animation
都有關係,關係就是結合這二者,生成一個具備指定動畫曲線的Animation
對象。
CurvedAnimation
繼承自 Animation<T>
,一樣,它能夠標識動畫的當前值,卻不能控制動畫的執行。
動畫已經建立出來了,怎麼根據建立好的動畫構建出能夠刷新的動畫呢。
這裏有主要的三種方式構建動畫UI。有興趣的能夠看這篇文章。這裏就直接上手最推薦使用AnimationedBuilder
。示例代碼以下:
@override
Widget build(BuildContext context) {
return Center(
child: AnimatedBuilder(
animation: _controller,
builder: (BuildContext context, Widget _) => Container(
width: 200,
height: height.value,
color: Colors.red,
),
),
);
}
複製代碼
完整代碼以下:
class BasicAnimation extends StatefulWidget {
@override
_BasicAnimationState createState() => _BasicAnimationState();
}
class _BasicAnimationState extends State<BasicAnimation> with SingleTickerProviderStateMixin {
AnimationController _animationController;
Animation<double> _height;
@override
void initState() {
_animationController = AnimationController(vsync: this, duration: Duration(seconds: 1));
_height = CurvedAnimation(parent: _animationController, curve: Curves.easeInOut);
_height = Tween<double>(begin: 0, end: 200).animate(_height);
super.initState();
_animationController.forward();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Container(
width: 300,
height: 300,
color: Colors.grey,
alignment: Alignment.bottomCenter,
child: _buildAnimatedWidget,
),
));
}
Widget get _buildAnimatedWidget => AnimatedBuilder(
animation: _animationController,
builder: (BuildContext context, Widget child) {
print('build animation');
return Container(
width: 40,
height: _height.value,
color: Colors.red,
);
},
);
@override
void dispose() {
_animationController?.dispose();
super.dispose();
}
}
複製代碼
相似於組動畫,或者動畫組的概念。同時或者交叉的執行多個動畫。
下面咱們實現一個:
的一個組合動畫。
咱們直接看代碼(未完待續...)