溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶(hù)服務(wù)條款》

Flutter中如何自定義實(shí)現(xiàn)神奇動(dòng)效的卡片切換視圖

發(fā)布時(shí)間:2021-07-22 14:42:44 來(lái)源:億速云 閱讀:254 作者:小新 欄目:移動(dòng)開(kāi)發(fā)

小編給大家分享一下Flutter中如何自定義實(shí)現(xiàn)神奇動(dòng)效的卡片切換視圖,相信大部分人都還不怎么了解,因此分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后大有收獲,下面讓我們一起去了解一下吧!

先來(lái)看看效果吧:

Android

Flutter中如何自定義實(shí)現(xiàn)神奇動(dòng)效的卡片切換視圖

iOS

Flutter中如何自定義實(shí)現(xiàn)神奇動(dòng)效的卡片切換視圖

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

思路

首先,關(guān)于卡片的層疊效果,在原Android項(xiàng)目中,是通過(guò)Scale差異以及TranslationY來(lái)體現(xiàn)的,F(xiàn)lutter可以繼續(xù)采用這種方式。

其次,對(duì)于自定義卡片的內(nèi)容,原Android項(xiàng)目是通過(guò)Adapter實(shí)現(xiàn),對(duì)于Flutter,則可以采用IndexedWidgetBuilder實(shí)現(xiàn)。

最后,就是自定義動(dòng)效的實(shí)現(xiàn),原Android項(xiàng)目是通過(guò)一個(gè)0到1的ValueAnimator來(lái)定義動(dòng)畫(huà)的展示過(guò)程,而Flutter中,正好有與之對(duì)應(yīng)的Animation和AnimationController,如此我們就可以直接自定義一個(gè)動(dòng)畫(huà)過(guò)程中,具體的視圖展示方式。

組件總覽

由于卡片視圖需要根據(jù)動(dòng)畫(huà)情況進(jìn)行渲染,所以顯然是一個(gè)StatefulWidget。

同時(shí),我們給出三種基本的動(dòng)畫(huà)模式:

enum AnimType {
 TO_FRONT,//被選中的卡片通過(guò)自定義動(dòng)效移至第一,其他的卡片通過(guò)通用動(dòng)效補(bǔ)位
 SWITCH,//選中的卡片和第一張卡片互換位置,并都是自定義動(dòng)效
 TO_END,//第一張圖片通過(guò)自定義動(dòng)效移至最后,其他卡片通過(guò)通用動(dòng)效補(bǔ)位
}

并通過(guò)Helper和Controller來(lái)處理所有的動(dòng)畫(huà)邏輯

其中Controller由構(gòu)造方法傳入

InfiniteCards({
 @required this.controller,
 this.width,
 this.height,
 this.background,
});

Helper在initState中進(jìn)行構(gòu)建,并初始化,同時(shí)將Helper綁定給Controller:

@override
void initState() {
 ...
 _helper = AnimHelper(
   controller: widget.controller,
   //傳入動(dòng)畫(huà)更新監(jiān)聽(tīng),動(dòng)畫(huà)時(shí)調(diào)用setState進(jìn)行實(shí)時(shí)渲染
   listenerForSetState: () {
    setState(() {});
   });
 _helper.init(this, context);
 if (widget.controller != null) {
   widget.controller.animHelper = _helper;
 }
}

而build過(guò)程中,則通過(guò)Helper返回具體的Widget列表,而Stack則是為了實(shí)現(xiàn)層疊效果。

Widget build(BuildContext context) {
 ...
 return Container(
  ...
  child: Stack(
   children: _helper.getCardList(_width, _height),
  ),
 );
}

如此,基本的初始化等操作就算是完成了。下面我們來(lái)看看Controller和Helper都是怎么工作的。

Controller

我們先來(lái)看看Controller所包含的內(nèi)容:

class InfiniteCardsController {
 //卡片構(gòu)造器
 IndexedWidgetBuilder _itemBuilder;
 //卡片個(gè)數(shù)
 int _itemCount;
 //動(dòng)畫(huà)時(shí)長(zhǎng)
 Duration _animDuration;
 //點(diǎn)擊卡片是否觸發(fā)切換動(dòng)畫(huà)
 bool _clickItemToSwitch;
 //動(dòng)畫(huà)Transform
 AnimTransform _transformToFront,_transformToBack,...;
 //排序Transform
 ZIndexTransform _zIndexTransformCommon,...;
 //動(dòng)畫(huà)類(lèi)型
 AnimType _animType;
 //曲線(xiàn)定義(類(lèi)Android插值器)
 Curve _curve;
 //helper
 AnimHelper _animHelper;
 ...
 void anim(int index) {
  _animHelper.anim(index);
 }
 void reset(...) {
  ...
  //重設(shè)各參數(shù)
  setControllerParams();
  _animHelper.reset(); 
  ...
 }
}

由此可以看到,Controller基本上就是作為參數(shù)配置器和Helper的方法代理的存在。由此童鞋們肯定就知道了,對(duì)于動(dòng)效的自定義和動(dòng)效的觸發(fā)等操作,都是通過(guò)Controller來(lái)完成,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)用展示下一張卡片動(dòng)畫(huà)
_controller.reset(animType: AnimType.TO_END);
_controller.next();

關(guān)于具體的自定義,我們稍后再聊,咱們先來(lái)看看Helper。

Helper

Helper是整個(gè)動(dòng)畫(huà)效果實(shí)現(xiàn)的核心類(lèi),我們先看幾個(gè)它所包含的核心成員:

class AnimHelper {
 final InfiniteCardsController controller;
 //切換動(dòng)畫(huà)
 AnimationController _animationController;
 Animation<double> _animation;
 //卡片列表
 List<CardItem> _cardList = new List();
 //需要向后切換的卡片,和需要向前切換的卡片
 CardItem _cardToBack, _cardToFront;
 //需要向后切換的卡片位置,和需要向前切換的卡片位置
 int _positionToBack, _positionToFront;
}

現(xiàn)在我們來(lái)看看,如果要觸發(fā)一個(gè)切換動(dòng)畫(huà),這些成員是如何相互配合的。

當(dāng)選中一張卡片進(jìn)行切換時(shí),這張卡片就是需要向前切換的卡片(ToFront),而第一張卡片,就是需要向后切換的卡片(ToBack)。

void _cardAnim(int index, CardItem card) {
 //記錄要切換的卡片
 _cardToFront = card;
 _cardToBack = _cardList[0];
 _positionToBack = 0;
 _positionToFront = index;
 //觸發(fā)動(dòng)畫(huà)
 _animationController.forward(from: 0.0);
}

由于設(shè)置了AnimationListener,在動(dòng)畫(huà)過(guò)程中,setState就會(huì)被調(diào)用,如此就會(huì)觸發(fā)Widget的build,從而觸發(fā)Helper的getCardList方法。我們來(lái)看看在切換動(dòng)畫(huà)的過(guò)程中,是如何返回卡片Widget列表的。

List<Widget> getCardList(double width, double height) {
 for (int i = 0; i < controller.itemCount; i++) {
  ...
  if (_isSwitchAnim) {
   //處理切換動(dòng)畫(huà)
   _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)行動(dòng)畫(huà)處理,后根據(jù)zIndex進(jìn)行排序,因?yàn)橐WC在前面的后渲染。

而動(dòng)畫(huà)是如何處理的呢,以切換到前面的卡片為例:

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);
 }

原來(lái),正是在這一步,Helper通過(guò)Controller中配置的自定義動(dòng)畫(huà)方法,得到了卡片的Widget。

由此,動(dòng)畫(huà)展示的基本流程就描述完了,下面我們進(jìn)入最關(guān)鍵的部分--如何自定義動(dòng)畫(huà)。

自定義動(dòng)畫(huà)

我們以通用動(dòng)畫(huà)為例,來(lái)看看自定義動(dòng)畫(huà)的主要流程。

首先,AnimTransform為如下方法的定義:

typedef AnimTransform = Transform Function(
  Widget item,//卡片原始Widget
  double fraction,//動(dòng)畫(huà)執(zhí)行的系數(shù)
  double curveFraction,//曲線(xiàn)轉(zhuǎn)換后的系數(shù)
  double cardHeight,//整體高度
  double cardWidth,//整體寬度
  int fromPosition,//卡片開(kāi)始位置
  int toPosition);//卡片要移動(dòng)到的位置

該方法返回的是一個(gè)Transform,專(zhuān)門(mén)用于處理視圖變換的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) = 移動(dòng)過(guò)程中需要改變的縮放尺寸 
 double scale = (0.8 - 0.1 * fromPosition) + (0.1 * fraction * positionCount);
 //在Y方向的偏移量,每向后一張,向上偏移卡片寬度的0.02
 //-cardHeight * (0.8 - scale) * 0.5 對(duì)卡片做整體居中處理
 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,
  ),
 );
}

對(duì)于向第一位移動(dòng)的選中卡片,也是同理,只不過(guò)是根據(jù)該卡片對(duì)應(yīng)的轉(zhuǎn)換器來(lái)進(jìn)行自定義動(dòng)畫(huà)的轉(zhuǎn)換。

最后的效果,就像演示圖中第一次點(diǎn)擊,圖片向前翻轉(zhuǎn)到第一位的效果一樣。

以上是“Flutter中如何自定義實(shí)現(xiàn)神奇動(dòng)效的卡片切換視圖”這篇文章的所有內(nèi)容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內(nèi)容對(duì)大家有所幫助,如果還想學(xué)習(xí)更多知識(shí),歡迎關(guān)注億速云行業(yè)資訊頻道!

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀(guān)點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI