在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官网 查询。