Flutter自定义实现神奇动效的卡片切换视图

image

前言

这一段时间,Flutter的势头是越来越猛了,作为一个Android程序猿,我自然也是想要赶紧尝试一把。在学习到动画的这部分后,为了加深对Flutter动画实现的理解,我决定把之前写的一个卡片切换效果的开源小项目,用Flutter“翻译”一遍。

废话不多说,先来看看效果吧:

Android iOS
image
image

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

思路

首先,关于卡片的层叠效果,在原Android项目中,是通过Scale差异以及TranslationY来体现的,Flutter可以继续采用这种方式。

其次,对于自定义卡片的内容,原Android项目是通过Adapter实现,对于Flutter,则可以采用IndexedWidgetBuilder实现。

最后,就是自定义动效的实现,原Android项目是通过一个0到1的ValueAnimator来定义动画的展示过程,而Flutter中,正好有与之对应的Animation和AnimationController,如此我们就可以直接自定义一个动画过程中,具体的视图展示方式。

组件总览

由于卡片视图需要根据动画情况进行渲染,所以显然是一个StatefulWidget。

同时,我们给出三种基本的动画模式:

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为例:

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,869评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,716评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,223评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,047评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,089评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,839评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,516评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,410评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,920评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,052评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,179评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,868评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,522评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,070评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,186评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,487评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,162评论 2 356

推荐阅读更多精彩内容

  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,109评论 4 62
  • 东西有点多,但是资源绝对nice,自己都全部亲身体验过了,大家可放心使用 github排名: https://gi...
    Rance935阅读 11,213评论 26 312
  • 对于泰国菜,谈不上钟爱,不过是待在老挝时,有几家不错的泰国餐馆,三天两头地去吃,吃了一年,终于吃出了感情。 这次来...
    十步之泽阅读 429评论 1 1
  • 文|漫漫 今夜,窗外漂浮着紫丁香的味道,淡淡的有些苦涩,明月也像是你的眼睛,那样分明的颜色, 那样冷漠...
    D旭伶阅读 756评论 4 9
  • 看完整本书,心中有种荡气回肠之感。是为好得缺乏人性的刘峰晚景凄惨而叹息吗?是为那一晃几十年的时光就流逝了而伤感吗?...
    知不为阅读 421评论 0 1