Flutter實戰(zhàn) 動畫過渡組件

2021-03-08 18:02 更新

為了表述方便,本書約定,將在 Widget 屬性發(fā)生變化時會執(zhí)行過渡動畫的組件統(tǒng)稱為”動畫過渡組件“,而動畫過渡組件最明顯的一個特征就是它會在內(nèi)部自管理AnimationController。我們知道,為了方便使用者可以自定義動畫的曲線、執(zhí)行時長、方向等,在前面介紹過的動畫封裝方法中,通常都需要使用者自己提供一個AnimationController對象來自定義這些屬性值。但是,如此一來,使用者就必須得手動管理AnimationController,這又會增加使用的復(fù)雜性。因此,如果也能將AnimationController進行封裝,則會大大提高動畫組件的易用性。

#9.7.1 自定義動畫過渡組件

我們要實現(xiàn)一個AnimatedDecoratedBox,它可以在decoration屬性發(fā)生變化時,從舊狀態(tài)變成新狀態(tài)的過程可以執(zhí)行一個過渡動畫。根據(jù)前面所學(xué)的知識,我們實現(xiàn)了一個AnimatedDecoratedBox1組件:

  1. class AnimatedDecoratedBox1 extends StatefulWidget {
  2. AnimatedDecoratedBox1({
  3. Key key,
  4. @required this.decoration,
  5. this.child,
  6. this.curve = Curves.linear,
  7. @required this.duration,
  8. this.reverseDuration,
  9. });
  10. final BoxDecoration decoration;
  11. final Widget child;
  12. final Duration duration;
  13. final Curve curve;
  14. final Duration reverseDuration;
  15. @override
  16. _AnimatedDecoratedBox1State createState() => _AnimatedDecoratedBox1State();
  17. }
  18. class _AnimatedDecoratedBox1State extends State<AnimatedDecoratedBox1>
  19. with SingleTickerProviderStateMixin {
  20. @protected
  21. AnimationController get controller => _controller;
  22. AnimationController _controller;
  23. Animation<double> get animation => _animation;
  24. Animation<double> _animation;
  25. DecorationTween _tween;
  26. @override
  27. Widget build(BuildContext context) {
  28. return AnimatedBuilder(
  29. animation: _animation,
  30. builder: (context, child){
  31. return DecoratedBox(
  32. decoration: _tween.animate(_animation).value,
  33. child: child,
  34. );
  35. },
  36. child: widget.child,
  37. );
  38. }
  39. @override
  40. void initState() {
  41. super.initState();
  42. _controller = AnimationController(
  43. duration: widget.duration,
  44. reverseDuration: widget.reverseDuration,
  45. vsync: this,
  46. );
  47. _tween = DecorationTween(begin: widget.decoration);
  48. _updateCurve();
  49. }
  50. void _updateCurve() {
  51. if (widget.curve != null)
  52. _animation = CurvedAnimation(parent: _controller, curve: widget.curve);
  53. else
  54. _animation = _controller;
  55. }
  56. @override
  57. void didUpdateWidget(AnimatedDecoratedBox1 oldWidget) {
  58. super.didUpdateWidget(oldWidget);
  59. if (widget.curve != oldWidget.curve)
  60. _updateCurve();
  61. _controller.duration = widget.duration;
  62. _controller.reverseDuration = widget.reverseDuration;
  63. if(widget.decoration!= (_tween.end ?? _tween.begin)){
  64. _tween
  65. ..begin = _tween.evaluate(_animation)
  66. ..end = widget.decoration;
  67. _controller
  68. ..value = 0.0
  69. ..forward();
  70. }
  71. }
  72. @override
  73. void dispose() {
  74. _controller.dispose();
  75. super.dispose();
  76. }
  77. }

下面我們來使用AnimatedDecoratedBox1來實現(xiàn)按鈕點擊后背景色從藍色過渡到紅色的效果:

  1. Color _decorationColor = Colors.blue;
  2. var duration = Duration(seconds: 1);
  3. ...//省略無關(guān)代碼
  4. AnimatedDecoratedBox(
  5. duration: duration,
  6. decoration: BoxDecoration(color: _decorationColor),
  7. child: FlatButton(
  8. onPressed: () {
  9. setState(() {
  10. _decorationColor = Colors.red;
  11. });
  12. },
  13. child: Text(
  14. "AnimatedDecoratedBox",
  15. style: TextStyle(color: Colors.white),
  16. ),
  17. ),
  18. )

點擊前效果如圖9-8所示,點擊后截取了過渡過程的一幀如圖9-9所示: ![img] 點擊后,按鈕背景色會從藍色向紅色過渡,圖9-9是過渡過程中的一幀,有點偏紫色,整個過渡動畫結(jié)束后背景會變?yōu)榧t色。

