Flutter 滑动删除最佳实践

在Gmail中,咱们常常会看到以下效果:git

滑动去存档,也能够滑动删除。github

那做为Google 自家出品的Flutter,固然也会有这种组件。bash

Dismissible

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

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

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

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

child没必要多说,就是咱们须要滑动删除的组件,那key是什么?ide

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

如今咱们只须要理解,key 是 widget 的惟一标示。由于有了key,因此 widget tree 才知道咱们删除了什么widget。ui

简单使用

知道了须要传什么参数,那咱们开始撸一个demo:this

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()来返回值。

效果以下:

总结

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

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

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

以为不错,能够关注一下公众号,天天分享 Flutter & Dart 知识。

相关文章
相关标签/搜索