美文网首页Flutter圈子程序员Flutter中文社区
Flutter自定义实现神奇动效的卡片切换视图

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

作者: BakerJQ | 来源:发表于2019-04-04 16:14 被阅读3次
    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

    相关文章

      网友评论

        本文标题:Flutter自定义实现神奇动效的卡片切换视图

        本文链接:https://www.haomeiwen.com/subject/mokqiqtx.html