在移動端所謂的用戶交互事件既是用戶的手勢操作處理。
手勢操作在flutter中可分為兩類:
- 第一類是原始的指針事件(Pointer Event),即原生開發(fā)中常見的觸摸事件,表示屏幕上觸摸(或鼠標、手寫筆)行為觸發(fā)所的位移行為。
- 第二類則是手勢識別(Gesture Detector),表示多個原生指針事件的組合操作,如點擊、雙擊、長按、等,是指針事件的語義化封裝。
指針事件
在移動端,各個平臺或UI系統(tǒng)的原始指針事件模型基本都是一致,即:一次完整的事件分為三個階段:手指按下(PointerDownEvent)、手指移動(PointerMoveEvent)、和手指抬起(PointerUpEvent),還有觸摸取消事件(PointerCancelEvent)。
從官方文檔中我們可以了解到:指針按下事件(以及該指針的后續(xù)事件)然后被分發(fā)到由命中測試發(fā)現(xiàn)的最內(nèi)部的組件,然后從那里開始,事件會在組件樹中向上冒泡,這些事件會從最內(nèi)部的組件被分發(fā)到組件樹根的路徑上的所有組件。
也就是說flutter的指針事件機制也是基于冒泡事件進行處理的,但是這種冒泡卻無法自主停止。只能通過命中測試(Hit Test)去調(diào)整組件在生命中測試期內(nèi)該如何表現(xiàn),比如把觸摸事件交給子組件,或者交給其他視圖層級之下的組件去響應。
Flutter中可以使用Listener來監(jiān)聽原始觸摸事件,按照本書對組件的分類,則Listener也是一個功能性組件。下面是Listener的構(gòu)造函數(shù)定義:
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(()=> print("down $event"),//手勢按下回調(diào)
onPointerMove: (PointerMoveEvent event) => setState(()=>print("down $event"),//手勢移動回調(diào)
onPointerUp: (PointerUpEvent event) => setState(()=>print("down $event"),//手勢抬起回調(diào)
),
通過拖拽綠色區(qū)塊可以看到控制臺打印如下:
I/flutter (13829): up PointerUpEvent(Offset(97.7, 287.7))
I/flutter (13829): down PointerDownEvent(Offset(110.7, 317.7))
I/flutter (13829): move PointerMoveEvent(Offset(112.9, 317.7))
I/flutter (13829): move PointerMoveEvent(Offset(114.2, 317.7))
I/flutter (13829): up PointerUpEvent(Offset(117.1, 315.7))
上面說到的命中測試是通過PointerEvent中的 behavior 屬性控制的。
改屬性的值類型為HitTestBehavior,這是一個枚舉類,有三個枚舉值:
- deferToChild:子組件會一個接一個的進行命中測試,如果子組件中有測試通過的,則當前組件通過,這就意味著,如果指針事件作用于子組件上時,其父級組件也肯定可以收到該事件。
- opaque:在命中測試時,將當前組件當成不透明處理(即使本身是透明的),最終的效果相當于當前Widget的整個區(qū)域都是點擊區(qū)域。舉個例子:
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")
),
上例中,只有點擊文本內(nèi)容區(qū)域才會觸發(fā)點擊事件,因為 deferToChild 會去子組件判斷是否命中測試,而該例中子組件就是 Text("Box A") 。 如果我們想讓整個300×150的矩形區(qū)域都能點擊我們可以將behavior設為HitTestBehavior.opaque。注意,該屬性并不能用于在組件樹中攔截(忽略)事件,它只是決定命中測試時的組件大小。
- translucent:當點擊組件透明區(qū)域時,可以對自身邊界內(nèi)及底部可視區(qū)域都進行命中測試,這意味著點擊頂部組件透明區(qū)域時,頂部組件和底部組件都可以接收到事件,例如:
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范圍內(nèi)非文本區(qū)域點擊")),
),
onPointerDown: (event) => print("down1"),
//behavior: HitTestBehavior.translucent, //放開此行注釋后可以"點透"
)
],
)
上例中,當注釋掉最后一行代碼后,在左上角200*100范圍內(nèi)非文本區(qū)域點擊時(頂部組件透明區(qū)域),控制臺只會打印“down0”,也就是說頂部組件沒有接收到事件,而只有底部接收到了。當放開注釋后,再點擊時頂部和底部都會接收到事件,此時會打?。?br> I/flutter ( 3039): down1
I/flutter ( 3039): down0
如果behavior值改為HitTestBehavior.opaque,則只會打印"down1"。
手勢識別
flutter 是通過GestureDetector和GestureRecognizer處理手勢。
GestureDetector 常用的方法:
- onTap:點擊
- onDoubleTap:雙擊
- onLongPress:長按
- onPanUpdate:拖拽
- onScaleUpdate:縮放
- 等等
flutter支持手勢監(jiān)聽多個事件,也就是說可以同時監(jiān)聽多個事件。但是由于有手勢競技場(下面解釋)出現(xiàn),因此每次只有一個事件能競技成功也就是能得到相應。
如下例子所示:假設有一個widget,它可以左右拖動,現(xiàn)在我們也想檢測在它上面手指按下和抬起的事件,代碼如下:
class GestureConflictTestRouteState extends State<GestureConflictTestRoute> {
double _left = 0.0;
@override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
Positioned(
left: _left,
child: GestureDetector(
child: CircleAvatar(child: Text("B")), //要拖動和點擊的widget
onHorizontalDragUpdate: (DragUpdateDetails details) {
setState(() {
_left += details.delta.dx;
});
},
onHorizontalDragEnd: (details){
print("onHorizontalDragEnd");
},
onTapDown: (details){
print("down");
},
onTapUp: (details){
print("up");
},
),
)
],
);
}
}
現(xiàn)在我們按住圓形“B”拖動然后抬起手指,控制臺日志如下:
I/flutter (17539): down
I/flutter (17539): onHorizontalDragEnd
具體原因:剛開始按下手指時在沒有移動時,拖動手勢還沒有完整的語義,此時TapDown手勢勝出(win),此時打印"down",而拖動時,拖動手勢會勝出,當手指抬起時,onHorizontalDragEnd 和 onTapUp發(fā)生了沖突,但是因為是在拖動的語義中,所以onHorizontalDragEnd勝出,所以就會打印 “onHorizontalDragEnd”。
當然如果只是單純想要得到按下,抬起,以及拖拽觸發(fā),按下和抬起這種單一事件是可以通過結(jié)合Listener指針事件進行處理。
Positioned(
top:80.0,
left: _leftB,
child: Listener(
onPointerDown: (details) {
print("down");
},
onPointerUp: (details) {
//會觸發(fā)
print("up");
},
child: GestureDetector(
child: CircleAvatar(child: Text("C")),
onHorizontalDragUpdate: (DragUpdateDetails details) {
setState(() {
_leftB += details.delta.dx;
});
},
onHorizontalDragEnd: (details) {
print("onHorizontalDragEnd");
},
),
),
)
如上寫法:可以監(jiān)聽到用戶點擊,拖拽,以及抬起事件。
手勢競技場
其實現(xiàn)方式是通過GestureDetector內(nèi)部對每一個手勢都建立了一個工廠類(Gesture Factory)。而工廠類內(nèi)部都會使用手勢識別類(GestureRecognizer),來確定當前處理的手勢。
而所有手勢的工廠類都會交給RawGestureDetector類,以完成檢測手勢大量工作:使用Listener監(jiān)聽原始指針事件,并在狀態(tài)改變時把信息同步給所有的手勢識別器。然后這些手勢會在競技場決定最后又誰來響應用戶事件。
但現(xiàn)實場景中也存在當點擊時需要父類事件和子類事件同時執(zhí)行響應的操作。如下代碼:
GestureDetector(
onTap: ()=> print('parent tap');
child:Container(
color:Colors.pinkAccent,
child: GestureDetector(
onTap: ()=> print('child tap');
child:Container(
color:Colors.red,
width:200.0,
height:200.0,
),
)
),
);
運行后點擊藍色區(qū)域,可以發(fā)現(xiàn)只打印了 Child tap;
因此為了能讓父類也能接收到手勢,我們需要同時使用RawGestureGetector和GestureFactory,來改變競技場決定誰來響應事件的結(jié)果。
① 我們需要自定義一個手勢識別器,讓改手勢識別器在競技失敗時也能夠把自己重新添加回來,以便接下來能繼續(xù)去響應用戶事件。
如下定義了一個繼承點擊手勢識別器TapGestureRecognizer的類并重寫了器rejectGesture方法,手動的把自己復活:
class MultipleTapGesture extends TapGestureRecognizer{
@override
void rejectGesture(int pointer) {
acceptGesture(pointer);
}
}
② 需要將手勢識別器和其工廠類傳遞給RawGestureDetector,以便用戶產(chǎn)生手勢交互事件時能夠立刻找到對應的識別方法。RawGestureDetector的初始化函數(shù)所做的配置工作,就是定義手勢識別器和其工廠類的映射關(guān)系。
如下所示:點擊藍色區(qū)域時,其父類容器也收到了Tap事件
RawGestureDetector(// 自己構(gòu)造父 Widget 的手勢識別映射關(guān)系
gestures: {
// 建立多手勢識別器與手勢識別工廠類的映射關(guān)系,從而返回可以響應該手勢的 recognizer
MultipleTapGesture: GestureRecognizerFactoryWithHandlers<
MultipleTapGesture>(
() => MultipleTapGesture(),
(MultipleTapGesture instance) {
instance.onTap = () => print('parent tapped ');// 點擊回調(diào)
},
)
},
child: Container(
color: Colors.pinkAccent,
child: Center(
child: GestureDetector(// 子視圖可以繼續(xù)使用 GestureDetector
onTap: () => print('Child tapped'),
child: Container(...),
),
),
),
);