上面的代碼雖然實現(xiàn)了我們期望的功能,但是代碼卻比較復(fù)雜。稍加思考后,我們就可以發(fā)現(xiàn),AnimationController的管理以及 Tween 更新部分的代碼都是可以抽象出來的,如果我們這些通用邏輯封裝成基類,那么要實現(xiàn)動畫過渡組件只需要繼承這些基類,然后定制自身不同的代碼(比如動畫每一幀的構(gòu)建方法)即可,這樣將會簡化代碼。

為了方便開發(fā)者來實現(xiàn)動畫過渡組件的封裝,F(xiàn)lutter 提供了一個ImplicitlyAnimatedWidget抽象類,它繼承自 StatefulWidget,同時提供了一個對應(yīng)的ImplicitlyAnimatedWidgetState類,AnimationController的管理就在ImplicitlyAnimatedWidgetState類中。開發(fā)者如果要封裝動畫,只需要分別繼承ImplicitlyAnimatedWidgetImplicitlyAnimatedWidgetState類即可,下面我們演示一下具體如何實現(xiàn)。

我們需要分兩步實現(xiàn):

  1. 繼承ImplicitlyAnimatedWidget類。

  1. class AnimatedDecoratedBox extends ImplicitlyAnimatedWidget {
  2. AnimatedDecoratedBox({
  3. Key key,
  4. @required this.decoration,
  5. this.child,
  6. Curve curve = Curves.linear, //動畫曲線
  7. @required Duration duration, // 正向動畫執(zhí)行時長
  8. Duration reverseDuration, // 反向動畫執(zhí)行時長
  9. }) : super(
  10. key: key,
  11. curve: curve,
  12. duration: duration,
  13. reverseDuration: reverseDuration,
  14. );
  15. final BoxDecoration decoration;
  16. final Widget child;
  17. @override
  18. _AnimatedDecoratedBoxState createState() {
  19. return _AnimatedDecoratedBoxState();
  20. }
  21. }

其中curveduration、reverseDuration三個屬性在ImplicitlyAnimatedWidget中已定義。 可以看到AnimatedDecoratedBox類和普通繼承自StatefulWidget的類沒有什么不同。

  1. State類繼承自AnimatedWidgetBaseState(該類繼承自ImplicitlyAnimatedWidgetState類)。

  1. class _AnimatedDecoratedBoxState
  2. extends AnimatedWidgetBaseState<AnimatedDecoratedBox> {
  3. DecorationTween _decoration; //定義一個Tween
  4. @override
  5. Widget build(BuildContext context) {
  6. return DecoratedBox(
  7. decoration: _decoration.evaluate(animation),
  8. child: widget.child,
  9. );
  10. }
  11. @override
  12. void forEachTween(visitor) {
  13. // 在需要更新Tween時,基類會調(diào)用此方法
  14. _decoration = visitor(_decoration, widget.decoration,
  15. (value) => DecorationTween(begin: value));
  16. }
  17. }

可以看到我們實現(xiàn)了buildforEachTween兩個方法。在動畫執(zhí)行過程中,每一幀都會調(diào)用build方法(調(diào)用邏輯在ImplicitlyAnimatedWidgetState中),所以在build方法中我們需要構(gòu)建每一幀的DecoratedBox狀態(tài),因此得算出每一幀的decoration 狀態(tài),這個我們可以通過_decoration.evaluate(animation) 來算出,其中animationImplicitlyAnimatedWidgetState基類中定義的對象,_decoration是我們自定義的一個DecorationTween類型的對象,那么現(xiàn)在的問題就是它是在什么時候被賦值的呢?要回答這個問題,我們就得搞清楚什么時候需要對_decoration賦值。我們知道_decoration是一個 Tween,而 Tween的主要職責就是定義動畫的起始狀態(tài)(begin)和終止狀態(tài)(end)。對于AnimatedDecoratedBox來說,decoration的終止狀態(tài)就是用戶傳給它的值,而起始狀態(tài)是不確定的,有以下兩種情況:

  1. AnimatedDecoratedBox首次 build,此時直接將其decoration值置為起始狀態(tài),即_decoration值為DecorationTween(begin: decoration) 。
  2. AnimatedDecoratedBoxdecoration更新時,則起始狀態(tài)為_decoration.animate(animation),即_decoration值為DecorationTween(begin: _decoration.animate(animation),end:decoration)。

現(xiàn)在forEachTween的作用就很明顯了,它正是用于來更新 Tween 的初始值的,在上述兩種情況下會被調(diào)用,而開發(fā)者只需重寫此方法,并在此方法中更新 Tween 的起始狀態(tài)值即可。而一些更新的邏輯被屏蔽在了visitor回調(diào),我們只需要調(diào)用它并給它傳遞正確的參數(shù)即可,visitor方法簽名如下:

  1. Tween visitor(
  2. Tween<dynamic> tween, //當前的tween,第一次調(diào)用為null
  3. dynamic targetValue, // 終止狀態(tài)
  4. TweenConstructor<dynamic> constructor,//Tween構(gòu)造器,在上述三種情況下會被調(diào)用以更新tween
  5. );

