前言:
这是我参与8月更文挑战的第 2 天,活动详情查看:8月更文挑战。为应掘金的八月更文挑战
,我准备在本月挑选 31
个以前没有介绍过的组件,进行全面分析和属性介绍。这些文章将来会作为 Flutter 组件集录
的重要素材。希望可以坚持下去,你的支持将是我最大的动力~
| 本系列 | 组件文章 | 列表 | | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | | 1.NotificationListener | 2.Dismissible | 3.Switch | | 4.Scrollbar | 5.ClipPath | 6.CupertinoActivityIndicator | | 7.Opacity | 8.FadeTransition | 9. AnimatedOpacity | | 10. FadeInImage | 11. Offstage | 12. TickerMode | | 13. Visibility | 14. Padding | 15. AnimatedContainer | | 16.CircleAvatar | 17.PhysicalShape | 18.Divider | | 19.Flexible、Expanded 和 Spacer | 20.Card | |
一、认识 Dismissible 组件
今天来看一个和滑动相关的组件:Dismissible
。如下图效果,该组件可以通过滑动来使条目移除。先来看一下它最简单的使用。
| 左滑 | 右滑 | | ---- | ---- | | |
|
_HomePageState
中通过 ListView
展示 60 个条目。如下 tag1
处,在构建条目时在条目外层包裹 Dismissible
组件。 构造中传入 key
和 child
入参。其中 key
用于标识条目,child
为条目组件。onDismissed
回调是在条目被移除时被调用。
指定注意的是:Dismissible
组件滑动移除只是 UI 的效果,实际的数据并未被移除。为了保证数据
与 UI
的一致性,我们一般
在移除后,会同时移除对应的数据,并进行重建,如下 tag2
。
```dart class HomePage extends StatefulWidget { const HomePage({Key? key}) : super(key: key);
@override _HomePageState createState() => _HomePageState(); }
class _HomePageState extends State { List 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 (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
,其中显示一下文字信息。
```dart 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
处理。
```dart 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
值。可以看出这个回调是一个异步方法
,所以我们可以处理一下异步事件。
```dart ---->[Dismissible#confirmDismiss 声明]---- final ConfirmDismissCallback? confirmDismiss;
typedef ConfirmDismissCallback = Future Function(DismissDirection direction); ```
如下左图中,滑动结束后,等待两秒
再执行后续逻辑。效果上来看条目会在两秒后移除。也就说明 onDismissed
是在 confirmDismiss
异步方法完成后才被调用的。
该回调有一个 Future<bool?>
类型的返回值,返回 false
则表示不移除条目。如下右图中,绿色背景下不会
移除条目,红色背景下会
移除条目。就可以通过该返回值进行控制。
| 执行异步事件 | 返回值的功效 | | ------------------------------------------------------------ | ------------------------------------------------------------ | | |
|
代码实现如下, tag1
处设置 confirmDismiss
属性。返回值是看 direction
是否不是 startToEnd
,即 从左向右滑动
。也就是说, 从左向右滑动
时,会返回 false
,即不消除条目。
```dart 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 confirmDismiss(DismissDirection direction) async{ await Future.delayed(Duration(seconds: 2)); print('confirmDismiss:$direction'); return direction!=DismissDirection.startToEnd; } ```
3. direction 滑动方向
direction
表示滑动的方向,类型是 DismissDirection
是枚举,有 7
元素。
dart 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
。
```dart 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
代表滑动方向上移动的动画时长。
```dart const double _kDismissThreshold = 0.4;
final Map dismissThresholds; ```
| 默认效果 | 本案例效果 | | ------------------------------------------------------------ | ------------------------------------------------------------ | | |
|
下面代码的效果如上图右侧,当 startToEnd
的宇宙设置为 0.8
, 就会比默认的 难触发移除事件
。其中 movementDuration
设置为 3 s
可以很明显地看出,水平移动的慢速。
dart 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 组件
的使用方式到这里就完全介绍完毕,那本文到这里就结束了,谢谢观看,明天见~