做者: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本
1. 本文下留言,留言主题为:本身为何学习Flutter? 留言点赞数前两者,每人送一本
2. 小程序抽奖送出3本
截止时间:2020年4月19日22:00
---END---

本文分享自微信公众号 - 技术最TOP(Tech-Android)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。