最近 Flutter 爭氣了溅固! Flutter 也可以做這么炫酷的動畫

在 2019 年的谷歌 I/O 大會上遇伞,開發(fā)團(tuán)隊發(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動畫實現(xiàn)的理解噩死,我決定把之前寫的一個卡片切換效果的開源小項目,用Flutter“翻譯”一遍神年。

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

Android:

IOS

Github地址:

https://github.com/BakerJQ/Flutter-InfiniteCards

思路

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

  • 其次艾扮,對于自定義卡片的內(nèi)容,原Android項目是通過Adapter實現(xiàn)占婉,對于Flutter泡嘴,則可以采用IndexedWidgetBuilder實現(xiàn)。

  • 最后逆济,就是自定義動效的實現(xiàn)酌予,原Android項目是通過一個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)行實時渲染
      listenerForSetState: () {
        setState(() {});
      });
  _helper.init(this, context);
  if (widget.controller != null) {
      widget.controller.animHelper = _helper;
  }
}

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

  ...
  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;
  //點擊卡片是否觸發(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。

Helper

Helper是整個動畫效果實現(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)行排序声搁,因為要保證在前面的后渲染黑竞。

而動畫是如何處理的呢,以切換到前面的卡片為例:

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)換。

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

總結(jié)

由于Flutter采用的是聲明式的視圖構(gòu)建方式嗦明,在編碼初期冲呢,多少會受到原生編碼方式的思維影響,而覺得很難受招狸。但是在熟悉了之后,就會發(fā)現(xiàn)其實很多思想都是共通的邻薯,比如Animation裙戏,比如插值器的概念等等。

另外厕诡,研讀源碼仍然是最有效的解決問題的方式累榜,比如相比Android中直接對ScrollView進(jìn)行animateTo操作,在Flutter中需要通過ScrollController進(jìn)行animateTo操作灵嫌,正是這一點讓我找到了在Flutter中實現(xiàn)InfiniteCards效果的方法壹罚。

更具體的Demo請前往Github的Flutter-InfiniteCards Repo,歡迎大家star和提issue寿羞。

再次貼一下Github地址:
https://github.com/BakerJQ/Flutter-InfiniteCards

### 文末送福利啦猖凛!

為此我整理了一些以往自己學(xué)習(xí)的視頻資料,如果有需要借鑒學(xué)習(xí)的開發(fā)者可以聯(lián)系我绪穆,包括上文講到的flutter 免費獲取共同進(jìn)步(Flutter丶Glide丶OPencv丶EventBus丶自定義View丶數(shù)據(jù)庫框架設(shè)計丶插件化組件化丶Binder等都有對應(yīng)的視頻教學(xué)以及一些面試題)免費分享給大家辨泳。
(包括Java在Android開發(fā)中應(yīng)用、APP框架知識體系玖院、高級UI菠红、全方位性能調(diào)優(yōu),NDK開發(fā)难菌,音視頻技術(shù)试溯,人工智能技術(shù),跨平臺技術(shù)等技術(shù)資料)郊酒,希望能幫助到大家遇绞, 也節(jié)省大家在網(wǎng)上搜索資料的時間來學(xué)習(xí)。
資料領(lǐng)取方式:點擊鏈接加入群聊【Android開發(fā)交流1018342383】:https://jq.qq.com/?_wv=1027&k=57fcAxd猎塞,找群管理免費領(lǐng)取试读。備注一下簡書看到的來領(lǐng)取資料就可以了!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末荠耽,一起剝皮案震驚了整個濱河市钩骇,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖倘屹,帶你破解...
    沈念sama閱讀 219,427評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件银亲,死亡現(xiàn)場離奇詭異,居然都是意外死亡纽匙,警方通過查閱死者的電腦和手機(jī)务蝠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來烛缔,“玉大人馏段,你說我怎么就攤上這事〖桑” “怎么了院喜?”我有些...
    開封第一講書人閱讀 165,747評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長晕翠。 經(jīng)常有香客問我喷舀,道長,這世上最難降的妖魔是什么淋肾? 我笑而不...
    開封第一講書人閱讀 58,939評論 1 295
  • 正文 為了忘掉前任硫麻,我火速辦了婚禮,結(jié)果婚禮上樊卓,老公的妹妹穿的比我還像新娘拿愧。我一直安慰自己,他們只是感情好碌尔,可當(dāng)我...
    茶點故事閱讀 67,955評論 6 392
  • 文/花漫 我一把揭開白布赶掖。 她就那樣靜靜地躺著,像睡著了一般七扰。 火紅的嫁衣襯著肌膚如雪奢赂。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,737評論 1 305
  • 那天颈走,我揣著相機(jī)與錄音膳灶,去河邊找鬼。 笑死立由,一個胖子當(dāng)著我的面吹牛轧钓,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播锐膜,決...
    沈念sama閱讀 40,448評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼毕箍,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了道盏?” 一聲冷哼從身側(cè)響起而柑,我...
    開封第一講書人閱讀 39,352評論 0 276
  • 序言:老撾萬榮一對情侶失蹤文捶,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后媒咳,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體粹排,經(jīng)...
    沈念sama閱讀 45,834評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,992評論 3 338
  • 正文 我和宋清朗相戀三年涩澡,在試婚紗的時候發(fā)現(xiàn)自己被綠了顽耳。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,133評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡妙同,死狀恐怖射富,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情粥帚,我是刑警寧澤辉浦,帶...
    沈念sama閱讀 35,815評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站茎辐,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏掂恕。R本人自食惡果不足惜拖陆,卻給世界環(huán)境...
    茶點故事閱讀 41,477評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望懊亡。 院中可真熱鬧依啰,春花似錦、人聲如沸店枣。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽鸯两。三九已至闷旧,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間钧唐,已是汗流浹背忙灼。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留钝侠,地道東北人该园。 一個月前我還...
    沈念sama閱讀 48,398評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像帅韧,于是被迫代替她去往敵國和親里初。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,077評論 2 355

推薦閱讀更多精彩內(nèi)容