美文网首页谷歌-Flutter全栈~~技术栈Flutter Developer
Flutter:手把手教你实现一个仿QQ侧滑菜单的功能

Flutter:手把手教你实现一个仿QQ侧滑菜单的功能

作者: 吉原拉面 | 来源:发表于2018-10-16 10:53 被阅读146次

      一个类似于QQ侧滑菜单的功能,支持从上、下、左、右四个方法打开菜单栏。可以通过自定义transform实现更加炫酷的动效!

      先上效果图: slide from left.gif slide from right.gif slide from top.gif

    Github地址:https://github.com/yumi0629/SlideDrawer

    使用方法:

    SlideStack(
          child: SlideContainer(
            key: _slideKey,
            child: Container(
             /// widget mian.
            ),
            slideDirection: SlideDirection.top,
            onSlide: onSlide,
            drawerSize: maxSlideDistance,
            transform: transform,
          ),
          drawer: Container(
            /// widget drawer.
          ),
        );
    

      slideDirection属性用来控制菜单从哪个方法打开;调用key.currentState.openOrClose()方法可以手动打开或关闭菜单;配合transform属性和滑动过程中返回的监听值,可以在动画过程中为布局添加各种个样的变换。

    实现分析

      用Flutter实现这样的一个效果其实很简单,300行代码足矣。侧滑菜单的实现其实就是上层布局随着用户手势,更改自身的位置,从而让底层菜单栏展示出来。明白了这么一个过程之后,一切就都好办了。
      基本思路:上下两层布局用Stack组合,上层布局需要支持手势,下层布局只需要是一个普通布局就可以了。所以难点就是,上层布局如何支持手势?关于Flutter中的手势可以看下这篇文章:解析Flutter中的手势控制Gestures,了解一下GestureRecognizer是什么。当然,我们实现简单的侧滑功能并不需要这么复杂,因为没有涉及到滑动冲突,我们只需使用系统自带的HorizontalDragGestureRecognizer类就可以了。上层布局每一帧的变换进度使用AnimationController来控制,其回调中的value值可以让我们很方便的就获取到动画的进度值。

    上层布局的实现

    Step 1 注册手势监听Recognizer

      首先,我们给我们的自定义布局注册手势监听Recognizer,_registerGestureRecognizer()方法在布局的initState()方法中执行:

    final Map<Type, GestureRecognizerFactory> gestures =
          <Type, GestureRecognizerFactory>{};
    
    void _registerGestureRecognizer() {
        if (isSlideVertical) {
          gestures[VerticalDragGestureRecognizer] =
              createGestureRecognizer<VerticalDragGestureRecognizer>(
                  () => VerticalDragGestureRecognizer());
        } else {
          gestures[HorizontalDragGestureRecognizer] =
              createGestureRecognizer<HorizontalDragGestureRecognizer>(
                  () => HorizontalDragGestureRecognizer());
        }
      }
    
    GestureRecognizerFactoryWithHandlers<T>
          createGestureRecognizer<T extends DragGestureRecognizer>(
                  GestureRecognizerFactoryConstructor<T> constructor) =>
              GestureRecognizerFactoryWithHandlers<T>(
                constructor,
                (T instance) {
                  instance
                    ..onStart = handleDragStart
                    ..onUpdate = handleDragUpdate
                    ..onEnd = handleDragEnd;
                },
              );
    
    Step 2 绑定Ticker和AnimationController

      我们有了Recognizer,怎么跟用户的手势绑定起来呢?这里用到了AnimationControllerTicker类。

    AnimationController animationController;
    Ticker fingerTicker;
    
    @override
      void initState() {
        animationController =
            AnimationController(vsync: this, duration: widget.autoSlideDuration)
              ..addListener(() {
                ······
                // 刷新上层布局位置
                setState(() {});
              });
    
        fingerTicker = createTicker((_) {
         ······
          // 更具用户手势移动位置,更新animationController.value
          animationController.value = ······;
        });
    
        _registerGestureRecognizer();
    
        super.initState();
      }
    

      很明显,用户的手势滑动时会产生一个滑动值,我们将这个滑动值进行计算,再赋值给animationController.value;同时计算出上层布局需要的偏移量,通过调用setState(() {});刷新上层布局位置。

    Step 3 构建基本控件

      所以,build函数的返回值就很好定义了,因为有手势,我们最外层包裹一个RawGestureDetector,然后将我们在Step 1中注册的gestures传进去,表示这个控件之后将会接收垂直/水平方向的gestures。因为上层布局涉及到位置的移动,因此我们选择使用Transform来构建。每次用户手指滑动时,产生一个dragValue,通过该值计算出控件应该偏移的值,我们将其保存为containerOffset,将这个containerOffset传给Transform,setState时就会产生页面上的移动视觉效果了。

     @override
      Widget build(BuildContext context) => RawGestureDetector(
            gestures: gestures,
            child: Transform.translate(
              offset: isSlideVertical
                  ? Offset(
                      0.0,
                      containerOffset,
                    )
                  : Offset(
                      containerOffset,
                      0.0,
                    ),
              child: _getContainer(),
            ),
          );
    
    Step 4 计算偏移距离

      到目前为止,大致的实现框架已经出来了,接下来就是计算部分了。
      首先,我们的containerOffset其实就是dragValue,很好理解。

    double get containerOffset =>  dragValue;
    

     &emsp其次是滑动(动画)的进度,很简单,dragValue / maxDragDistance,也就是拖动距离/总距离(Drawer的宽度/高度)

     fingerTicker = createTicker((_) {
            animationController.value = dragValue / maxDragDistance;
        });
    

      这里有人可能会有一个疑问了,我根据dragValue,直接算出了containerOffset,然后让上层控件移动位置,整个过程不久OK了嘛,还要什么AnimationController干嘛?确实,animationController只是起到了一个记录作用。我们之所以要用到animationController,一是可以通过AnimationController将拖动进度返回给最外层的父控件,还有一个原因是,可以通过animationController去快速完成/取消滑动动作。
      AnimationController好处都有啥,看下面:

    void openOrClose() {
        final AnimationStatus status = animationController.status;
        final bool isOpen = status == AnimationStatus.completed || status == AnimationStatus.forward;
        animationController.fling(velocity: isOpen ? -2.0 : 2.0);
      }
    
      void _completeSlide() => animationController.forward().then((_) {
            if (widget.onSlideCompleted != null) widget.onSlideCompleted();
          });
    
      void _cancelSlide() => animationController.reverse().then((_) {
            if (widget.onSlideCanceled != null) widget.onSlideCanceled();
          });
    
    

      我们可以很方便的通过AnimationController提供的API,在用户拖动到一半,或者说用户点击了某个按钮来打开/关闭菜单时,快速地完成打开/关闭操作,而不是手动的不停的刷新containerOffset。所以说,AnimationController是一个未雨绸缪的设计,因为这不是一个单纯地布局跟着用户手势动就OK了的控件,我们需要一个控制器来自由地控制布局的位置。

    Step 5 实现用户拖动到一半时自动完成/取消操作

      实际使用中,我们经常会碰到一个问题,就是用户的手指并没有完全滑动到maxDragDistance这个值,可能化到一半就停止了。那么我们的上层控件应该怎么做呢?将布局位置定位在用户手势停止的地方明显是不友好的。QQ侧滑菜单的解决方案是:用户手指超过了某个边界值则自动完成打开操作;若未达到边界值,则取消这个打开操作:


      实现这个功能,我们需要修改handleDragEnd方法,这个方法在Step 1中注册GestureRecognizer时,我们将其传入了Recognizer的onEnd回调监听中,minAutoSlideDragVelocity就是我们定义的这个边界值:
    void handleDragUpdate(DragUpdateDetails details) {
        if (dragValue >
            widget.minAutoSlideDragVelocity) {
          _completeSlide();
        } else if (dragValue <
            widget.minAutoSlideDragVelocity) {
          _cancelSlide();
        } 
        fingerTicker.stop();
    }
    

    合并上、下层控件

      这个很简单,之前已经提到了,使用Stack布局时最简单的方法了:

    class SlideStack extends StatefulWidget {
      /// The main widget.
      final SlideContainer child;
    
      /// The drawer hidden below.
      final Widget drawer;
    
      const SlideStack({
        @required this.child,
        @required this.drawer,
      }) : super();
    
      @override
      State<StatefulWidget> createState() => _StackState();
    }
    
    class _StackState extends State<SlideStack> {
      @override
      Widget build(BuildContext context) {
        return Stack(
          children: <Widget>[
            widget.drawer,
            widget.child,
          ],
        );
      }
    }
    

    细节修饰

      到此为止,我们已经完成了90%的工作了,接下来就是修饰一些细节了,我们添加一些属性,让侧滑菜单体验更加友好。这部分具体的请看源码

    • 给上层布局添加阴影:参考shadowBlurRadiusshadowSpreadRadius属性;
    • 添加阻尼系数dragDampening,这个参数在我们做List滑动的时候很常见,布局的实际移动距离,跟用户手指的移动距离往往是不一致的,我们可以通过这个阻尼系数来控制;
    • 添加自定义transform,我们上面的实现都只是将上层布局进行了平移,如果需要实现效果图1中的平移+缩小效果,需要添加自定义的transform。之所以没有将缩小效果包裹进控件,是因为我希望控件的形变可以更为灵活,大家可以从外部去控制,而不是直接写死。而且我已经通过AnimationController将动画进度暴露出来了,通过动画进度可以很方便的进行各种你想要的transform。
    • 添加进度回调监听onSlideStartedonSlideCompletedonSlideCanceledonSlide

    相关文章

      网友评论

        本文标题:Flutter:手把手教你实现一个仿QQ侧滑菜单的功能

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