在 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)取資料就可以了!