Flutter 滑动删除最佳实践

原文链接: https://juejin.im/post/5cebd65751882532b9301b22

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

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

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

Dismissible

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

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 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 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 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。

如果返回的futuretrue,则该小部件将被dismiss,否则它将被移回其原始位置。

如果返回的futurefalse或空,则不会运行[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()来返回值。

效果如下:

总结

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

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

完整代码已经传至GitHub:github.com/wanglu1209/…

觉得不错,可以关注一下公众号,每天分享 Flutter & Dart 知识。

转载于:https://juejin.im/post/5cebd65751882532b9301b22

你可能感兴趣的:(Flutter 滑动删除最佳实践)