【Flutter 元件集錄】Dismissible| 8月更文挑戰

張風捷特烈發表於2021-08-02
前言:

為應掘金的八月更文挑戰,我準備在本月挑選 31 個以前沒有介紹過的元件,進行全面分析和屬性介紹。這些文章將來會作為 Flutter 元件集錄 的重要素材。希望可以堅持下去,你的支援將是我最大的動力~


一、認識 Dismissible 元件

今天來看一個和滑動相關的元件:Dismissible 。如下圖效果,該元件可以通過滑動來使條目移除。先來看一下它最簡單的使用。

左滑右滑

_HomePageState 中通過 ListView 展示 60 個條目。如下 tag1 處,在構建條目時在條目外層包裹 Dismissible 元件。 構造中傳入 keychild 入參。其中 key 用於標識條目,child 為條目元件。onDismissed 回撥是在條目被移除時被呼叫。

指定注意的是:Dismissible 元件滑動移除只是 UI 的效果,實際的資料並未被移除。為了保證資料UI 的一致性,我們一般在移除後,會同時移除對應的資料,並進行重建,如下 tag2

class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  List<String> data = List.generate(60, (index) => '第$index個');

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Dismissible 測試'),),
      body: ListView.builder(
        itemCount: data.length,
        itemBuilder: _buildItems,
      ),
    );
  }

  Widget _buildItems(BuildContext context, int index) {
    return Dismissible( //<---- tag1
      key: ValueKey<String>(data[index]),
      child: ItemBox(
        info: data[index],
      ),
      onDismissed: (direction) =>_onDismissed(direction,index),
    );
  }

  void _onDismissed(DismissDirection direction,int index) {
    setState(() {
      data.removeAt(index); //<--- tag 2
    });
  }
}
複製程式碼

其中 ItemBox 就是個高度為 56Container ,其中顯示一下文字資訊。

class ItemBox extends StatelessWidget {
  final String info;

  const ItemBox({Key? key, required this.info}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      alignment: Alignment.center,
      height: 56,
      child: Text(
        info,
        style: TextStyle(fontSize: 20),
      ),
    );
  }
}
複製程式碼

二、詳細瞭解 Dismissible 元件

上面我們已經簡單認識了 Dismissible 元件的使用。如下原始碼中可以看出,keychild 屬性是必選項,除此之外,還有很多其他的屬性。下面我們來逐一認識一下:


1、 background 和 secondaryBackground

Dismissible 元件滑動時,我們可以指定背景元件。如果只設定 background ,那麼左滑和右滑背景都是一樣的,如下左圖綠色背景。如果設定 backgroundsecondaryBackground ,則左滑背景為 background ,右滑背景為 secondaryBackground ,如下右圖。

單背景雙背景

程式碼實現也 很簡單,指定 backgroundsecondaryBackground 對於元件即可。如下 tag1tag2 處理。

Widget _buildItems(BuildContext context, int index) {
  return Dismissible(
    key: ValueKey(data[index]),
    background: buildBackground(), // tag1
    secondaryBackground: buildSecondaryBackground(), // tag2
    child: ItemBox(
      info: data[index],
    ),
    onDismissed: (direction) =>_onDismissed(direction,index),
  );
}

Widget buildBackground(){
  return Container(
    color: Colors.green,
    alignment: Alignment(-0.9, 0),
    child: Icon(
      Icons.check,
      color: Colors.white,
    ),
  );
}

Widget buildSecondaryBackground(){
  return Container(
    alignment: Alignment(0.9, 0),
    child: Icon(
      Icons.close,
      color: Colors.white,
    ),
    color: Colors.red,
  );
}
複製程式碼

2. confirmDismiss 回撥

從原始碼中可以看出 confirmDismiss 的型別為 ConfirmDismissCallback 。它是一個函式型別,可以回撥出 DismissDirection 物件,返回 bool 值。可以看出這個回撥是一個非同步方法,所以我們可以處理一下非同步事件。

