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

相關文章
相關標籤/搜索