在Gmail中,我们经常会看到如下效果:
滑动去存档,也可以滑动删除。
那作为Google 自家出品的Flutter,当然也会有这种组件。
按照惯例来看一下官方文档上给出的解释:
A widget that can be dismissed by dragging in the indicated direction.
Dragging or flinging this widget in the DismissDirection causes the child to slide out of view.
可以通过指示的方向来拖动消失的组件。
在DismissDirection中拖动或投掷该组件会导致该组件滑出视图。
再来看一下构造方法,来确认一下我们怎么使用:
const Dismissible({
@required Key key,
@required this.child,
this.background,
this.secondaryBackground,
this.confirmDismiss,
this.onResize,
this.onDismissed,
this.direction = DismissDirection.horizontal,
this.resizeDuration = const Duration(milliseconds: 300),
this.dismissThresholds = const <DismissDirection, double>{},
this.movementDuration = const Duration(milliseconds: 200),
this.crossAxisEndOffset = 0.0,
this.dragStartBehavior = DragStartBehavior.start,
}) : assert(key != null),
assert(secondaryBackground != null ? background != null : true),
assert(dragStartBehavior != null),
super(key: key);
可以发现我们必传的参数有 key 和 child。
child不必多说,就是我们需要滑动删除的组件,那key是什么?
后续我会出一篇关于 Flutter Key 的文章来详细解释一下什么是 Key。
现在我们只需要理解,key 是 widget 的唯一标示。因为有了key,所以 widget tree 才知道我们删除了什么widget。
知道了需要传什么参数,那我们开始撸一个demo:
class _DismissiblePageState extends State<DismissiblePage> {
// 生成列表数据
var _listData = List<String>.generate(30, (i) => 'Items $i');
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('DismissiblePage'),
),
body: _createListView(),
);
}
// 创建ListView
Widget _createListView() {
return ListView.builder(
itemCount: _listData.length,
itemBuilder: (context, index) {
return Dismissible(
// Key
key: Key('key${_listData[index]}'),
// Child
child: ListTile(
title: Text('Title${_listData[index]}'),
),
);
},
);
}
}
代码很简单,就是生成了一个 ListView ,在ListView 的 item中用 Dismissible 包起来。
效果如下:
虽然看起来这里每一个 item 被删除了,但是实际上并没有,因为我们没对数据源进行处理。
// 创建ListView
Widget _createListView() {
return ListView.builder(
itemCount: _listData.length,
itemBuilder: (context, index) {
return Dismissible(
// Key
key: Key('key${_listData[index]}'),
// Child
child: ListTile(
title: Text('${_listData[index]}'),
),
onDismissed: (direction){
// 删除后刷新列表,以达到真正的删除
setState(() {
_listData.removeAt(index);
});
},
);
},
);
}
可以看到我们添加了一个 onDismissed
参数。
这个方法会在删除后进行回调,我们在这里把数据源删除,并刷新列表即可。
现在数据可以真正的删除了,但是用户并不知道我们做了什么,所以要来一点提示:
代码如下:
onDismissed: (direction) {
// 展示 SnackBar
Scaffold.of(context).showSnackBar(SnackBar(
content: Text('删除了${_listData[index]}'),
));
// 删除后刷新列表,以达到真正的删除
setState(() {
_listData.removeAt(index);
});
},
虽然我们处理了删除后的逻辑,但是我们在滑动的时候,用户还是不知道我们在干什么。
这个时候我们就要增加滑动时候的视觉效果了。
还是来看构造函数:
const Dismissible({
@required Key key,
@required this.child,
this.background,
this.secondaryBackground,
this.confirmDismiss,
this.onResize,
this.onDismissed,
this.direction = DismissDirection.horizontal,
this.resizeDuration = const Duration(milliseconds: 300),
this.dismissThresholds = const <DismissDirection, double>{},
this.movementDuration = const Duration(milliseconds: 200),
this.crossAxisEndOffset = 0.0,
this.dragStartBehavior = DragStartBehavior.start,
}) : assert(key != null),
assert(secondaryBackground != null ? background != null : true),
assert(dragStartBehavior != null),
super(key: key);
可以看到有个 background 和 secondaryBackground。
一个背景和一个次要的背景,我们点过去查看:
/// A widget that is stacked behind the child. If secondaryBackground is also
/// specified then this widget only appears when the child has been dragged
/// down or to the right.
final Widget background;
/// A widget that is stacked behind the child and is exposed when the child
/// has been dragged up or to the left. It may only be specified when background
/// has also been specified.
final Widget secondaryBackground;
可以看到两个 background 都是一个Widget,那么也就是说我们写什么上去都行。
通过查看注释我们了解到:
background 是向右滑动展示的,secondaryBackground是向左滑动展示的。
如果只有一个 background,那么左滑右滑都是它自己。
那我们开始撸码,先来一个背景的:
background: Container(
color: Colors.red,
// 这里使用 ListTile 因为可以快速设置左右两端的Icon
child: ListTile(
leading: Icon(
Icons.bookmark,
color: Colors.white,
),
trailing: Icon(
Icons.delete,
color: Colors.white,
),
),
),
效果如下:
再来两个背景的:
background: Container(
color: Colors.green,
// 这里使用 ListTile 因为可以快速设置左右两端的Icon
child: ListTile(
leading: Icon(
Icons.bookmark,
color: Colors.white,
),
),
),
secondaryBackground: Container(
color: Colors.red,
// 这里使用 ListTile 因为可以快速设置左右两端的Icon
child: ListTile(
trailing: Icon(
Icons.delete,
color: Colors.white,
),
),
),
效果如下:
那现在问题就来了,既然我现在有两个滑动方向了,就代表着两个业务逻辑。
这个时候我们应该怎么办?
这个时候 onDismissed: (direction) 中的 direction 就有用了:
我们找到 direction 的类为 DismissDirection,该类为一个枚举类:
/// The direction in which a [Dismissible] can be dismissed.
enum DismissDirection {
/// 上下滑动
vertical,
/// 左右滑动
horizontal,
/// 从右到左
endToStart,
/// 从左到右
startToEnd,
/// 向上滑动
up,
/// 向下滑动
down
}
那我们就可以根据上面的枚举来判断了:
onDismissed: (direction) {
var _snackStr;
if(direction == DismissDirection.endToStart){
// 从右向左 也就是删除
_snackStr = '删除了${_listData[index]}';
}else if (direction == DismissDirection.startToEnd){
_snackStr = '收藏了${_listData[index]}';
}
// 展示 SnackBar
Scaffold.of(context).showSnackBar(SnackBar(
content: Text(_snackStr),
));
// 删除后刷新列表,以达到真正的删除
setState(() {
_listData.removeAt(index);
});
},
效果如下:
看到这肯定有人觉得,这手一抖不就删除了么,能不能有什么操作来防止误操作?
那肯定有啊,你能想到的,Google都想好了,还是来看构造函数:
const Dismissible({
@required Key key,
@required this.child,
this.background,
this.secondaryBackground,
this.confirmDismiss,
this.onResize,
this.onDismissed,
this.direction = DismissDirection.horizontal,
this.resizeDuration = const Duration(milliseconds: 300),
this.dismissThresholds = const <DismissDirection, double>{},
this.movementDuration = const Duration(milliseconds: 200),
this.crossAxisEndOffset = 0.0,
this.dragStartBehavior = DragStartBehavior.start,
}) : assert(key != null),
assert(secondaryBackground != null ? background != null : true),
assert(dragStartBehavior != null),
super(key: key);
看没看到一个 confirmDismiss ?,就是它,来看一下源码:
/// Gives the app an opportunity to confirm or veto a pending dismissal.
///
/// If the returned Future<bool> completes true, then this widget will be
/// dismissed, otherwise it will be moved back to its original location.
///
/// If the returned Future<bool> completes to false or null the [onResize]
/// and [onDismissed] callbacks will not run.
final ConfirmDismissCallback confirmDismiss;
大致意思就是:
使应用程序有机会是否决定dismiss。
如果返回的future<bool>为true,则该小部件将被dismiss,否则它将被移回其原始位置。
如果返回的future<bool>为false或空,则不会运行[onResize]和[ondismissed]回调。
既然如此,我们就在该方法中,show 一个Dialog来判断用户是否删除:
confirmDismiss: (direction) async {
var _confirmContent;
var _alertDialog;
if (direction == DismissDirection.endToStart) {
// 从右向左 也就是删除
_confirmContent = '确认删除${_listData[index]}?';
_alertDialog = _createDialog(
_confirmContent,
() {
// 展示 SnackBar
Scaffold.of(context).showSnackBar(SnackBar(
content: Text('确认删除${_listData[index]}'),
duration: Duration(milliseconds: 400),
));
Navigator.of(context).pop(true);
},
() {
// 展示 SnackBar
Scaffold.of(context).showSnackBar(SnackBar(
content: Text('不删除${_listData[index]}'),
duration: Duration(milliseconds: 400),
));
Navigator.of(context).pop(false);
},
);
} else if (direction == DismissDirection.startToEnd) {
_confirmContent = '确认收藏${_listData[index]}?';
_alertDialog = _createDialog(
_confirmContent,
() {
// 展示 SnackBar
Scaffold.of(context).showSnackBar(SnackBar(
content: Text('确认收藏${_listData[index]}'),
duration: Duration(milliseconds: 400),
));
Navigator.of(context).pop(true);
},
() {
// 展示 SnackBar
Scaffold.of(context).showSnackBar(SnackBar(
content: Text('不收藏${_listData[index]}'),
duration: Duration(milliseconds: 400),
));
Navigator.of(context).pop(false);
},
);
}
var isDismiss = await showDialog(
context: context,
builder: (context) {
return _alertDialog;
});
return isDismiss;
},
解释一下上面的代码。
首先判断滑动的方向,然后根据创建的方向来创建Dialog 以及 点击事件。
最后点击时通过 Navigator.pop()来返回值。
效果如下: