【文末送書】Flutter自定義實現神奇動效的卡片切換視圖


做者:BakerJQ, 連接:https://juejin.im/post/5ca375f3e51d451a18362e2agit

前言

這一段時間,Flutter的勢頭是愈來愈猛了,做爲一個Android程序猿,我天然也是想要趕忙嘗試一把。在學習到動畫的這部分後,爲了加深對Flutter動畫實現的理解,我決定把以前寫的一個卡片切換效果的開源小項目,用Flutter「翻譯」一遍。廢話很少說,先來看看效果吧:github

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

思路

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

其次,對於自定義卡片的內容,原Android項目是經過Adapter實現,對於Flutter,則能夠採用IndexedWidgetBuilder實現。小程序

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

組件總覽

因爲卡片視圖須要根據動畫狀況進行渲染,因此顯然是一個StatefulWidget。同時,咱們給出三種基本的動畫模式:app

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爲例:
```dart
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

送書

今天聯合電子工業出版社送出5本書籍《Flutter從0基礎到APP上線》
書籍簡介

在移動互聯網高速發展的今天,跨平臺的移動開發框架層出不窮。爲了幫助廣大開發者快速掌握跨平臺的移動開發並下降互聯網公司的產品研發成本,本書從開發環境的搭建、Dart編程語言基礎和不錯應用、Flutter框架中的組件(包括通用組件、Android風格和iOS風格的組件)、Flutter App中的數據持久化方案、多語言靠前化、使用設備傳感器、和原生代碼通訊、App的測試和Dart調試技巧及App上線流程等方面,全面闡述了Flutter框架的開發技巧。在多個章節後面都附有練習題,你能夠經過練習來鞏固相應知識。
此外,本書還具備很強的工具屬性。它既能夠做爲入門書籍來使用,也能夠用於在必要時隨時查閱某一個知識點;既適合零基礎的學員,也適合有必定開發基礎的朋友。

送書規則:總共送出5本

1. 本文下留言,留言主題爲:本身爲何學習Flutter?  留言點贊數前兩者,每人送一本

2. 小程序抽獎送出3

截止時間:2020年4月19日22:00

---END---


推薦閱讀:
Android消息推送MQTT實戰
Android&nbsp;Native&nbsp;Hook技術你知道多少?
JVM史上最最最完整深刻解析!萬字長文!
解決CoordinatorLayout的動畫抖動以及回彈問題
Java14新特性速覽!
2020 年編程語言盤點展望:Java 老兵不死,Kotlin 蓄勢待發


每個「在看」,我都當成真的喜歡

本文分享自微信公衆號 - 技術最TOP(Tech-Android)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索