---->[Dismissible#confirmDismiss 宣告]----
final ConfirmDismissCallback? confirmDismiss;

typedef ConfirmDismissCallback = Future<bool?> Function(DismissDirection direction);
複製程式碼

如下左圖中,滑動結束後,等待兩秒再執行後續邏輯。效果上來看條目會在兩秒後移除。也就說明 onDismissed 是在 confirmDismiss 非同步方法完成後才被呼叫的。

該回撥有一個 Future<bool?> 型別的返回值,返回 false 則表示不移除條目。如下右圖中,綠色背景下不會移除條目,紅色背景下移除條目。就可以通過該返回值進行控制。

執行非同步事件返回值的功效

程式碼實現如下, tag1 處設定 confirmDismiss 屬性。返回值是看 direction 是否不是 startToEnd,即 從左向右滑動 。也就是說, 從左向右滑動 時,會返回 false ,即不消除條目。

Widget _buildItems(BuildContext context, int index) {
  return Dismissible(
    key: ValueKey(data[index]),
    background: buildBackground(),
    secondaryBackground: buildSecondaryBackground(),
    child: ItemBox(
      info: data[index],
    ),
    onDismissed: (direction) =>_onDismissed(direction,index),
    confirmDismiss: _confirmDismiss, // tag1
  );
}

Future<bool?> _confirmDismiss(DismissDirection direction) async{
  await Future.delayed(Duration(seconds: 2));
  print('_confirmDismiss:$direction');
  return direction!=DismissDirection.startToEnd;
}
複製程式碼

3. direction 滑動方向

direction 表示滑動的方向,型別是 DismissDirection 是列舉,有 7 元素。

enum DismissDirection {
  vertical,
  horizontal,
  endToStart,
  startToEnd,
  up,
  down,
  none
}
複製程式碼

如下左圖中,設定 startToEnd ,那麼從右往左就無法滑動。如下右圖中,設定 vertical ,那條目就只能在豎直方向響應滑動。不過和列表同向滑動有個問題,條目響應了豎直拖拽手勢,那列表的拖拽手勢就會競技失敗,所以列表是滑不動的。一般來說不會讓 Dismissible 和列表滑動方向相同,當列表是水平方向滑動, Dismissible 可以使用豎直方向滑動。

startToEndvertical

4. onResize 和 resizeDuration

在豎直列表中,滑動消失時,下方條目會有一個 上移 的動畫。resizeDuration 就代表動畫時長,而 onResize 會在動畫執行的每幀中進行回撥。

預設時長2s

原始碼中可以看出 resizeDuration 的預設時長為 300 ms

在深入瞄一眼,可以看出會監聽動畫器執行 _handleResizeProgressChanged 。而 onResize 就是在此觸發的。另外這個動畫的曲線是 _kResizeTimeCurve

void _handleResizeProgressChanged() {
  if (_resizeController!.isCompleted) {
    widget.onDismissed?.call(_dismissDirection);
  } else {
    widget.onResize?.call();
  }
}

const Curve _kResizeTimeCurve = Interval(0.4, 1.0, curve: Curves.ease);
複製程式碼

5.dismissThresholds 和 movementDuration

dismissThresholds 表示消失的閾值,型別為Map<DismissDirection, double> 對映,也就是說我們可以設定不同滑動方向的容忍度, 預設是 0.4 。而 movementDuration 代表滑動方向上移動的動畫時長。

const double _kDismissThreshold = 0.4;

final Map<DismissDirection, double> dismissThresholds;
複製程式碼
預設效果本案例效果

下面程式碼的效果如上圖右側,當 startToEnd 的宇宙設定為 0.8 , 就會比預設的 難觸發移除事件 。其中 movementDuration 設定為 3 s 可以很明顯地看出,水平移動的慢速。

Widget _buildItems(BuildContext context, int index) {
  return Dismissible(
    key: ValueKey(data[index]),
    background: buildBackground(),
    secondaryBackground: buildSecondaryBackground(),
    onResize: _onResize,
    resizeDuration: const Duration(seconds: 2),
    dismissThresholds: {
      DismissDirection.startToEnd: 0.8,
      DismissDirection.endToStart: 0.2,
    },
    movementDuration: const Duration(seconds: 3),
    child: ItemBox(
      info: data[index],
    ),
    direction: DismissDirection.horizontal,
    onDismissed: (direction) => _onDismissed(direction, index),
    confirmDismiss: _confirmDismiss,
  );
}
複製程式碼

6. crossAxisEndOffset 交叉軸偏移

如下圖,是 crossAxisEndOffset-2 的效果,在滑動過程中,原條目在交叉軸(此處為縱軸)會發生偏移,偏移量就是 crossAxisEndOffset * 元件高 。右圖所示,滑動到一般時, 條目 4 已經上移了一個條目高度。

12

最後 dragStartBehaviorbehavior 就不說了,這種通用的屬性大家應該非常清楚。


三、從 Dismissible 原始碼中可以學到什麼

Dismissible 元件中的 confirmDismissonDismissed 兩個回撥打的一個組合拳,還是非常巧妙的,在實際開發中我們也可以通過非同步回撥來處理一些介面效果。我們來看一下原始碼中的實現: confirmDismiss 回撥在 _confirmStartResizeAnimation 方法中進行呼叫,

在拖拽結束,會先等待 _confirmStartResizeAnimation 的執行,且返回 true ,才會執行 _startResizeAnimation

另外一處是在 _moveController 動畫器執行完畢,如果動畫完成,也會執行類似邏輯。

最後 onDismissed 回撥會在 _startResizeAnimation 中觸發。這也就是如何通過一個非同步方法,來控制另一個回撥的觸發。


Dismissible 元件的使用方式到這裡就完全介紹完畢,那本文到這裡就結束了,謝謝觀看,明天見~

相關文章