前言:
為應掘金的八月更文挑戰
,我準備在本月挑選 31
個以前沒有介紹過的元件,進行全面分析和屬性介紹。這些文章將來會作為 Flutter 元件集錄
的重要素材。希望可以堅持下去,你的支援將是我最大的動力~
- 1.【Flutter 元件集錄】NotificationListener| 8月更文挑戰
- 2.【Flutter 元件集錄】Dismissible| 8月更文挑戰
[本文]
一、認識 Dismissible 元件
今天來看一個和滑動相關的元件:Dismissible
。如下圖效果,該元件可以通過滑動來使條目移除。先來看一下它最簡單的使用。
左滑 | 右滑 |
---|---|
_HomePageState
中通過 ListView
展示 60 個條目。如下 tag1
處,在構建條目時在條目外層包裹 Dismissible
元件。 構造中傳入 key
和 child
入參。其中 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
就是個高度為 56
的 Container
,其中顯示一下文字資訊。
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
元件的使用。如下原始碼中可以看出,key
和 child
屬性是必選項,除此之外,還有很多其他的屬性。下面我們來逐一認識一下:
1、 background 和 secondaryBackground
Dismissible
元件滑動時,我們可以指定背景元件。如果只設定 background
,那麼左滑和右滑背景都是一樣的,如下左圖綠色背景。如果設定 background
和 secondaryBackground
,則左滑背景為 background
,右滑背景為 secondaryBackground
,如下右圖。
單背景 | 雙背景 |
---|---|
程式碼實現也 很簡單,指定 background
和 secondaryBackground
對於元件即可。如下 tag1
和 tag2
處理。
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
可以使用豎直方向滑動。
startToEnd | vertical |
---|---|
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
已經上移了一個條目高度。
1 | 2 |
---|---|
最後 dragStartBehavior
和 behavior
就不說了,這種通用的屬性大家應該非常清楚。
三、從 Dismissible 原始碼中可以學到什麼
Dismissible
元件中的 confirmDismiss
和 onDismissed
兩個回撥打的一個組合拳
,還是非常巧妙的,在實際開發中我們也可以通過非同步回撥
來處理一些介面效果。我們來看一下原始碼中的實現:
confirmDismiss
回撥在 _confirmStartResizeAnimation
方法中進行呼叫,
在拖拽結束,會先等待 _confirmStartResizeAnimation
的執行,且返回 true
,才會執行 _startResizeAnimation
。
另外一處是在 _moveController
動畫器執行完畢,如果動畫完成,也會執行類似邏輯。
最後 onDismissed
回撥會在 _startResizeAnimation
中觸發。這也就是如何通過一個非同步方法,來控制另一個回撥的觸發。
Dismissible 元件
的使用方式到這裡就完全介紹完畢,那本文到這裡就結束了,謝謝觀看,明天見~