Flutter自定義實現神奇動效的卡片切換視圖的示例代碼

這篇文章主要介紹了Flutter自定義實現神奇動效的卡片切換視圖的示例代碼,小編覺得挺不錯的,現在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧

前言

這一段時間,Flutter的勢頭是越來越猛了,作爲一個Android程序猿,我自然也是想要趕緊嘗試一把。在學習到動畫的這部分後,爲了加深對Flutter動畫實現的理解,我決定把之前寫的一個卡片切換效果的開源小項目,用Flutter“翻譯”一遍。

廢話不多說,先來看看效果吧:

Android

iOS

Github地址:https://github.com/BakerJQ/Flutter-InfiniteCards

思路

首先,關於卡片的層疊效果,在原Android項目中,是通過Scale差異以及TranslationY來體現的,Flutter可以繼續採用這種方式。

其次,對於自定義卡片的內容,原Android項目是通過Adapter實現,對於Flutter,則可以採用IndexedWidgetBuilder實現。

最後,就是自定義動效的實現,原Android項目是通過一個0到1的ValueAnimator來定義動畫的展示過程,而Flutter中,正好有與之對應的Animation和AnimationController,如此我們就可以直接自定義一個動畫過程中,具體的視圖展示方式。

組件總覽

由於卡片視圖需要根據動畫情況進行渲染,所以顯然是一個StatefulWidget。

同時,我們給出三種基本的動畫模式:

enum AnimType {
 TO_FRONT,//被選中的卡片通過自定義動效移至第一,其他的卡片通過通用動效補位
 SWITCH,//選中的卡片和第一張卡片互換位置,並都是自定義動效
 TO_END,//第一張圖片通過自定義動效移至最後,其他卡片通過通用動效補位
}

並通過Helper和Controller來處理所有的動畫邏輯

其中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

我們先來看看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

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/Flutter-InfiniteCards

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持神馬文庫。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章