【Flutter】番外篇之手势操作滑动抽屉效果

1,782 阅读4分钟

前言

这是第二篇番外了,第一篇是关于动画翻转效果实现而这次要说的是运用手势操作实现类似于Drawer抽屉拖拽功能。Drawer是默认全部隐藏在屏幕外,在屏幕边缘做手势操作使其滑出或者通过点击实现滑出。而我希望实现默认带偏移量和多方向侧拉效果功能,在默认情况下能够展示侧边组件部分内容通过拖拽展示组件整体内容,可能描述上不好理解就直接上图吧😂。

没图怎么行

实现效果如上图所示,当初需求是希望侧拉组件可以拖拽边缘实现滑出。

PS:为什么会有这样的需求,个人认为默认情况下能够展示出组件部分内容可以让用户更直观地知道侧边可操作,感官上交互性更强😆。(例如豆瓣电影底部影评列表上拉展示)以上纯属个人对于交互设计理解╮( ̄▽ ̄)╭

实现方案

实现拖拽功能肯定离不开手势操作和位移这两点,然后结合这两点组合操作实现抽屉拖拽滑出的功能组件。

  • 拖拽组件在指定偏移量范围内实现组件实时位移
  • 手势抬起时根据组件位移具体做出不同动画效果
  • 当拖拽距离小于最小偏移量则缩回组件
  • 当拖拽距离大于最小偏移量则展开组件

本次实现方案中使用到的组件包含:Transform 、RawGestureDetector 、 AnimationController 、 Animation。Transform、AnimationController、Animatio在之前实战篇中都有介绍过而RawGestureDetector是第一次听说。RawGestureDetector也是实现手势监听组件和GestureDetector相似但比起GestureDetector使用上更为复杂。

简要介绍一下两者不同点:

  • GestureDetector无状态、RawGestureDetector有状态
  • GestureDetector通过设置各种手势回调监听手势、RawGestureDetector则是设置gestures通过GestureRecognizerFactory实现手势监听
  • GestureDetector是高级组件,它的build方法就是一个RawGestureDetector组件。

所以在方案中完全可以使用GestureDetector代替RawGestureDetector更简单,纯粹是因为没有用过RawGestureDetector所以尝试一下😄

代码

手势部分

Transform.translate(
      offset: Offset(offsetX, offsetY),
      child: RawGestureDetector(
        gestures: {
          DrawerPanGestureRecognizer:
              GestureRecognizerFactoryWithHandlers<DrawerPanGestureRecognizer>(
            () => DrawerPanGestureRecognizer(),
            (DrawerPanGestureRecognizer instance) {
              instance
                ..onUpdate = (DragUpdateDetails details) {
                    // 省略处理逻辑,获取实时手势坐标点根据拖拽方向更新Transform的offsetX或offsetY
                  }
                }
                ..onEnd = (DragEndDetails details) {
                    // 省略处理逻辑 手势抬起操作根据位移距离判断组件展开或是缩回动画
                  }
                };
            },
          ),
        },
        child: Container(
          width: width,
          child: widget.child,
        ),
      ),
    );

动画部分

动画分为展开动画和缩回动画。通过手势位移量大小决定执行对应动画效果,同样是通过监听动画执行过程中animation.value数值变化改变offsetX或offsetY的大小实现组件移动最终位移到最终点。

/// 复原动画
  _setCallBackAnimation() {
    double offset;
    switch (widget.direction) { //根据方向设置起始偏移值
      case DragDirection.top:
      case DragDirection.bottom:
        offset = offsetY;
        break;
      case DragDirection.left:
      case DragDirection.right:
        offset = offsetX;
        break;
    }
    print("dragdemo _setCallBackAnimation  begin: $offset, end: $originOffset");
    _animation = Tween<double>(begin: offset, end: originOffset).animate(
        CurvedAnimation(
            parent: _callbackAnimationController, curve: Curves.easeOut))
      ..addListener(() {
        print("dragdemo _setCallBackAnimation  ${_animation.value}");
        setState(() {
          switch (widget.direction) {
            case DragDirection.top:
            case DragDirection.bottom:
              offsetY = _animation.value;
              break;
            case DragDirection.left:
            case DragDirection.right:
              offsetX = _animation.value;
              break;
          }
        });
      });
  }

  /// 展开动画
  _setToMaxAnimation() {
    double offset;
    switch (widget.direction) {//根据方向设置起始偏移值
      case DragDirection.top:
      case DragDirection.bottom:
        offset = offsetY;
        break;
      case DragDirection.left:
      case DragDirection.right:
        offset = offsetX;
        break;
    }
    print("dragdemo _setToMaxAnimation  begin: $offset, end: $maxOffset");
    _animation = Tween<double>(begin: offset, end: maxOffset).animate(
        CurvedAnimation(
            parent: _toMaxAnimationController, curve: Curves.easeOutQuart))
      ..addListener(() {
        print("dragdemo _setToMaxAnimation    ${_animation.value}");
        setState(() {
          switch (widget.direction) {
            case DragDirection.top:
            case DragDirection.bottom:
              offsetY = _animation.value;
              break;
            case DragDirection.left:
            case DragDirection.right:
              offsetX = _animation.value;
              break;
          }
        });
      });
  }

初始化部分

GestureDragDrawer(
      {this.child, //拖拽组件要展示的子内容
      this.childSize = 0, //子内容的大小
      this.originOffset = 0, //预设偏移量
      this.parentWidth = 0, //父级组件的宽度 当拖拽组件在bottom和right时需要用到
      this.parentHeight = 0,//父级组件的高度
      this.direction = DragDirection.left});
      .....
_initValue() {
    width = widget.childSize.abs();
    minOffset = -width / 2;
    midOffset = -width / 3;
    maxOffset = 0;
    /// 底部和右边的偏移量需要特殊计算初始值(右边和底部的值偏移量 = 父组件的宽或高 - 初始预设偏移量)
    switch (widget.direction) {
      case DragDirection.bottom:
        originOffset = widget.parentHeight - widget.originOffset;
        maxOffset = widget.parentHeight - width;
        midOffset = maxOffset + width / 3;
        minOffset = maxOffset + width / 2;
        break;
      case DragDirection.right:
        originOffset = widget.parentWidth - widget.originOffset;
        maxOffset = widget.parentWidth - width;
        midOffset = maxOffset + width / 3;
        minOffset = maxOffset + width / 2;
        break;
      default:
        originOffset = -width + widget.originOffset;
        break;
    }
  }

🚀完整代码看这里🚀

最后

最终实现效果如下(demo如果存在问题可以告诉我哈~这样我才能进步)

考虑以后会写更多自定义组件或Demo的可能性,所以Flutter番外篇组合成一个项目库方便自己学习和总结。我也将第一篇番外Flip翻转效果完整代码添加到该项目中了希望小伙伴多多点赞支持一下。

番外篇的项目地址

最后的最后欢迎大家下载我自己开发的应用时钟软件,如果手头有闲置Android手机可以下载安装日常使用。

参考资料