59

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

 4 years ago
source link: https://www.tuicool.com/articles/RFRNfqa
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

meqEvaN.jpg!web

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

看一下实现的效果:

MRjERn3.gif

需求分析

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

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 也足够用了。

另我个人创建了一个「Flutter 交流群」,可以添加我个人微信 「17610912320」来入群。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK