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

Flutter 滑动删除最佳实践

标签:
CSS3

在Gmail中,我们经常会看到如下效果:

滑动去存档,也可以滑动删除。

那作为Google 自家出品的Flutter,当然也会有这种组件。

Dismissible

按照惯例来看一下官方文档上给出的解释:

1.  `A widget that can be dismissed by dragging in the indicated direction.`
    
2.    
    
3.  `Dragging  or flinging this widget in the DismissDirection causes the child to slide out of view.`
    
4.    
    
5.  `可以通过指示的方向来拖动消失的组件。`
    
6.  `在DismissDirection中拖动或投掷该组件会导致该组件滑出视图。`
    

再来看一下构造方法,来确认一下我们怎么使用:

1.  `const  Dismissible({`
    
2.   `@required  Key key,`
    
3.   `@required  this.child,`
    
4.   `this.background,`
    
5.   `this.secondaryBackground,`
    
6.   `this.confirmDismiss,`
    
7.   `this.onResize,`
    
8.   `this.onDismissed,`
    
9.   `this.direction =  DismissDirection.horizontal,`
    
10.   `this.resizeDuration =  const  Duration(milliseconds:  300),`
    
11.   `this.dismissThresholds =  const  <DismissDirection,  double>{},`
    
12.   `this.movementDuration =  const  Duration(milliseconds:  200),`
    
13.   `this.crossAxisEndOffset =  0.0,`
    
14.   `this.dragStartBehavior =  DragStartBehavior.start,`
    
15.  `})  :  assert(key !=  null),`
    
16.  `assert(secondaryBackground !=  null  ? background !=  null  :  true),`
    
17.  `assert(dragStartBehavior !=  null),`
    
18.  `super(key: key);`

可以发现我们必传的参数有 key 和 child。

child不必多说,就是我们需要滑动删除的组件,那key是什么?

后续我会出一篇关于 Flutter Key 的文章来详细解释一下什么是 Key。

现在我们只需要理解,key 是 widget 的唯一标示。因为有了key,所以 widget tree 才知道我们删除了什么widget。

简单使用

知道了需要传什么参数,那我们开始撸一个demo:

1.  `class  _DismissiblePageState  extends  State<DismissiblePage>  {`
    
2.   `// 生成列表数据`
    
3.   `var _listData =  List<String>.generate(30,  (i)  =>  'Items $i');`
    
4.    
    
5.   `@override`
    
6.   `Widget build(BuildContext context)  {`
    
7.   `return  Scaffold(`
    
8.   `appBar:  AppBar(`
    
9.   `title:  Text('DismissiblePage'),`
    
10.   `),`
    
11.   `body: _createListView(),`
    
12.   `);`
    
13.   `}`
    
14.    
    
15.   `// 创建ListView`
    
16.   `Widget _createListView()  {`
    
17.   `return  ListView.builder(`
    
18.   `itemCount: _listData.length,`
    
19.   `itemBuilder:  (context, index)  {`
    
20.   `return  Dismissible(`
    
21.   `// Key`
    
22.   `key:  Key('key${_listData[index]}'),`
    
23.   `// Child`
    
24.   `child:  ListTile(`
    
25.   `title:  Text('Title${_listData[index]}'),`
    
26.   `),`
    
27.   `);`
    
28.   `},`
    
29.   `);`
    
30.   `}`
    
31.  `}`

代码很简单,就是生成了一个 ListView ,在ListView 的 item中用 Dismissible 包起来。

效果如下:

虽然看起来这里每一个 item 被删除了,但是实际上并没有,因为我们没对数据源进行处理。

添加删除逻辑
1.  `// 创建ListView`
    
2.  `Widget _createListView()  {`
    
3.   `return  ListView.builder(`
    
4.   `itemCount: _listData.length,`
    
5.   `itemBuilder:  (context, index)  {`
    
6.   `return  Dismissible(`
    
7.   `// Key`
    
8.   `key:  Key('key${_listData[index]}'),`
    
9.   `// Child`
    
10.   `child:  ListTile(`
    
11.   `title:  Text('${_listData[index]}'),`
    
12.   `),`
    
13.   `onDismissed:  (direction){`
    
14.   `// 删除后刷新列表,以达到真正的删除`
    
15.   `setState(()  {`
    
16.   `_listData.removeAt(index);`
    
17.   `});`
    
18.   `},`
    
19.   `);`
    
20.   `},`
    
21.   `);`
    
22.  `}`

可以看到我们添加了一个 onDismissed参数。

这个方法会在删除后进行回调,我们在这里把数据源删除,并刷新列表即可。

现在数据可以真正的删除了,但是用户并不知道我们做了什么,所以要来一点提示:

代码如下:

1.  `onDismissed:  (direction)  {`
    
2.    
    
3.   `// 展示 SnackBar`
    
4.   `Scaffold.of(context).showSnackBar(SnackBar(`
    
5.   `content:  Text('删除了${_listData[index]}'),`
    
6.   `));`
    
7.    
    
8.   `// 删除后刷新列表,以达到真正的删除`
    
9.   `setState(()  {`
    
10.   `_listData.removeAt(index);`
    
11.   `});`
    
12.    
    
13.  `},`
增加视觉效果

虽然我们处理了删除后的逻辑,但是我们在滑动的时候,用户还是不知道我们在干什么。

这个时候我们就要增加滑动时候的视觉效果了。

还是来看构造函数:

1.  `const  Dismissible({`
    
2.   `@required  Key key,`
    
3.   `@required  this.child,`
    
4.   `this.background,`
    
5.   `this.secondaryBackground,`
    
6.   `this.confirmDismiss,`
    
7.   `this.onResize,`
    
8.   `this.onDismissed,`
    
9.   `this.direction =  DismissDirection.horizontal,`
    
10.   `this.resizeDuration =  const  Duration(milliseconds:  300),`
    
11.   `this.dismissThresholds =  const  <DismissDirection,  double>{},`
    
12.   `this.movementDuration =  const  Duration(milliseconds:  200),`
    
13.   `this.crossAxisEndOffset =  0.0,`
    
14.   `this.dragStartBehavior =  DragStartBehavior.start,`
    
15.  `})  :  assert(key !=  null),`
    
16.  `assert(secondaryBackground !=  null  ? background !=  null  :  true),`
    
17.  `assert(dragStartBehavior !=  null),`
    
18.  `super(key: key);`

可以看到有个 background 和 secondaryBackground。

一个背景和一个次要的背景,我们点过去查看:

1.   `/// A widget that is stacked behind the child. If secondaryBackground is also`
    
2.   `/// specified then this widget only appears when the child has been dragged`
    
3.   `/// down or to the right.`
    
4.   `final  Widget background;`
    
5.    
    
6.   `/// A widget that is stacked behind the child and is exposed when the child`
    
7.   `/// has been dragged up or to the left. It may only be specified when background`
    
8.   `/// has also been specified.`
    
9.   `final  Widget secondaryBackground;`

可以看到两个 background 都是一个Widget,那么也就是说我们写什么上去都行。

通过查看注释我们了解到:

background 是向右滑动展示的,secondaryBackground是向左滑动展示的。

如果只有一个 background,那么左滑右滑都是它自己。

那我们开始撸码,先来一个背景的:

1.  `background:  Container(`
    
2.   `color:  Colors.red,`
    
3.   `// 这里使用 ListTile 因为可以快速设置左右两端的Icon`
    
4.   `child:  ListTile(`
    
5.   `leading:  Icon(`
    
6.   `Icons.bookmark,`
    
7.   `color:  Colors.white,`
    
8.   `),`
    
9.   `trailing:  Icon(`
    
10.   `Icons.delete,`
    
11.   `color:  Colors.white,`
    
12.   `),`
    
13.   `),`
    
14.  `),`

效果如下:

再来两个背景的:

1.  `background:  Container(`
    
2.   `color:  Colors.green,`
    
3.   `// 这里使用 ListTile 因为可以快速设置左右两端的Icon`
    
4.   `child:  ListTile(`
    
5.   `leading:  Icon(`
    
6.   `Icons.bookmark,`
    
7.   `color:  Colors.white,`
    
8.   `),`
    
9.   `),`
    
10.  `),`
    
11.    
    
12.  `secondaryBackground:  Container(`
    
13.   `color:  Colors.red,`
    
14.   `// 这里使用 ListTile 因为可以快速设置左右两端的Icon`
    
15.   `child:  ListTile(`
    
16.   `trailing:  Icon(`
    
17.   `Icons.delete,`
    
18.   `color:  Colors.white,`
    
19.   `),`
    
20.   `),`
    
21.  `),`
    

效果如下:

处理不同滑动方向的完成事件

那现在问题就来了,既然我现在有两个滑动方向了,就代表着两个业务逻辑。

这个时候我们应该怎么办?

这个时候 onDismissed: (direction) 中的 direction 就有用了:

我们找到 direction 的类为 DismissDirection,该类为一个枚举类:

1.  `/// The direction in which a [Dismissible] can be dismissed.`
    
2.  `enum  DismissDirection  {`
    
3.   `/// 上下滑动`
    
4.   `vertical,`
    
5.    
    
6.   `/// 左右滑动`
    
7.   `horizontal,`
    
8.    
    
9.   `/// 从右到左`
    
10.   `endToStart,`
    
11.    
    
12.   `/// 从左到右`
    
13.   `startToEnd,`
    
14.    
    
15.   `/// 向上滑动`
    
16.   `up,`
    
17.    
    
18.   `/// 向下滑动`
    
19.   `down`
    
20.  `}`
    

那我们就可以根据上面的枚举来判断了:

1.  `onDismissed:  (direction)  {`
    
2.   `var _snackStr;`
    
3.   `if(direction ==  DismissDirection.endToStart){`
    
4.   `// 从右向左  也就是删除`
    
5.   `_snackStr =  '删除了${_listData[index]}';`
    
6.   `}else  if  (direction ==  DismissDirection.startToEnd){`
    
7.   `_snackStr =  '收藏了${_listData[index]}';`
    
8.   `}`
    
9.    
    
10.   `// 展示 SnackBar`
    
11.   `Scaffold.of(context).showSnackBar(SnackBar(`
    
12.   `content:  Text(_snackStr),`
    
13.   `));`
    
14.    
    
15.   `// 删除后刷新列表,以达到真正的删除`
    
16.   `setState(()  {`
    
17.   `_listData.removeAt(index);`
    
18.   `});`
    
19.  `},`

效果如下:

避免误操作

看到这肯定有人觉得,这手一抖不就删除了么,能不能有什么操作来防止误操作?

那肯定有啊,你能想到的,Google都想好了,还是来看构造函数:

1.  `const  Dismissible({`
    
2.   `@required  Key key,`
    
3.   `@required  this.child,`
    
4.   `this.background,`
    
5.   `this.secondaryBackground,`
    
6.   `this.confirmDismiss,`
    
7.   `this.onResize,`
    
8.   `this.onDismissed,`
    
9.   `this.direction =  DismissDirection.horizontal,`
    
10.   `this.resizeDuration =  const  Duration(milliseconds:  300),`
    
11.   `this.dismissThresholds =  const  <DismissDirection,  double>{},`
    
12.   `this.movementDuration =  const  Duration(milliseconds:  200),`
    
13.   `this.crossAxisEndOffset =  0.0,`
    
14.   `this.dragStartBehavior =  DragStartBehavior.start,`
    
15.  `})  :  assert(key !=  null),`
    
16.  `assert(secondaryBackground !=  null  ? background !=  null  :  true),`
    
17.  `assert(dragStartBehavior !=  null),`
    
18.  `super(key: key);`

看没看到一个 confirmDismiss ?,就是它,来看一下源码:

1.  `/// Gives the app an opportunity to confirm or veto a pending dismissal.`
    
2.  `///`
    
3.  `/// If the returned Future<bool> completes true, then this widget will be`
    
4.  `/// dismissed, otherwise it will be moved back to its original location.`
    
5.  `///`
    
6.  `/// If the returned Future<bool> completes to false or null the [onResize]`
    
7.  `/// and [onDismissed] callbacks will not run.`
    
8.  `final  ConfirmDismissCallback confirmDismiss;`

大致意思就是:

1.  `使应用程序有机会是否决定dismiss。`
    
2.    
    
3.  `如果返回的future<bool>为true,则该小部件将被dismiss,否则它将被移回其原始位置。`
    
4.    
    
5.  `如果返回的future<bool>为false或空,则不会运行[onResize]和[ondismissed]回调。`
    

既然如此,我们就在该方法中,show 一个Dialog来判断用户是否删除:

1.  `confirmDismiss:  (direction)  async  {`
    
2.   `var _confirmContent;`
    
3.    
    
4.   `var _alertDialog;`
    
5.    
    
6.   `if  (direction ==  DismissDirection.endToStart)  {`
    
7.   `// 从右向左  也就是删除`
    
8.   `_confirmContent =  '确认删除${_listData[index]}?';`
    
9.   `_alertDialog = _createDialog(`
    
10.   `_confirmContent,`
    
11.   `()  {`
    
12.   `// 展示 SnackBar`
    
13.   `Scaffold.of(context).showSnackBar(SnackBar(`
    
14.   `content:  Text('确认删除${_listData[index]}'),`
    
15.   `duration:  Duration(milliseconds:  400),`
    
16.   `));`
    
17.   `Navigator.of(context).pop(true);`
    
18.   `},`
    
19.   `()  {`
    
20.   `// 展示 SnackBar`
    
21.   `Scaffold.of(context).showSnackBar(SnackBar(`
    
22.   `content:  Text('不删除${_listData[index]}'),`
    
23.   `duration:  Duration(milliseconds:  400),`
    
24.   `));`
    
25.   `Navigator.of(context).pop(false);`
    
26.   `},`
    
27.   `);`
    
28.   `}  else  if  (direction ==  DismissDirection.startToEnd)  {`
    
29.   `_confirmContent =  '确认收藏${_listData[index]}?';`
    
30.   `_alertDialog = _createDialog(`
    
31.   `_confirmContent,`
    
32.   `()  {`
    
33.   `// 展示 SnackBar`
    
34.   `Scaffold.of(context).showSnackBar(SnackBar(`
    
35.   `content:  Text('确认收藏${_listData[index]}'),`
    
36.   `duration:  Duration(milliseconds:  400),`
    
37.   `));`
    
38.   `Navigator.of(context).pop(true);`
    
39.   `},`
    
40.   `()  {`
    
41.   `// 展示 SnackBar`
    
42.   `Scaffold.of(context).showSnackBar(SnackBar(`
    
43.   `content:  Text('不收藏${_listData[index]}'),`
    
44.   `duration:  Duration(milliseconds:  400),`
    
45.   `));`
    
46.   `Navigator.of(context).pop(false);`
    
47.   `},`
    
48.   `);`
    
49.   `}`
    
50.    
    
51.   `var isDismiss =  await showDialog(`
    
52.   `context: context,`
    
53.   `builder:  (context)  {`
    
54.   `return _alertDialog;`
    
55.   `});`
    
56.   `return isDismiss;`
    
57.  `},`
    

解释一下上面的代码。

首先判断滑动的方向,然后根据创建的方向来创建Dialog 以及 点击事件。

最后点击时通过 Navigator.pop()来返回值。

效果如下:

总结

到目前为止滑动删除的最佳实践也就结束了。

至于构造函数中其他参数是什么意思,可以自行上Flutter官网 查询。

点击查看更多内容
TA 点赞

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

评论

作者其他优质文章

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

关注作者,订阅最新文章

阅读免费教程

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

100积分直接送

付费专栏免费学

大额优惠券免费领

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

举报

0/150
提交
取消