手勢(shì)的識(shí)別和處理都是在事件分發(fā)階段的
1.手勢(shì)識(shí)別原理
GestureDetector 是一個(gè) StatelessWidget浊闪,返回的是一個(gè) RawGestureDetector蛾茉。
GestureDetector Build方法 源碼
@override
Widget build(BuildContext context) {
final gestures = <Type, GestureRecognizerFactory>{};
// 構(gòu)建 TapGestureRecognizer
if (onTapDown != null ||
onTapUp != null ||
onTap != null ||
... //省略
) {
gestures[TapGestureRecognizer] = GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
() => TapGestureRecognizer(debugOwner: this),
(TapGestureRecognizer instance) {
instance
..onTapDown = onTapDown
..onTapUp = onTapUp
..onTap = onTap
//省略
},
);
}
return RawGestureDetector(
gestures: gestures, // 傳入手勢(shì)識(shí)別器
behavior: behavior, // 同 Listener 中的 HitTestBehavior
child: child,
);
}
注意,上面我們刪除了很多代碼闻伶,只保留了 TapGestureRecognizer(點(diǎn)擊手勢(shì)識(shí)別器) 相關(guān)代碼,我們以點(diǎn)擊手勢(shì)識(shí)別為例講一下整個(gè)過(guò)程忠藤。RawGestureDetector 中會(huì)通過(guò) Listener 組件監(jiān)聽(tīng) PointerDownEvent 事件掉冶,相關(guān)源碼如下:
@override
Widget build(BuildContext context) {
... // 省略無(wú)關(guān)代碼
Widget result = Listener(
onPointerDown: _handlePointerDown,
behavior: widget.behavior ?? _defaultBehavior,
child: widget.child,
);
}
void _handlePointerDown(PointerDownEvent event) {
for (final GestureRecognizer recognizer in _recognizers!.values)
recognizer.addPointer(event);
}
下面我們看一下 TapGestureRecognizer 的幾個(gè)相關(guān)方法,由于 TapGestureRecognizer 有多層繼承關(guān)系糙及,筆者合并了一個(gè)簡(jiǎn)化版:
class CustomTapGestureRecognizer1 extends TapGestureRecognizer {
void addPointer(PointerDownEvent event) {
//會(huì)將 handleEvent 回調(diào)添加到 pointerRouter 中
GestureBinding.instance!.pointerRouter.addRoute(event.pointer, handleEvent);
}
@override
void handleEvent(PointerEvent event) {
//會(huì)進(jìn)行手勢(shì)識(shí)別详幽,并決定是是調(diào)用 acceptGesture 還是 rejectGesture,
}
@override
void acceptGesture(int pointer) {
// 競(jìng)爭(zhēng)勝出會(huì)調(diào)用
}
@override
void rejectGesture(int pointer) {
// 競(jìng)爭(zhēng)失敗會(huì)調(diào)用
}
}
可以看到當(dāng) PointerDownEvent 事件觸發(fā)時(shí)浸锨,會(huì)調(diào)用 TapGestureRecognizer 的 addPointer唇聘,在 addPointer 中會(huì)將 handleEvent 方法添加到 pointerRouter 中保存起來(lái)。這樣一來(lái)當(dāng)手勢(shì)發(fā)生變化時(shí)只需要在 pointerRouter中取出 GestureRecognizer 的 handleEvent 方法進(jìn)行手勢(shì)識(shí)別即可柱搜。
正常情況下應(yīng)該是手勢(shì)直接作用的對(duì)象應(yīng)該來(lái)處理手勢(shì)迟郎,所以一個(gè)簡(jiǎn)單的原則就是同一個(gè)手勢(shì)應(yīng)該只有一個(gè)手勢(shì)識(shí)別器生效,為此聪蘸,手勢(shì)識(shí)別才引入了手勢(shì)競(jìng)技場(chǎng)(Arena)的概念宪肖,簡(jiǎn)單來(lái)講:
- 1. 每一個(gè)手勢(shì)識(shí)別器(GestureRecognizer)都是一個(gè)“競(jìng)爭(zhēng)者”(GestureArenaMember),當(dāng)發(fā)生指針事件時(shí)健爬,他們都要在“競(jìng)技場(chǎng)”去競(jìng)爭(zhēng)本次事件的處理權(quán)控乾,默認(rèn)情況最終只有一個(gè)“競(jìng)爭(zhēng)者”會(huì)勝出(win)。
- 2. GestureRecognizer 的 handleEvent 中會(huì)識(shí)別手勢(shì)娜遵,如果手勢(shì)發(fā)生了某個(gè)手勢(shì)蜕衡,競(jìng)爭(zhēng)者可以宣布自己是否勝出,一旦有一個(gè)競(jìng)爭(zhēng)者勝出设拟,競(jìng)技場(chǎng)管理者(GestureArenaManager)就會(huì)通知其它競(jìng)爭(zhēng)者失敗衷咽。
- 3. 勝出者的 acceptGesture 會(huì)被調(diào)用,其余的 rejectGesture 將會(huì)被調(diào)用蒜绽。
上一節(jié)我們說(shuō)過(guò)命中測(cè)試是從 RenderBinding 的 hitTest 開(kāi)始的:
@override
void hitTest(HitTestResult result, Offset position) {
// 從根節(jié)點(diǎn)開(kāi)始進(jìn)行命中測(cè)試
renderView.hitTest(result, position: position);
// 會(huì)調(diào)用 GestureBinding 中的 hitTest()方法
super.hitTest(result, position);
}
渲染樹(shù)命中測(cè)試完成后會(huì)調(diào)用 GestureBinding 中的 hitTest() 方法:
@override // from HitTestable
void hitTest(HitTestResult result, Offset position) {
result.add(HitTestEntry(this));
}
很簡(jiǎn)單镶骗, GestureBinding 也通過(guò)命中測(cè)試了,這樣的話在事件分發(fā)階段躲雅,GestureBinding 的 handleEvent 也便會(huì)被調(diào)用鼎姊,由于它是最后被添加到 HitTestResult 中的,所以在事件分發(fā)階段 GestureBinding 的 handleEvent會(huì)調(diào)用:
GestureBinding 的 handleEvent 源碼
@override
void handleEvent(PointerEvent event, HitTestEntry entry) {
// 會(huì)調(diào)用在 pointerRouter 中添加的 GestureRecognizer 的 handleEvent
pointerRouter.route(event);
if (event is PointerDownEvent) {
// 分發(fā)完畢后,關(guān)閉競(jìng)技場(chǎng)
gestureArena.close(event.pointer);
} else if (event is PointerUpEvent) {
gestureArena.sweep(event.pointer);
} else if (event is PointerSignalEvent) {
pointerSignalResolver.resolve(event);
}
}
gestureArena 是 GestureArenaManager 類實(shí)例相寇,負(fù)責(zé)管理競(jìng)技場(chǎng)慰于。
上面關(guān)鍵的代碼就是第一行,功能是會(huì)調(diào)用之前在 pointerRouter 中添加的 GestureRecognizer 的 handleEvent唤衫,不同 GestureRecognizer 的 handleEvent 會(huì)識(shí)別不同的手勢(shì)婆赠,然后它會(huì)和 gestureArena 交互(如果當(dāng)前的 GestureRecognizer 勝出,需要 gestureArena 去通知其它競(jìng)爭(zhēng)者它們失敗了)佳励,最終休里,如果當(dāng)前GestureRecognizer 勝出,則最終它的 acceptGesture 會(huì)被調(diào)用赃承,如果失敗則其 rejectGesture 將會(huì)被調(diào)用妙黍,因?yàn)檫@部分代碼不同的 GestureRecognizer 會(huì)不同,知道做了什么就行瞧剖,讀者有興趣可以自行查看源碼拭嫁。
2. 手勢(shì)競(jìng)爭(zhēng)
如果對(duì)一個(gè)組件同時(shí)監(jiān)聽(tīng)水平和垂直方向的拖動(dòng)手勢(shì),當(dāng)我們斜著拖動(dòng)時(shí)哪個(gè)方向的拖動(dòng)手勢(shì)回調(diào)會(huì)被觸發(fā)抓于?
實(shí)際上取決于第一次移動(dòng)時(shí)兩個(gè)軸上的位移分量做粤,哪個(gè)軸的大,哪個(gè)軸在本次滑動(dòng)事件競(jìng)爭(zhēng)中就勝出捉撮。
上面已經(jīng)說(shuō)過(guò)驮宴,每一個(gè)手勢(shì)識(shí)別器(GestureRecognizer)都是一個(gè)“競(jìng)爭(zhēng)者”(GestureArenaMember),當(dāng)發(fā)生指針事件時(shí)呕缭,他們都要在“競(jìng)技場(chǎng)”去競(jìng)爭(zhēng)本次事件的處理權(quán)堵泽,默認(rèn)情況最終只有一個(gè)“競(jìng)爭(zhēng)者”會(huì)勝出(win)。
例如恢总,假設(shè)有一個(gè)ListView迎罗,它的第一個(gè)子組件也是ListView,如果現(xiàn)在滑動(dòng)這個(gè)子ListView片仿,父ListView會(huì)動(dòng)嗎纹安?答案是否定的,這時(shí)只有子ListView會(huì)動(dòng)砂豌,因?yàn)檫@時(shí)子ListView會(huì)勝出而獲得滑動(dòng)事件的處理權(quán)厢岂。
示例1
class MSGestureDetailDemo extends StatelessWidget {
const MSGestureDetailDemo({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("GestureDemo")),
body: Center(
child: GestureDetector(
onTapUp: (details) => print("2"), // 監(jiān)聽(tīng)父組件 tapUp 手勢(shì)
child: Container(
width: 200,
height: 200,
color: Colors.red,
alignment: Alignment.center,
child: GestureDetector(
onTapUp: (details) => print("1"), // 監(jiān)聽(tīng)子組件 tapUp 手勢(shì)
child: Container(
width: 100,
height: 100,
color: Colors.grey,
),
),
),
),
),
);
}
}
當(dāng)我們點(diǎn)擊子組件(灰色區(qū)域)時(shí),控制臺(tái)只會(huì)打印 “1”, 并不會(huì)打印 “2”阳距,這是因?yàn)槭种柑鸷笏#珿estureDetector1 和 GestureDetector 2 會(huì)發(fā)生競(jìng)爭(zhēng),判定獲勝的規(guī)則是“子組件優(yōu)先”筐摘,所以 GestureDetector1 獲勝卒茬,因?yàn)橹荒苡幸粋€(gè)“競(jìng)爭(zhēng)者”勝出船老,所以 GestureDetector 2 將被忽略。
這個(gè)例子中想要解決沖突的方法很簡(jiǎn)單圃酵,將 GestureDetector 換為 Listener 即可柳畔,簡(jiǎn)單的說(shuō),手勢(shì)的識(shí)別和處理是在事件的分發(fā)階段郭赐,而Listener是在監(jiān)聽(tīng)原始指針事件薪韩。具體原因我們?cè)诤竺娼忉尅?/p>
示例2
我們以拖動(dòng)手勢(shì)為例,同時(shí)識(shí)別水平和垂直方向的拖動(dòng)手勢(shì)捌锭,當(dāng)用戶按下手指時(shí)就會(huì)觸發(fā)競(jìng)爭(zhēng)(水平方向和垂直方向)俘陷,一旦某個(gè)方向“獲勝”,則直到當(dāng)次拖動(dòng)手勢(shì)結(jié)束都會(huì)沿著該方向移動(dòng)
class MSGestureDetailDemo2 extends StatefulWidget {
const MSGestureDetailDemo2({Key? key}) : super(key: key);
@override
State<MSGestureDetailDemo2> createState() => _MSGestureDetailDemo2State();
}
class _MSGestureDetailDemo2State extends State<MSGestureDetailDemo2> {
double _left = 0.0;
double _top = 0.0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("MSGestureDetailDemo2")),
body: Stack(
children: [
Positioned(
left: _left,
top: _top,
child: GestureDetector(
child: CircleAvatar(child: Text("A")),
//垂直方向拖動(dòng)事件
onVerticalDragUpdate: (DragUpdateDetails details) {
_top += details.delta.dy;
setState(() {});
},
// 水平方向拖動(dòng)事件
onHorizontalDragUpdate: (DragUpdateDetails details) {
_left += details.delta.dx;
setState(() {});
},
),
),
],
),
);
}
}
此示例運(yùn)行后舀锨,每次拖動(dòng)只會(huì)沿一個(gè)方向移動(dòng)(水平或垂直)岭洲,而競(jìng)爭(zhēng)發(fā)生在手指按下后首次移動(dòng)(move)時(shí)宛逗,此例中具體的“獲勝”條件是:首次移動(dòng)時(shí)的位移在水平和垂直方向上的分量大的一個(gè)獲勝坎匿。
3. 多手勢(shì)沖突
由于手勢(shì)競(jìng)爭(zhēng)最終只有一個(gè)勝出者,所以雷激,當(dāng)我們通過(guò)一個(gè) GestureDetector 監(jiān)聽(tīng)多種手勢(shì)時(shí)替蔬,也可能會(huì)產(chǎn)生沖突。
假設(shè)有一個(gè)widget屎暇,它可以左右拖動(dòng)承桥,現(xiàn)在我們也想檢測(cè)在它上面手指按下和抬起的事件。
示例
class MSGestureDetailDemo3 extends StatefulWidget {
const MSGestureDetailDemo3({Key? key}) : super(key: key);
@override
State<MSGestureDetailDemo3> createState() => _MSGestureDetailDemo3State();
}
class _MSGestureDetailDemo3State extends State<MSGestureDetailDemo3> {
double _left = 0.0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("MSGestureDetailDemo3")),
body: Stack(
children: [
Positioned(
left: _left,
child: GestureDetector(
child: CircleAvatar(child: Text("A")),
onHorizontalDragUpdate: (DragUpdateDetails details) {
_left += details.delta.dx;
setState(() {});
},
onHorizontalDragEnd: (details) {
print("onHorizontalDragEnd");
},
onTapDown: (TapDownDetails details) {
print("down");
},
onTapUp: (TapUpDetails details) {
print("up");
},
),
),
],
),
);
}
}
現(xiàn)在我們按住圓形“A”拖動(dòng)然后抬起手指根悼,控制臺(tái)日志如下:
flutter: down
flutter: onHorizontalDragEnd
我們發(fā)現(xiàn)沒(méi)有打印"up"凶异,這是因?yàn)樵谕蟿?dòng)時(shí),剛開(kāi)始按下手指且沒(méi)有移動(dòng)時(shí)挤巡,拖動(dòng)手勢(shì)還沒(méi)有完整的語(yǔ)義剩彬,此時(shí)TapDown手勢(shì)勝出(win),此時(shí)打印"down"矿卑,而拖動(dòng)時(shí)喉恋,拖動(dòng)手勢(shì)會(huì)勝出,當(dāng)手指抬起時(shí)母廷,onHorizontalDragEnd 和 onTapUp發(fā)生了沖突轻黑,但是因?yàn)槭窃谕蟿?dòng)的語(yǔ)義中,所以onHorizontalDragEnd勝出琴昆,所以就會(huì)打印 “onHorizontalDragEnd”氓鄙。
如果我們的代碼邏輯中,對(duì)于手指按下和抬起是強(qiáng)依賴的业舍,比如在一個(gè)輪播圖組件中玖详,我們希望手指按下時(shí)把介,暫停輪播,而抬起時(shí)恢復(fù)輪播蟋座,但是由于輪播圖組件中本身可能已經(jīng)處理了拖動(dòng)手勢(shì)(支持手動(dòng)滑動(dòng)切換)拗踢,甚至可能也支持了縮放手勢(shì),這時(shí)我們?nèi)绻谕獠吭儆胦nTapDown向臀、onTapUp來(lái)監(jiān)聽(tīng)的話是不行的巢墅。
這時(shí)我們應(yīng)該怎么做?其實(shí)很簡(jiǎn)單券膀,通過(guò)Listener監(jiān)聽(tīng)原始指針事件就行:
class MSGestureDetailDemo4 extends StatefulWidget {
const MSGestureDetailDemo4({Key? key}) : super(key: key);
@override
State<MSGestureDetailDemo4> createState() => _MSGestureDetailDemo4State();
}
class _MSGestureDetailDemo4State extends State<MSGestureDetailDemo4> {
double _left = 0.0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("MSGestureDetailDemo4")),
body: Stack(
children: [
Positioned(
left: _left,
child: Listener(
onPointerDown: (PointerDownEvent event) {
print("down");
},
onPointerUp: (PointerUpEvent event) {
print("up");
},
child: GestureDetector(
child: CircleAvatar(
child: Text("A"),
),
onHorizontalDragUpdate: (DragUpdateDetails details) {
_left += details.delta.dx;
setState(() {});
},
onHorizontalDragEnd: (DragEndDetails details) {
print("onHorizontalDragEnd");
},
),
),
),
],
),
);
}
}
現(xiàn)在我們按住圓形“A”拖動(dòng)然后抬起手指君纫,控制臺(tái)日志如下:
flutter: down
flutter: up
flutter: onHorizontalDragEnd
是有“up”打印的,說(shuō)明我們監(jiān)聽(tīng)到指針抬起事件了芹彬。
4. 解決手勢(shì)沖突
手勢(shì)是對(duì)原始指針的語(yǔ)義化的識(shí)別蓄髓,手勢(shì)沖突只是手勢(shì)級(jí)別的,也就是說(shuō)只會(huì)在組件樹(shù)中的多個(gè) GestureDetector 之間才有沖突的場(chǎng)景舒帮,如果壓根就沒(méi)有使用 GestureDetector 則不存在所謂的沖突会喝,因?yàn)槊恳粋€(gè)節(jié)點(diǎn)都能收到事件,只是在 GestureDetector 中為了識(shí)別語(yǔ)義玩郊,它會(huì)去決定哪些子節(jié)點(diǎn)應(yīng)該忽略事件肢执,哪些節(jié)點(diǎn)應(yīng)該生效。
解決手勢(shì)沖突的方法有兩種:
- 1. 使用 Listener译红。這相當(dāng)于跳出了手勢(shì)識(shí)別那套規(guī)則预茄。
- 2. 自定義手勢(shì)手勢(shì)識(shí)別器( Recognizer)。
4.1 通過(guò) Listener 解決手勢(shì)沖突
通過(guò) Listener 解決手勢(shì)沖突的原因是競(jìng)爭(zhēng)只是針對(duì)手勢(shì)的侦厚,而 Listener 是監(jiān)聽(tīng)原始指針事件耻陕,原始指針事件并非語(yǔ)義話的手勢(shì),所以根本不會(huì)走手勢(shì)競(jìng)爭(zhēng)的邏輯刨沦,所以也就不會(huì)相互影響诗宣。
拿上面兩個(gè) Container 嵌套的例子來(lái)說(shuō),通過(guò)Listener的解決方式為:
class MSGestureDetailDemo5 extends StatelessWidget {
const MSGestureDetailDemo5({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("MSGestureDetailDemo5")),
body: Center(
child: Listener(
onPointerUp: (details) => print("2"), // 監(jiān)聽(tīng)父組件 tapUp 手勢(shì)
child: Container(
width: 200,
height: 200,
color: Colors.red,
alignment: Alignment.center,
child: GestureDetector(
onTapUp: (details) => print("1"), // 監(jiān)聽(tīng)子組件 tapUp 手勢(shì)
child: Container(
width: 100,
height: 100,
color: Colors.grey,
),
),
),
),
),
);
}
}
點(diǎn)擊灰色區(qū)域已卷,會(huì)同時(shí)打印“2”梧田、“1”
代碼很簡(jiǎn)單,只需將 GestureDetector 換位 Listener 即可侧蘸,可以兩個(gè)都換裁眯,也可以只換一個(gè)』浒可以看見(jiàn)穿稳,通過(guò)Listener
直接識(shí)別原始指針事件來(lái)解決沖突的方法很簡(jiǎn)單,因此晌坤,當(dāng)遇到手勢(shì)沖突時(shí)逢艘,我們應(yīng)該優(yōu)先考慮 Listener 旦袋。
4.2 通過(guò)自定義 Recognizer 解決手勢(shì)沖突
自定義手勢(shì)識(shí)別器的方式比較麻煩,原理是當(dāng)確定手勢(shì)競(jìng)爭(zhēng)勝出者時(shí)它改,會(huì)調(diào)用勝出者的acceptGesture 方法疤孕,表示“宣布成功”,然后會(huì)調(diào)用其它手勢(shì)識(shí)別的rejectGesture 方法央拖,表示“宣布失敗”祭阀。既然如此,我們可以自定義手勢(shì)識(shí)別器(Recognizer)鲜戒,然后去重寫(xiě)它的rejectGesture 方法:在里面調(diào)用acceptGesture 方法专控,這就相當(dāng)于它失敗是強(qiáng)制將它也變成競(jìng)爭(zhēng)的成功者了,這樣它的回調(diào)也就會(huì)執(zhí)行遏餐。
我們先自定義tap手勢(shì)識(shí)別器(Recognizer):
class MSCustomTapGestureRecognizer extends TapGestureRecognizer {
@override
void rejectGesture(int pointer) {
//強(qiáng)制宣布成功
super.acceptGesture(pointer);
}
}
//創(chuàng)建一個(gè)新的GestureDetector伦腐,用我們自定義的 CustomTapGestureRecognizer 替換默認(rèn)的
RawGestureDetector customGestureDetector({
GestureTapCallback? onTap,
GestureTapDownCallback? onTapDown,
Widget? child,
}) {
return RawGestureDetector(
child: child,
gestures: {
MSCustomTapGestureRecognizer:
GestureRecognizerFactoryWithHandlers<MSCustomTapGestureRecognizer>(
() => MSCustomTapGestureRecognizer(),
(MSCustomTapGestureRecognizer rec) {
rec
..onTap = onTap
..onTapDown = onTapDown;
},
),
},
);
}
我們通過(guò) RawGestureDetector 來(lái)自定義 customGestureDetector,GestureDetector 中也是通過(guò) RawGestureDetector 來(lái)包裝各種Recognizer 來(lái)實(shí)現(xiàn)的失都,我們需要自定義哪個(gè) Recognizer柏蘑,就添加哪個(gè)即可。
現(xiàn)在我們看看修改調(diào)用代碼:將GestureDetector 替換為customGestureDetector
class MSGestureDetailDemo6 extends StatelessWidget {
const MSGestureDetailDemo6({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("MSGestureDetailDemo5")),
body: Center(
child: customGestureDetector(
onTap: () => print("2"), // 監(jiān)聽(tīng)父組件 tapUp 手勢(shì)
child: Container(
width: 200,
height: 200,
color: Colors.red,
alignment: Alignment.center,
child: customGestureDetector(
onTap: () => print("1"), // 監(jiān)聽(tīng)子組件 tapUp 手勢(shì)
child: Container(
width: 100,
height: 100,
color: Colors.grey,
),
),
),
),
),
);
}
}
這樣就 OK 了嗅剖,需要注意辩越,這個(gè)例子同時(shí)說(shuō)明了一次手勢(shì)處理過(guò)程也是可以有多個(gè)勝出者的嘁扼。
https://book.flutterchina.club/chapter8/gesture_conflict.html