這一段時間,Flutter的勢頭是愈來愈猛了,做爲一個Android程序猿,我天然也是想要趕忙嘗試一把。在學習到動畫的這部分後,爲了加深對Flutter動畫實現的理解,我決定把以前寫的一個卡片切換效果的開源小項目,用Flutter「翻譯」一遍。git
廢話很少說,先來看看效果吧:github
Android | iOS |
---|---|
Github地址:https://github.com/BakerJQ/Fl...ide
首先,關於卡片的層疊效果,在原Android項目中,是經過Scale差別以及TranslationY來體現的,Flutter能夠繼續採用這種方式。學習
其次,對於自定義卡片的內容,原Android項目是經過Adapter實現,對於Flutter,則能夠採用IndexedWidgetBuilder實現。動畫
最後,就是自定義動效的實現,原Android項目是經過一個0到1的ValueAnimator來定義動畫的展現過程,而Flutter中,正好有與之對應的Animation和AnimationController,如此咱們就能夠直接自定義一個動畫過程當中,具體的視圖展現方式。ui
因爲卡片視圖須要根據動畫狀況進行渲染,因此顯然是一個StatefulWidget。this
同時,咱們給出三種基本的動畫模式:編碼
enum AnimType { TO_FRONT,//被選中的卡片經過自定義動效移至第一,其餘的卡片經過通用動效補位 SWITCH,//選中的卡片和第一張卡片互換位置,並都是自定義動效 TO_END,//第一張圖片經過自定義動效移至最後,其餘卡片經過通用動效補位 }
並經過Helper和Controller來處理全部的動畫邏輯spa
其中Controller由構造方法傳入翻譯
InfiniteCards({ @required this.controller, this.width, this.height, this.background, });
Helper在initState中進行構建,並初始化,同時將Helper綁定給Controller:
@override void initState() { ... _helper = AnimHelper( controller: widget.controller, //傳入動畫更新監聽,動畫時調用setState進行實時渲染 listenerForSetState: () { setState(() {}); }); _helper.init(this, context); if (widget.controller != null) { widget.controller.animHelper = _helper; } }
而build過程當中,則經過Helper返回具體的Widget列表,而Stack則是爲了實現層疊效果。
Widget build(BuildContext context) { ... return Container( ... child: Stack( children: _helper.getCardList(_width, _height), ), ); }
如此,基本的初始化等操做就算是完成了。下面咱們來看看Controller和Helper都是怎麼工做的。
咱們先來看看Controller所包含的內容:
class InfiniteCardsController { //卡片構造器 IndexedWidgetBuilder _itemBuilder; //卡片個數 int _itemCount; //動畫時長 Duration _animDuration; //點擊卡片是否觸發切換動畫 bool _clickItemToSwitch; //動畫Transform AnimTransform _transformToFront,_transformToBack,...; //排序Transform ZIndexTransform _zIndexTransformCommon,...; //動畫類型 AnimType _animType; //曲線定義(類Android插值器) Curve _curve; //helper AnimHelper _animHelper; ... void anim(int index) { _animHelper.anim(index); } void reset(...) { ... //重設各參數 setControllerParams(); _animHelper.reset(); ... } }
由此能夠看到,Controller基本上就是做爲參數配置器和Helper的方法代理的存在。由此童鞋們確定就知道了,對於動效的自定義和動效的觸發等操做,都是經過Controller來完成,demo以下:
//構建Controller _controller = InfiniteCardsController( itemBuilder: _renderItem, itemCount: 5, animType: AnimType.SWITCH, ); //調用reset _controller.reset( itemCount: 4, animType: AnimType.TO_FRONT, transformToBack: _customToBackTransform, ); //調用展現下一張卡片動畫 _controller.reset(animType: AnimType.TO_END); _controller.next();
關於具體的自定義,咱們稍後再聊,我們先來看看Helper。
Helper是整個動畫效果實現的核心類,咱們先看幾個它所包含的核心成員:
class AnimHelper { final InfiniteCardsController controller; //切換動畫 AnimationController _animationController; Animation<double> _animation; //卡片列表 List<CardItem> _cardList = new List(); //須要向後切換的卡片,和須要向前切換的卡片 CardItem _cardToBack, _cardToFront; //須要向後切換的卡片位置,和須要向前切換的卡片位置 int _positionToBack, _positionToFront; }
如今咱們來看看,若是要觸發一個切換動畫,這些成員是如何相互配合的。
當選中一張卡片進行切換時,這張卡片就是須要向前切換的卡片(ToFront),而第一張卡片,就是須要向後切換的卡片(ToBack)。
void _cardAnim(int index, CardItem card) { //記錄要切換的卡片 _cardToFront = card; _cardToBack = _cardList[0]; _positionToBack = 0; _positionToFront = index; //觸發動畫 _animationController.forward(from: 0.0); }
因爲設置了AnimationListener,在動畫過程當中,setState就會被調用,如此就會觸發Widget的build,從而觸發Helper的getCardList方法。咱們來看看在切換動畫的過程當中,是如何返回卡片Widget列表的。
List<Widget> getCardList(double width, double height) { for (int i = 0; i < controller.itemCount; i++) { ... if (_isSwitchAnim) { //處理切換動畫 _switchTransform(width, height, i); } ... } //根據zIndex進行排序渲染 List<CardItem> copy = List.from(_cardList); copy.sort((card1, card2) { return card1.zIndex < card2.zIndex ? 1 : -1; }); return copy.map((card) { return card.transformWidget; }).toList(); }
如上代碼所示,先進行動畫處理,後根據zIndex進行排序,由於要保證在前面的後渲染。
而動畫是如何處理的呢,以切換到前面的卡片爲例:
void _toFrontTransform(double width, double height, int fromPosition, int toPosition) { CardItem cardItem = _cardList[fromPosition]; controller.zIndexTransformToFront( cardItem, _animation.value, _getCurveValue(_animation.value), width, height, fromPosition, toPosition); cardItem.transformWidget = controller.transformToFront( cardItem.widget, _animation.value, _getCurveValue(_animation.value), width, height, fromPosition, toPosition); }
原來,正是在這一步,Helper經過Controller中配置的自定義動畫方法,獲得了卡片的Widget。
由此,動畫展現的基本流程就描述完了,下面咱們進入最關鍵的部分--如何自定義動畫。
咱們以通用動畫爲例,來看看自定義動畫的主要流程。
首先,AnimTransform爲以下方法的定義:
typedef AnimTransform = Transform Function( Widget item,//卡片原始Widget double fraction,//動畫執行的係數 double curveFraction,//曲線轉換後的係數 double cardHeight,//總體高度 double cardWidth,//總體寬度 int fromPosition,//卡片開始位置 int toPosition);//卡片要移動到的位置
該方法返回的是一個Transform,專門用於處理視圖變換的Widget,而咱們要作的,就是根據傳入的參數,構建相應係數下的Widget。以DefaultCommonTransform爲例:
Transform _defaultCommonTransform(Widget item, double fraction, double curveFraction, double cardHeight, double cardWidth, int fromPosition, int toPosition) //須要跨越的卡片數量{ int positionCount = fromPosition - toPosition; //以0.8作爲第一張的縮放尺寸,每向後一張縮小0.1 //(0.8 - 0.1 * fromPosition) = 當前位置的縮放尺寸 //(0.1 * fraction * positionCount) = 移動過程當中須要改變的縮放尺寸 double scale = (0.8 - 0.1 * fromPosition) + (0.1 * fraction * positionCount); //在Y方向的偏移量,每向後一張,向上偏移卡片寬度的0.02 //-cardHeight * (0.8 - scale) * 0.5 對卡片作總體居中處理 double translationY = -cardHeight * (0.8 - scale) * 0.5 - cardHeight * (0.02 * fromPosition - 0.02 * fraction * positionCount); //返回縮放後,進行Y方向偏移的Widget return Transform.translate( offset: Offset(0, translationY), child: Transform.scale( scale: scale, child: item, ), ); }
對於向第一位移動的選中卡片,也是同理,只不過是根據該卡片對應的轉換器來進行自定義動畫的轉換。
最後的效果,就像演示圖中第一次點擊,圖片向前翻轉到第一位的效果同樣。
因爲Flutter採用的是聲明式的視圖構建方式,在編碼初期,多少會受到原生編碼方式的思惟影響,而以爲很難受。可是在熟悉了以後,就會發現其實不少思想都是共通的,好比Animation,好比插值器的概念等等。
另外,研讀源碼仍然是最有效的解決問題的方式,好比相比Android中直接對ScrollView進行animateTo操做,在Flutter中須要經過ScrollController進行animateTo操做,正是這一點讓我找到了在Flutter中實現InfiniteCards效果的方法。
更具體的Demo請前往Github的Flutter-InfiniteCards Repo,歡迎你們star和提issue。
再次貼一下Github地址:https://github.com/BakerJQ/Fl...