可以看到,通過繼承ImplicitlyAnimatedWidgetImplicitlyAnimatedWidgetState類可以快速的實現(xiàn)動畫過渡組件的封裝,這和我們純手工實現(xiàn)相比,代碼簡化了很多。

如果讀者還有疑惑,建議查看ImplicitlyAnimatedWidgetState的源碼并結(jié)合本示例代碼對比理解。

#動畫過渡組件的反向動畫

在使用動畫過渡組件,我們只需要在改變一些屬性值后重新 build 組件即可,所以要實現(xiàn)狀態(tài)反向過渡,只需要將前后狀態(tài)值互換即可實現(xiàn),這本來是不需要再浪費筆墨的。但是ImplicitlyAnimatedWidget構(gòu)造函數(shù)中卻有一個reverseDuration屬性用于設(shè)置反向動畫的執(zhí)行時長,這貌似在告訴讀者ImplicitlyAnimatedWidget本身也提供了執(zhí)行反向動畫的接口,于是筆者查看了ImplicitlyAnimatedWidgetState源碼并未發(fā)現(xiàn)有執(zhí)行反向動畫的接口,唯一有用的是它暴露了控制動畫的controller。所以如果要讓reverseDuration生效,我們只能先獲取controller,然后再通過controller.reverse()來啟動反向動畫,比如我們在上面示例的基礎(chǔ)上實現(xiàn)一個循環(huán)的點擊背景顏色變換效果,要求從藍色變?yōu)榧t色時動畫執(zhí)行時間為 400ms,從紅變藍為2s,如果要使reverseDuration生效,我們需要這么做:

  1. AnimatedDecoratedBox(
  2. duration: Duration( milliseconds: 400),
  3. decoration: BoxDecoration(color: _decorationColor),
  4. reverseDuration: Duration(seconds: 2),
  5. child: Builder(builder: (context) {
  6. return FlatButton(
  7. onPressed: () {
  8. if (_decorationColor == Colors.red) {
  9. ImplicitlyAnimatedWidgetState _state =
  10. context.findAncestorStateOfType<ImplicitlyAnimatedWidgetState>();
  11. // 通過controller來啟動反向動畫
  12. _state.controller.reverse().then((e) {
  13. // 經(jīng)驗證必須調(diào)用setState來觸發(fā)rebuild,否則狀態(tài)同步會有問題
  14. setState(() {
  15. _decorationColor = Colors.blue;
  16. });
  17. });
  18. } else {
  19. setState(() {
  20. _decorationColor = Colors.red;
  21. });
  22. }
  23. },
  24. child: Text(
  25. "AnimatedDecoratedBox toggle",
  26. style: TextStyle(color: Colors.white),
  27. ),
  28. );
  29. }),
  30. )

上面的代碼實際上是非常糟糕且沒必要的,它需要我們了解ImplicitlyAnimatedWidgetState內(nèi)部實現(xiàn),并且要手動去啟動反向動畫。我們完全可以通過如下代碼實現(xiàn)相同的效果:

  1. AnimatedDecoratedBox(
  2. duration: Duration(
  3. milliseconds: _decorationColor == Colors.red ? 400 : 2000),
  4. decoration: BoxDecoration(color: _decorationColor),
  5. child: Builder(builder: (context) {
  6. return FlatButton(
  7. onPressed: () {
  8. setState(() {
  9. _decorationColor = _decorationColor == Colors.blue
  10. ? Colors.red
  11. : Colors.blue;
  12. });
  13. },
  14. child: Text(
  15. "AnimatedDecoratedBox toggle",
  16. style: TextStyle(color: Colors.white),
  17. ),
  18. );
  19. }),
  20. )

這樣的代碼是不是優(yōu)雅的多!那么現(xiàn)在問題來了,為什么ImplicitlyAnimatedWidgetState要提供一個reverseDuration參數(shù)呢?筆者仔細研究了ImplicitlyAnimatedWidgetState的實現(xiàn),發(fā)現(xiàn)唯一的解釋就是該參數(shù)并非是給ImplicitlyAnimatedWidgetState用的,而是給子類用的!原因正如我們前面說的,要使reverseDuration 有用就必須得獲取controller 屬性來手動啟動反向動畫,ImplicitlyAnimatedWidgetState中的controller 屬性是一個保護屬性,定義如下:

  1. @protected
  2. AnimationController get controller => _controller;

