鏈接:https:///post/5ca375f3e51d451a18362e2a
在 2019 年的谷歌 I/O 大會上,開發(fā)團(tuán)隊(duì)發(fā)布了 Flutter for web 的首個技術(shù)預(yù)覽版,宣布 Flutter 正在為包括 Google Home Hub 在內(nèi)的 Google 智能顯示平臺提供支持,并通過結(jié)合 Chrome OS 為桌面級應(yīng)用程序提供支持邁出第一步。 
一張圖感受下 本篇作者給大家展示了一個如何用Flutter做炫酷動畫!可能很多同學(xué)對 Flutter 還不了解,沒關(guān)系,可以通過全文類比于 Android 上制作動畫的區(qū)別與相似之處! 前言這一段時間,F(xiàn)lutter的勢頭是越來越猛了,作為一個Android程序猿,我自然也是想要趕緊嘗試一把。在學(xué)習(xí)到動畫的這部分后,為了加深對Flutter動畫實(shí)現(xiàn)的理解,我決定把之前寫的一個卡片切換效果的開源小項(xiàng)目,用Flutter“翻譯”一遍。 廢話不多說,先來看看效果吧: Android: 
IOS

Github地址: https://github.com/BakerJQ/Flutter-InfiniteCards 思路首先,關(guān)于卡片的層疊效果,在原Android項(xiàng)目中,是通過Scale差異以及TranslationY來體現(xiàn)的,F(xiàn)lutter可以繼續(xù)采用這種方式。
其次,對于自定義卡片的內(nèi)容,原Android項(xiàng)目是通過Adapter實(shí)現(xiàn),對于Flutter,則可以采用IndexedWidgetBuilder實(shí)現(xiàn)。 最后,就是自定義動效的實(shí)現(xiàn),原Android項(xiàng)目是通過一個0到1的ValueAnimator來定義動畫的展示過程,而Flutter中,正好有與之對應(yīng)的Animation和AnimationController,如此我們就可以直接自定義一個動畫過程中,具體的視圖展示方式。 組件總覽由于卡片視圖需要根據(jù)動畫情況進(jìn)行渲染,所以顯然是一個StatefulWidget。 同時,我們給出三種基本的動畫模式: enum AnimType { TO_FRONT,//被選中的卡片通過自定義動效移至第一,其他的卡片通過通用動效補(bǔ)位 SWITCH,//選中的卡片和第一張卡片互換位置,并都是自定義動效 TO_END,//第一張圖片通過自定義動效移至最后,其他卡片通過通用動效補(bǔ)位 }
并通過Helper和Controller來處理所有的動畫邏輯 其中Controller由構(gòu)造方法傳入 InfiniteCards({ @required this.controller, this.width, this.height, this.background, });
Helper在initState中進(jìn)行構(gòu)建,并初始化,同時將Helper綁定給Controller: @override void initState() { ... _helper = AnimHelper( controller: widget.controller, //傳入動畫更新監(jiān)聽,動畫時調(diào)用setState進(jìn)行實(shí)時渲染 listenerForSetState: () { setState(() {}); }); _helper.init(this, context); if (widget.controller != null) { widget.controller.animHelper = _helper; } }
而build過程中,則通過Helper返回具體的Widget列表,而Stack則是為了實(shí)現(xiàn)層疊效果。 Widget build(BuildContext context) { ... return Container( ... child: Stack( children: _helper.getCardList(_width, _height), ), ); }
如此,基本的初始化等操作就算是完成了。下面我們來看看Controller和Helper都是怎么工作的。 Controller我們先來看看Controller所包含的內(nèi)容: class InfiniteCardsController { //卡片構(gòu)造器 IndexedWidgetBuilder _itemBuilder; //卡片個數(shù) int _itemCount; //動畫時長 Duration _animDuration; //點(diǎn)擊卡片是否觸發(fā)切換動畫 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(...) { ... //重設(shè)各參數(shù) setControllerParams(); _animHelper.reset(); ... } }
由此可以看到,Controller基本上就是作為參數(shù)配置器和Helper的方法代理的存在。由此童鞋們肯定就知道了,對于動效的自定義和動效的觸發(fā)等操作,都是通過Controller來完成,demo如下: //構(gòu)建Controller _controller = InfiniteCardsController( itemBuilder: _renderItem, itemCount: 5, animType: AnimType.SWITCH, ); //調(diào)用reset _controller.reset( itemCount: 4, animType: AnimType.TO_FRONT, transformToBack: _customToBackTransform, ); //調(diào)用展示下一張卡片動畫 _controller.reset(animType: AnimType.TO_END); _controller.next();
關(guān)于具體的自定義,我們稍后再聊,咱們先來看看Helper。 HelperHelper是整個動畫效果實(shí)現(xiàn)的核心類,我們先看幾個它所包含的核心成員: class AnimHelper { final InfiniteCardsController controller; //切換動畫 AnimationController _animationController; Animation<double> _animation; //卡片列表 List<CardItem> _cardList = new List(); //需要向后切換的卡片,和需要向前切換的卡片 CardItem _cardToBack, _cardToFront; //需要向后切換的卡片位置,和需要向前切換的卡片位置 int _positionToBack, _positionToFront; }
現(xiàn)在我們來看看,如果要觸發(fā)一個切換動畫,這些成員是如何相互配合的。 當(dāng)選中一張卡片進(jìn)行切換時,這張卡片就是需要向前切換的卡片(ToFront),而第一張卡片,就是需要向后切換的卡片(ToBack)。 void _cardAnim(int index, CardItem card) { //記錄要切換的卡片 _cardToFront = card; _cardToBack = _cardList[0]; _positionToBack = 0; _positionToFront = index; //觸發(fā)動畫 _animationController.forward(from: 0.0); }
由于設(shè)置了AnimationListener,在動畫過程中,setState就會被調(diào)用,如此就會觸發(fā)Widget的build,從而觸發(fā)Helper的getCardList方法。 我們來看看在切換動畫的過程中,是如何返回卡片Widget列表的。 List<Widget> getCardList(double width, double height) { for (int i = 0; i < controller.itemCount; i++) { ... if (_isSwitchAnim) { //處理切換動畫 _switchTransform(width, height, i); } ... } //根據(jù)zIndex進(jìn)行排序渲染 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(); }
如上代碼所示,先進(jìn)行動畫處理,后根據(jù)zIndex進(jìn)行排序,因?yàn)橐WC在前面的后渲染。 而動畫是如何處理的呢,以切換到前面的卡片為例: 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。 由此,動畫展示的基本流程就描述完了,下面我們進(jìn)入最關(guān)鍵的部分--如何自定義動畫。 自定義動畫我們以通用動畫為例,來看看自定義動畫的主要流程。 首先,AnimTransform為如下方法的定義: typedef AnimTransform = Transform Function( Widget item,//卡片原始Widget double fraction,//動畫執(zhí)行的系數(shù) double curveFraction,//曲線轉(zhuǎn)換后的系數(shù) double cardHeight,//整體高度 double cardWidth,//整體寬度 int fromPosition,//卡片開始位置 int toPosition);//卡片要移動到的位置
該方法返回的是一個Transform,專門用于處理視圖變換的Widget,而我們要做的,就是根據(jù)傳入的參數(shù),構(gòu)建相應(yīng)系數(shù)下的Widget。 以DefaultCommonTransform為例: Transform _defaultCommonTransform(Widget item, double fraction, double curveFraction, double cardHeight, double cardWidth, int fromPosition, int toPosition) //需要跨越的卡片數(shù)量{ int positionCount = fromPosition - toPosition; //以0.8做為第一張的縮放尺寸,每向后一張縮小0.1 //(0.8 - 0.1 * fromPosition) = 當(dāng)前位置的縮放尺寸 //(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); //返回縮放后,進(jìn)行Y方向偏移的Widget return Transform.translate( offset: Offset(0, translationY), child: Transform.scale( scale: scale, child: item, ), ); }
對于向第一位移動的選中卡片,也是同理,只不過是根據(jù)該卡片對應(yīng)的轉(zhuǎn)換器來進(jìn)行自定義動畫的轉(zhuǎn)換。 最后的效果,就像演示圖中第一次點(diǎn)擊,圖片向前翻轉(zhuǎn)到第一位的效果一樣。 總結(jié)由于Flutter采用的是聲明式的視圖構(gòu)建方式,在編碼初期,多少會受到原生編碼方式的思維影響,而覺得很難受。但是在熟悉了之后,就會發(fā)現(xiàn)其實(shí)很多思想都是共通的,比如Animation,比如插值器的概念等等。 另外,研讀源碼仍然是最有效的解決問題的方式,比如相比Android中直接對ScrollView進(jìn)行animateTo操作,在Flutter中需要通過ScrollController進(jìn)行animateTo操作,正是這一點(diǎn)讓我找到了在Flutter中實(shí)現(xiàn)InfiniteCards效果的方法。 更具體的Demo請前往Github的Flutter-InfiniteCards Repo,歡迎大家star和提issue。 再次貼一下Github地址: https://github.com/BakerJQ/Flutter-InfiniteCards 作者也實(shí)現(xiàn)了 Android 上相同效果,歡迎一起學(xué)習(xí): https://github.com/BakerJQ/Android-InfiniteCards
|