为了账号安全,请及时绑定邮箱和手机立即绑定

Flutter | 超简单仿微信QQ侧滑菜单组件

标签:
WebApp

相信大家对于这种需求肯定不陌生:

https://img1.sycdn.imooc.com//5da1991d000125af04930399.jpg

侧滑出菜单,在Flutter 当中,这种需求怎么实现?

看一下实现的效果:

https://img1.sycdn.imooc.com//5da199320001911804640672.jpg

需求分析

老套路,先分析一下需求:

1.首先可以滑出菜单2.菜单滑动到一定距离完全滑出,未达到距离回滚3.菜单数量、样式随意定制4.菜单点击回调5.菜单展开时,点击 item 收回菜单(见QQ)

代码实现

需求明了以后就可以写代码了。

1. 首先可以滑出菜单

最基本的,菜单要能滑的出来,我们思考一下,如何能在屏幕外面放置 Widget,并且还能滑动?

基本上不到一分钟,相信大家都能想出来答案:ScrollView,没错,也就只有 ScrollView 满足我们的需求。

说干就干:


SingleChildScrollView(

  physics: ClampingScrollPhysics(),

  scrollDirection: Axis.horizontal,

  controller: _controller,

  child: Row(children: children),

)


---------------

// 第一个 Widget,宽度为屏幕的宽度

SizedBox(

  width: screenWidth,

  child: child,

),


1.首先把 ScrollView 滑动的位置改为横向2.把滑动的效果改为 ClampingScrollPhysics,否则 iOS 会有回弹的效果3.设置一个 controller,用于监听滑动距离4.设置child 为Row,并且第一个 Widget 充满横向屏幕,这样后续的菜单就在屏幕外了

2. 菜单滑动到一定距离完全滑出,未达到距离回滚

这个效果就需要监听滑动距离和手势了。

如果滑动距离大于所有 menu 宽度的 1/4,那就全都滑出来,如果不到的话,就回滚回去。

那就要判断一下手是否已经离开屏幕,在这个时候判断距离。

本来想着套一个 Gesture,但是发现不行,问了一下大佬们,用了 Listener

代码如下:


Listener(

  onPointerUp: (d) {

    if (_controller.offset < (screenWidth / 5) * menu.length / 4) {

      _controller.animateTo(0, duration: Duration(milliseconds: 100), curve: Curves.linear);

    } else {

      _controller.animateTo(menu.length * (screenWidth / 5), duration: Duration(milliseconds: 100), curve: Curves.linear);

    }

  },

  child: SingleChildScrollView(

    physics: ClampingScrollPhysics(),

    scrollDirection: Axis.horizontal,

    controller: _controller,

    child: Row(children: children),

  ),

)


很简单,就是在 手抬起 的时候判断了一下距离,然后调用了 animteTo 方法。

3. 菜单数量、样式随意定制

这个其实很简单,让「用户」来传入就好了,

我只需要控制 menu 的宽度。

于是我把 Container 的参数都扒了下来,封装成了一个组件 SlideMenuItem


class SlideMenuItem extends StatelessWidget {

  SlideMenuItem({

    Key key,

    @required this.child,

    this.alignment,

    this.padding,

    Color color,

    Decoration decoration,

    this.foregroundDecoration,

    this.height,

    BoxConstraints constraints,

    this.margin,

    this.transform,

    @required this.onTap,

  })  : assert(child != null),

        assert(margin == null || margin.isNonNegative),

        assert(padding == null || padding.isNonNegative),

        assert(decoration == null || decoration.debugAssertIsValid()),

        assert(constraints == null || constraints.debugAssertIsValid()),

        assert(

            color == null || decoration == null,

            'Cannot provide both a color and a decoration\n'

            'The color argument is just a shorthand for "decoration: new BoxDecoration(color: color)".'),

        decoration =

            decoration ?? (color != null ? BoxDecoration(color: color) : null),

        constraints = (height != null)

            ? constraints?.tighten(height: height) ??

                BoxConstraints.tightFor(height: height)

            : constraints,

        super(key: key);


  final BoxConstraints constraints;

  final Decoration decoration;

  final AlignmentGeometry alignment;

  final EdgeInsets padding;

  final Decoration foregroundDecoration;

  final EdgeInsets margin;

  final Matrix4 transform;

  final Widget child;

  final double height;

  final GestureTapCallback onTap;


  @override

  Widget build(BuildContext context) {

    return Container(

      child: child,

      alignment: alignment,

      constraints: constraints,

      decoration: decoration,

      padding: padding,

      width: screenWidth / 5,

      height: height,

      foregroundDecoration: foregroundDecoration,

      margin: margin,

      transform: transform,

    );

  }

}


这么长的代码,其实就 「width」 和 「onTap」 是自己写的。

4. 菜单点击回调

这里有个小问题:把 Menu 单独封装成了一个组件,那如何在点击 menu 的时候把 menu 收回去?

基于这个问题,在创建整个 SlideItem 的时候,通过构造函数把每一个 menu 都添加上了 GestureDetector,然后在 onTap() 回调中调用 menu 的 onTap() 方法,再调用 dismiss() 方法来收回 menu。

代码如下:


addAll(menu

       .map((w) => GestureDetector(

         child: w,

         onTap: (){

           w.onTap();

           dismiss();

         },

       ))

       .toList());


5. 菜单展开时,点击 item 收回菜单

也就是 菜单展开时,点击了 item 的话,要先收回菜单。QQ 就是如此。

这里有一个知识点,我们设置的点击事件默认是不会命中透明组件的,所以要给第一个默认占满屏幕宽度的 Widget 加上一个属性:behavior: HitTestBehavior.opaque

完整的代码如下:


GestureDetector(

  behavior: HitTestBehavior.opaque,

  onTap: (){

    if(_controller.offset != 0){

      dismiss();

    }else{

      onTap();

    }

  },

  child: SizedBox(

    width: screenWidth,

    child: child,

  ),

)


其中 behavior 有三个值:

•deferToChild:子组件会一个接一个的进行命中测试,如果子组件中有测试通过的,则当前组件通过,这就意味着,如果指针事件作用于子组件上时,其父级组件也肯定可以收到该事件。•opaque:在命中测试时,将当前组件当成不透明处理(即使本身是透明的),最终的效果相当于当前Widget的整个区域都是点击区域。•translucent:当点击组件透明区域时,可以对自身边界内及底部可视区域都进行命中测试,这意味着点击顶部组件透明区域时,顶部组件和底部组件都可以接收到事件。

总结

引用群里一大佬的话:

不要把问题复杂化。

其实对于这种效果,我们仔细的想一分钟,几乎都能想出来解决方案的。而且实现起来也很简单。

本来想封装成一个 ListView 的,后来感觉没什么必要,单独封装成一个 Item 也足够用了。


点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
移动开发工程师
手记
粉丝
30
获赞与收藏
45

关注作者,订阅最新文章

阅读免费教程

  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消