而保護屬性原則上只應(yīng)該在子類中使用,而不應(yīng)該像上面示例代碼一樣在外部使用。綜上,我們可以得出兩條結(jié)論:

  1. 使用動畫過渡組件時如果需要執(zhí)行反向動畫的場景,應(yīng)盡量使用狀態(tài)互換的方法,而不應(yīng)該通過獲取ImplicitlyAnimatedWidgetStatecontroller的方式。

  1. 如果我們自定義的動畫過渡組件用不到reverseDuration ,那么最好就不要暴露此參數(shù),比如我們上面自定義的AnimatedDecoratedBox定義中就可以去除reverseDuration 可選參數(shù),如:

  1. class AnimatedDecoratedBox extends ImplicitlyAnimatedWidget {
  2. AnimatedDecoratedBox({
  3. Key key,
  4. @required this.decoration,
  5. this.child,
  6. Curve curve = Curves.linear,
  7. @required Duration duration,
  8. }) : super(
  9. key: key,
  10. curve: curve,
  11. duration: duration,
  12. );

#9.7.2 Flutter 預(yù)置的動畫過渡組件

Flutter SDK 中也預(yù)置了很多動畫過渡組件,實現(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 預(yù)置的動畫過渡組件

下面我們通過一個示例來感受一下這些預(yù)置的動畫過渡組件效果:

  1. import 'package:flutter/material.dart';
  2. class AnimatedWidgetsTest extends StatefulWidget {
  3. @override
  4. _AnimatedWidgetsTestState createState() => _AnimatedWidgetsTestState();
  5. }
  6. class _AnimatedWidgetsTestState extends State<AnimatedWidgetsTest> {
  7. double _padding = 10;
  8. var _align = Alignment.topRight;
  9. double _height = 100;
  10. double _left = 0;
  11. Color _color = Colors.red;
  12. TextStyle _style = TextStyle(color: Colors.black);
  13. Color _decorationColor = Colors.blue;
  14. @override
  15. Widget build(BuildContext context) {
  16. var duration = Duration(seconds: 5);
  17. return SingleChildScrollView(
  18. child: Column(
  19. children: <Widget>[
  20. RaisedButton(
  21. onPressed: () {
  22. setState(() {
  23. _padding = 20;
  24. });
  25. },
  26. child: AnimatedPadding(
  27. duration: duration,
  28. padding: EdgeInsets.all(_padding),
  29. child: Text("AnimatedPadding"),
  30. ),
  31. ),
  32. SizedBox(
  33. height: 50,
  34. child: Stack(
  35. children: <Widget>[
  36. AnimatedPositioned(
  37. duration: duration,
  38. left: _left,
  39. child: RaisedButton(
  40. onPressed: () {
  41. setState(() {
  42. _left = 100;
  43. });
  44. },
  45. child: Text("AnimatedPositioned"),
  46. ),
  47. )
  48. ],
  49. ),
  50. ),
  51. Container(
  52. height: 100,
  53. color: Colors.grey,
  54. child: AnimatedAlign(
  55. duration: duration,
  56. alignment: _align,
  57. child: RaisedButton(
  58. onPressed: () {
  59. setState(() {
  60. _align = Alignment.center;
  61. });
  62. },
  63. child: Text("AnimatedAlign"),
  64. ),
  65. ),
  66. ),
  67. AnimatedContainer(
  68. duration: duration,
  69. height: _height,
  70. color: _color,
  71. child: FlatButton(
  72. onPressed: () {
  73. setState(() {
  74. _height = 150;
  75. _color = Colors.blue;
  76. });
  77. },
  78. child: Text(
  79. "AnimatedContainer",
  80. style: TextStyle(color: Colors.white),
  81. ),
  82. ),
  83. ),
  84. AnimatedDefaultTextStyle(
  85. child: GestureDetector(
  86. child: Text("hello world"),
  87. onTap: () {
  88. setState(() {
  89. _style = TextStyle(
  90. color: Colors.blue,
  91. decorationStyle: TextDecorationStyle.solid,
  92. decorationColor: Colors.blue,
  93. );
  94. });
  95. },
  96. ),
  97. style: _style,
  98. duration: duration,
  99. ),
  100. AnimatedDecoratedBox(
  101. duration: duration,
  102. decoration: BoxDecoration(color: _decorationColor),
  103. child: FlatButton(
  104. onPressed: () {
  105. setState(() {
  106. _decorationColor = Colors.red;
  107. });
  108. },
  109. child: Text(
  110. "AnimatedDecoratedBox",
  111. style: TextStyle(color: Colors.white),
  112. ),
  113. ),
  114. )
  115. ].map((e) {
  116. return Padding(
  117. padding: EdgeInsets.symmetric(vertical: 16),
  118. child: e,
  119. );
  120. }).toList(),
  121. ),
  122. );
  123. }
  124. }

運行后效果如圖9-10所示:

圖9-10

讀者可以點擊一下相應(yīng)組件來查看一下實際的運行效果。

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號