一,概述
移動應用中一個必不可少的環節就是與用戶的交互,在Flutter中提供的手勢檢測為GestureDetector。 Flutter中的手勢系統分為二層:
- 第一層是觸摸原事件(指針)
- PointerDownEvent:用戶與屏幕接觸產生了聯系
- PointerMoveEvent:手指已從屏幕上的一個位置移動到另一個位置
- PointMoveEvent:指針停止接觸屏幕
- PointerUpEvent:用戶已停止接觸屏幕
- PointerCanceEvent:此指針的輸入不再指向此應用程序
- 第二層是手勢事件(輕擊,拖動,縮放)
- 自帶交互的控件監聽
- RaisedButton、
- IconButton、
- OutlineButton、
- Checkbox、
- SnackBar、
- Switch等
- 不自帶交互的控件監聽
- 用GestureDelector進行手勢檢測
- 用Dismissible實現滑動刪除
- 自帶交互的控件監聽
二,手勢事件
-
1.自帶交互的控件
在Flutter中,自帶如點擊事件的控件有RaisedButton、IconButton、OutlineButton、Checkbox、SnackBar、Switch等,如下面給OutlineButton添加點擊事件:body:Center( child: OutlineButton( child: Text('點擊我'), onPressed: (){ Fluttertoast.showToast( msg: '你點擊了FlatButton', toastLength: Toast.LENGTH_SHORT, gravity: ToastGravity.CENTER, timeInSecForIos: 1, ); }), ),
上面代碼就可以捕捉OutlineButton的點擊事件。
- 2.不自帶交互的控件
- GestureDetector
很多控件不像RaisedButton、OutlineButton等已經對presses(taps)或手勢做出了響應。那么如果要監聽這些控件的手勢就需要用另一個控件GestureDetector,那看看源碼GestureDetector支持哪些手勢:
GestureDetector({ Key key, this.child, this.onTapDown,//按下,每次和屏幕交互都會調用 this.onTapUp,//抬起,停止觸摸時調用 this.onTap,//點擊,短暫觸摸屏幕時調用 this.onTapCancel,//取消 觸發了onTapDown,但沒有完成onTap this.onDoubleTap,//雙擊,短時間內觸摸屏幕兩次 this.onLongPress,//長按,觸摸時間超過500ms觸發 this.onLongPressUp,//長按松開 this.onVerticalDragDown,//觸摸點開始和屏幕交互,同時豎直拖動按下 this.onVerticalDragStart,//觸摸點開始在豎直方向拖動開始 this.onVerticalDragUpdate,//觸摸點每次位置改變時,豎直拖動更新 this.onVerticalDragEnd,//豎直拖動結束 this.onVerticalDragCancel,//豎直拖動取消 this.onHorizontalDragDown,//觸摸點開始跟屏幕交互,並水平拖動 this.onHorizontalDragStart,//水平拖動開始,觸摸點開始在水平方向移動 this.onHorizontalDragUpdate,//水平拖動更新,觸摸點更新 this.onHorizontalDragEnd,//水平拖動結束觸發 this.onHorizontalDragCancel,//水平拖動取消 onHorizontalDragDown沒有成功觸發 //onPan可以取代onVerticalDrag或者onHorizontalDrag,三者不能並存 this.onPanDown,//觸摸點開始跟屏幕交互時觸發 this.onPanStart,//觸摸點開始移動時觸發 this.onPanUpdate,//屏幕上的觸摸點位置每次改變時,都會觸發這個回調 this.onPanEnd,//pan操作完成時觸發 this.onPanCancel,//pan操作取消 //onScale可以取代onVerticalDrag或者onHorizontalDrag,三者不能並存,不能與onPan並存 this.onScaleStart,//觸摸點開始跟屏幕交互時觸發,同時會建立一個焦點為1.0 this.onScaleUpdate,//跟屏幕交互時觸發,同時會標示一個新的焦點 this.onScaleEnd,//觸摸點不再跟屏幕交互,標示這個scale手勢完成 this.behavior, this.excludeFromSemantics = false })
直接上例子:-
2.1.onTapXXX
child: GestureDetector( child: Container( width: 300.0, height: 300.0, color:Colors.red, ), onTapDown: (d){ print("onTapDown"); }, onTapUp: (d){ print("onTapUp"); }, onTap:(){ print("onTap"); }, onTapCancel: (){ print("onTaoCancel"); }, )
點了一下,並且抬起,結果是:
I/flutter (16304): onTapDown I/flutter (16304): onTapUp I/flutter (16304): onTap
先觸發onTapDown 然后onTapUp 繼續onTap
-
2.2.onLongXXX
//手勢測試 Widget gestureTest = GestureDetector( child: Container( width: 300.0, height: 300.0, color:Colors.red, ), onDoubleTap: (){ print("雙擊onDoubleTap"); }, onLongPress: (){ print("長按onLongPress"); }, onLongPressUp: (){ print("長按抬起onLongPressUP"); }, );
實際結果:
I/flutter (16304): 長按onLongPress I/flutter (16304): 長按抬起onLongPressUP I/flutter (16304): 雙擊onDoubleTap
-
2.3.onVerticalXXX
//手勢測試 Widget gestureTest = GestureDetector( child: Container( width: 300.0, height: 300.0, color:Colors.red, ), onVerticalDragDown: (_){ print("豎直方向拖動按下onVerticalDragDown:"+_.globalPosition.toString()); }, onVerticalDragStart: (_){ print("豎直方向拖動開始onVerticalDragStart"+_.globalPosition.toString()); }, onVerticalDragUpdate: (_){ print("豎直方向拖動更新onVerticalDragUpdate"+_.globalPosition.toString()); }, onVerticalDragCancel: (){ print("豎直方向拖動取消onVerticalDragCancel"); }, onVerticalDragEnd: (_){ print("豎直方向拖動結束onVerticalDragEnd"); }, );
輸出結果:
I/flutter (16304): 豎直方向拖動按下onVerticalDragDown:Offset(191.7, 289.3) I/flutter (16304): 豎直方向拖動開始onVerticalDragStartOffset(191.7, 289.3) I/flutter (16304): 豎直方向拖動更新onVerticalDragUpdateOffset(191.7, 289.3) I/flutter (16304): 豎直方向拖動更新onVerticalDragUpdateOffset(191.7, 289.3) I/flutter (16304): 豎直方向拖動更新onVerticalDragUpdateOffset(191.7, 289.3) I/flutter (16304): 豎直方向拖動更新onVerticalDragUpdateOffset(191.7, 289.3) I/flutter (16304): 豎直方向拖動更新onVerticalDragUpdateOffset(191.7, 289.3) I/flutter (16304): 豎直方向拖動更新onVerticalDragUpdateOffset(191.3, 290.0) I/flutter (16304): 豎直方向拖動更新onVerticalDragUpdateOffset(191.3, 291.3) I/flutter (16304): 豎直方向拖動結束onVerticalDragEnd
-
2.4.onPanXXX
//手勢測試 Widget gestureTest = GestureDetector( child: Container( width: 300.0, height: 300.0, color:Colors.red, ), onPanDown: (_){ print("onPanDown"); }, onPanStart: (_){ print("onPanStart"); }, onPanUpdate: (_){ print("onPanUpdate"); }, onPanCancel: (){ print("onPanCancel"); }, onPanEnd: (_){ print("onPanEnd"); }, );
無論豎直拖動還是橫向拖動還是一起來,結果如下:
I/flutter (16304): onPanDown I/flutter (16304): onPanStart I/flutter (16304): onPanUpdate I/flutter (16304): onPanUpdate I/flutter (16304): onPanEnd
-
2.5.onScaleXXX
//手勢測試 Widget gestureTest = GestureDetector( child: Container( width: 300.0, height: 300.0, color:Colors.red, ), onScaleStart: (_){ print("onScaleStart"); }, onScaleUpdate: (_){ print("onScaleUpdate"); }, onScaleEnd: (_){ print("onScaleEnd");
} );無論點擊、豎直拖動、水平拖動,結果如下:
I/flutter (16304): onScaleStart I/flutter (16304): onScaleUpdate I/flutter (16304): onScaleUpdate I/flutter (16304): onScaleUpdate I/flutter (16304): onScaleUpdate I/flutter (16304): onScaleUpdate I/flutter (16304): onScaleUpdate I/flutter (16304): onScaleUpdate I/flutter (16304): onScaleEnd
-
-
- 用dismissible實現滑動刪除
滑動刪除模式在很多移動應用中很常見。例如,我們在整理手機通訊錄時,希望能快速刪除一些聯系人,一般手指輕輕一滑即可以實現刪除功能。Flutter提供了Dismissible組件使這項任務變得簡單。
- 構造函數
/** * 滑動刪除 * * const Dismissible({ @required Key key,// @required this.child,// this.background,//滑動時組件下一層顯示的內容,沒有設置secondaryBackground時,從右往左或者從左往右滑動都顯示該內容,設置了secondaryBackground后,從左往右滑動顯示該內容,從右往左滑動顯示secondaryBackground的內容 //secondaryBackground不能單獨設置,只能在已經設置了background后才能設置,從右往左滑動時顯示 this.secondaryBackground,// 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,// }) */
-
示例demo
/*** * 滑動刪除 */ class MyListView extends StatelessWidget { var list = ['第一個','第二個','第三個','第四個','第五個','第六個']; @override Widget build(BuildContext context) { // TODO: implement build return new ListView.builder( itemCount: list.length, itemBuilder: (context,index){ var item = list[index]; return new Dismissible( key: Key(item), child: new ListTile( title: new Text(item), ), onDismissed: (direction){ list.remove(index); print(direction); }, background: Container( color: Colors.red, child: new Center( child: new Text('刪除', style: new TextStyle( color: Colors.white ) ), ), ), secondaryBackground: new Container( color: Colors.green, ), ); }, ); } }
- 構造函數
- 用dismissible實現滑動刪除
三,原始指針事件
在移動端,各個平台或UI系統的原始指針事件模型基本都是一致,即:一次完整的事件分為三個階段:手指按下、手指移動、和手指抬起,而更高級別的手勢(如點擊、雙擊、拖動等)都是基於這些原始事件的。
- 響應流程
當指針按下時,Flutter會對應用程序執行命中測試(Hit Test),以確定指針與屏幕接觸的位置存在哪些widget, 指針按下事件(以及該指針的后續事件)然后被分發到由命中測試發現的最內部的widget,然后從那里開始,事件會在widget樹中向上冒泡,這些事件會從最內部的widget被分發到widget根的路徑上的所有Widget,這和Web開發中瀏覽器的事件冒泡機制相似, 但是Flutter中沒有機制取消或停止冒泡過程,而瀏覽器的冒泡是可以停止的。
注意,只有通過命中測試的Widget才能觸發事件。
- 事件監聽
Flutter中可以使用Listener widget來監聽原始觸摸事件,它也是一個功能性widget。
Listener({ Key key, this.onPointerDown, //手指按下回調 this.onPointerMove, //手指移動回調 this.onPointerUp,//手指抬起回調 this.onPointerCancel,//觸摸事件取消回調 this.behavior = HitTestBehavior.deferToChild, //在命中測試期間如何表現 Widget child })
-
示例demo:
我們先看一個示例,后面再單獨討論一下behavior
屬性。... //定義一個狀態,保存當前指針位置 PointerEvent _event; ... Listener( child: Container( alignment: Alignment.center, color: Colors.blue, width: 300.0, height: 150.0, child: Text(_event?.toString()??"",style: TextStyle(color: Colors.white)), ), onPointerDown: (PointerDownEvent event) => setState(()=>_event=event), onPointerMove: (PointerMoveEvent event) => setState(()=>_event=event), onPointerUp: (PointerUpEvent event) => setState(()=>_event=event), ),
- 效果圖
-
手指在藍色矩形區域內移動即可看到當前指針偏移,當觸發指針事件時,參數PointerDownEvent、PointerMoveEvent、PointerUpEvent都是PointerEvent的一個子類,PointerEvent類中包括當前指針的一些信息,如:
-
-
position
:它是鼠標相對於當對於全局坐標的偏移。delta
:兩次指針移動事件(PointerMoveEvent)的距離。pressure
:按壓力度,如果手機屏幕支持壓力傳感器(如iPhone的3D Touch),此屬性會更有意義,如果手機不支持,則始終為1。orientation
:指針移動方向,是一個角度值。
-
上面只是PointerEvent一些常用屬性,除了這些它還有很多屬性,讀者可以查看API文檔。
-
- behavior屬性
我們重點來介紹一下,它決定子Widget如何響應命中測試,它的值類型為HitTestBehavior,這是一個枚舉類,有三個枚舉值:
- behavior屬性
-
-
-
deferToChild
:子widget會一個接一個的進行命中測試,如果子Widget中有測試通過的,則當前Widget通過,這就意味着,如果指針事件作用於子Widget上時,其父(祖先)Widget也肯定可以收到該事件。 -
opaque
:在命中測試時,將當前Widget當成不透明處理(即使本身是透明的),最終的效果相當於當前Widget的整個區域都是點擊區域。
舉個例子:Listener( child: ConstrainedBox( constraints: BoxConstraints.tight(Size(300.0, 150.0)), child: Center(child: Text("Box A")), ), //behavior: HitTestBehavior.opaque, onPointerDown: (event) => print("down A") ),
上例中,只有點擊文本內容區域才會觸發點擊事件,因為 deferToChild 會去子widget判斷是否命中測試,而該例中子widget就是 Text("Box A") 。 如果我們想讓整個300×150的矩形區域都能點擊我們可以將behavior設為HitTestBehavior.opaque。
注意,該屬性並不能用於在Widget樹中攔截(忽略)事件,它只是決定命中測試時的Widget大小。
-
translucent
:當點擊Widget透明區域時,可以對自身邊界內及底部可視區域都進行命中測試,這意味着點擊頂部widget透明區域時,頂部widget和底部widget都可以接收到事件,例如:Stack( children: <Widget>[ Listener( child: ConstrainedBox( constraints: BoxConstraints.tight(Size(300.0, 200.0)), child: DecoratedBox( decoration: BoxDecoration(color: Colors.blue)), ), onPointerDown: (event) => print("down0"), ), Listener( child: ConstrainedBox( constraints: BoxConstraints.tight(Size(200.0, 100.0)), child: Center(child: Text("左上角200*100范圍內非文本區域點擊")), ), onPointerDown: (event) => print("down1"), //behavior: HitTestBehavior.translucent, //放開此行注釋后可以"點透" ) ], )
上例中,當注釋掉最后一行代碼后,在左上角200*100范圍內非文本區域點擊時(頂部Widget透明區域),控制台只會打印“down0”,也就是說頂部widget沒有接收到事件,而只有底部接收到了。當放開注釋后,再點擊時頂部和底部都會接收到事件,此時會打印:
I/flutter ( 3039): down1 I/flutter ( 3039): down0
如果behavior值改為HitTestBehavior.opaque,則只會打印"down1"。
-
-
-
忽略PointerEvent
假如我們不想讓某個子樹響應PointerEvent的話,我們可以使用IgnorePointer和AbsorbPointer,這兩個Widget都能阻止子樹接收指針事件,不同之處在於AbsorbPointer本身會參與命中測試,而IgnorePointer本身不會參與,這就意味着AbsorbPointer本身是可以接收指針事件的(但其子樹不行),而IgnorePointer不可以。
一個簡單的例子如下:Listener( child: AbsorbPointer( child: Listener( child: Container( color: Colors.red, width: 200.0, height: 100.0, ), onPointerDown: (event)=>print("in"), ), ), onPointerDown: (event)=>print("up"), )
點擊Container時,由於它在
AbsorbPointer
的子樹上,所以不會響應指針事件,所以日志不會輸出"in",但AbsorbPointer
本身是可以接收指針事件的,所以會輸出"up"。如果將AbsorbPointer
換成IgnorePointer
,那么兩個都不會輸出。