Flutter 滑动删除最佳实践

2019-07-24 10:41:59 浏览数 (1)

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

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

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

Dismissible

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

代码语言:javascript复制
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中拖动或投掷该组件会导致该组件滑出视图。

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

代码语言:javascript复制
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:

代码语言:javascript复制
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 被删除了,但是实际上并没有,因为我们没对数据源进行处理。

添加删除逻辑
代码语言:javascript复制
// 创建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参数。

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

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

代码如下:

代码语言:javascript复制
onDismissed: (direction) {

  // 展示 SnackBar
  Scaffold.of(context).showSnackBar(SnackBar(
    content: Text('删除了${_listData[index]}'),
  ));

  // 删除后刷新列表,以达到真正的删除
  setState(() {
    _listData.removeAt(index);
  });

},
增加视觉效果

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

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

还是来看构造函数:

代码语言:javascript复制
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。

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

代码语言:javascript复制
/// 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,那么左滑右滑都是它自己。

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

代码语言:javascript复制
background: Container(
  color: Colors.red,
  // 这里使用 ListTile 因为可以快速设置左右两端的Icon
  child: ListTile(
    leading: Icon(
      Icons.bookmark,
      color: Colors.white,
    ),
    trailing: Icon(
      Icons.delete,
      color: Colors.white,
    ),
  ),
),

效果如下:

再来两个背景的:

代码语言:javascript复制
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,该类为一个枚举类:

代码语言:javascript复制
/// The direction in which a [Dismissible] can be dismissed.
enum DismissDirection {
  /// 上下滑动
  vertical,

  /// 左右滑动
  horizontal,

  /// 从右到左
  endToStart,

    /// 从左到右
  startToEnd,

  /// 向上滑动
  up,

  /// 向下滑动
  down
}

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

代码语言:javascript复制
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都想好了,还是来看构造函数:

代码语言:javascript复制
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 ?,就是它,来看一下源码:

代码语言:javascript复制
/// 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;

大致意思就是:

代码语言:javascript复制
使应用程序有机会是否决定dismiss。

如果返回的future<bool>为true,则该小部件将被dismiss,否则它将被移回其原始位置。

如果返回的future<bool>为false或空,则不会运行[onResize]和[ondismissed]回调。

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

代码语言:javascript复制
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()来返回值。

效果如下:

0 人点赞