Listener
背景:
- 可能會(huì)遇到的問題:如下代碼,點(diǎn)擊文字以外的區(qū)域是無響應(yīng)的
GestureDetector(
child: Container(
height: 50,
// color: Colors.green,
padding: EdgeInsets.only(left: 5, right: 5),
alignment: Alignment.center,
child: Text(
"click me",
style: TextStyle(fontSize: 20),
)
),
onTap: () {
print("click");
},
)
原因分析:
GestureDetector -> RawGestureDetector -> Listener
Listener是一個(gè)監(jiān)聽指針事件的控件腕柜,比如按下辐真、移動(dòng)、釋放殃姓、取消等指針事件命满。
通常情況下蔬崩,監(jiān)聽手勢(shì)事件使用GestureDetector,GestureDetector是更高級(jí)的手勢(shì)事件便脊。
Listener的事件介紹如下:
onPointerDown:按下時(shí)回調(diào)
onPointerMove:移動(dòng)時(shí)回調(diào)
onPointerUp:抬起時(shí)回調(diào)
- 用法如下:
Listener(
onPointerDown: (PointerDownEvent pointerDownEvent) {
print('$pointerDownEvent');
},
onPointerMove: (PointerMoveEvent pointerMoveEvent) {
print('$pointerMoveEvent');
},
onPointerUp: (PointerUpEvent upEvent) {
print('$upEvent');
},
child: Container(
height: 200,
width: 200,
color: Colors.blue,
alignment: Alignment.center,
),
)
- 當(dāng)手指按下時(shí)蚂四,F(xiàn)lutter會(huì)對(duì)應(yīng)用程序執(zhí)行命中測(cè)試(Hit Test),以確定指針與屏幕接觸的位置存在哪些組件(widget)哪痰, 指針按下事件(以及該指針的后續(xù)事件)然后被分發(fā)到由命中測(cè)試發(fā)現(xiàn)的最內(nèi)部的組件遂赠,然后從那里開始,事件會(huì)在組件樹中向上冒泡晌杰,這些事件會(huì)從最內(nèi)部的組件被分發(fā)到組件樹根的路徑上的所有組件跷睦,這和Web開發(fā)中瀏覽器的事件冒泡機(jī)制相似, 但是Flutter中沒有機(jī)制取消或停止“冒泡”過程肋演,而瀏覽器的冒泡是可以停止的抑诸。注意烂琴,只有通過命中測(cè)試的組件才能觸發(fā)事件。
什么是命中測(cè)試哼鬓?
當(dāng)手指按下监右、移動(dòng)或者抬起時(shí),F(xiàn)lutter會(huì)給每一個(gè)事件新建一個(gè)對(duì)象异希,如按下是PointerDownEvent健盒,移動(dòng)是PointerMoveEvent,抬起是PointerUpEvent称簿。對(duì)于每一個(gè)事件對(duì)象扣癣,F(xiàn)lutter都會(huì)執(zhí)行命中測(cè)試,它經(jīng)歷了以下這幾步:
1憨降、從最底層的Widget開始執(zhí)行命中測(cè)試父虑,是否命中取決于hitTestChildren方法(它的children Widget是否命中測(cè)試)或hitTestSelf方法是否返回true, 如果返回true授药,表示命中測(cè)試通過士嚎,會(huì)把自己以HitTestEntry添加到HitTestResult對(duì)象中。
2悔叽、循環(huán)最底層Widget的children Widget莱衩,分別執(zhí)行child Widget的命中測(cè)試。child Widget是否命中也取決于hitTestChidren方法(它的children Widget是否命中測(cè)試)或hitTestSelf方法是否返回true娇澎。
3笨蚁、從下往上遞歸地執(zhí)行命中測(cè)試,直到找到最上層的一個(gè)命中測(cè)試的Widget趟庄,將它加入命中測(cè)試列表括细。由于它已命中測(cè)試,那么它的父Widget也命中了測(cè)試戚啥,將父Widget也加入命中測(cè)試列表奋单。以此類推,直到將所有命中測(cè)試的Widget加入命中測(cè)試列表猫十。
原則:優(yōu)先判斷children,再判斷自己览濒,只要有一個(gè)為true,就把自己加入到result中
bool hitTest(BoxHitTestResult result, { @required Offset position }) {
// 事件的position必須在當(dāng)前組件內(nèi)
if (_size.contains(position)) {
// 優(yōu)先判斷children,再判斷自己,只要有一個(gè)為true,就把自己加入到result中
if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
result.add(BoxHitTestEntry(this, position));
return true;
}
}
return false;
}
測(cè)試列表鏈:
- 在Flutter中炫彩,每一個(gè)Widget實(shí)際上會(huì)對(duì)應(yīng)一個(gè)RenderObject。對(duì)于上面代碼來說絮短,上圖為Widget和RenderObject的對(duì)應(yīng)關(guān)系江兢。
例:
Listener(
child: ConstrainedBox(
constraints: BoxConstraints.tight(Size(200, 200)),
child: Center(
child: Text('click me'),
)
),
onPointerDown: (event) => print("onPointerDown")
)
1、當(dāng)點(diǎn)擊了Text時(shí)丁频,它的命中測(cè)試列表是這樣的:
RenderParagraph->RenderPositionedBox->RenderConstrainedBox->RenderPointerListener杉允,
所以RenderPointerListener的handleEvent方法會(huì)被執(zhí)行邑贴,最終在控制臺(tái)會(huì)打印onPointerDown。
2叔磷、當(dāng)點(diǎn)擊了Text以外的區(qū)域時(shí)拢驾,它的命中測(cè)試列表就沒有RenderPointerListener了。為什么呢改基?繁疤??
Text以外的區(qū)域是ConstrainedBox的(為什么不是Center秕狰,因?yàn)镃enter的功能是幫助Text定位稠腊,它的區(qū)域和Text是一致的)。那ConstrainedBox對(duì)應(yīng)的RenderConstrainedBox命中測(cè)試了么鸣哀?很顯然是沒有的架忌。
因?yàn)镃onstrainedBox只有一個(gè)child,就是Center我衬。Center對(duì)應(yīng)的RenderPositionedBox沒有命中測(cè)試叹放,導(dǎo)致RenderConstrainedBox的hitTestChildren返回false,而它的hitTestSelf也返回false挠羔,所以RenderConstrainedBox沒有命中測(cè)試井仰。
而Listener也只有一個(gè)child,那就是ConstrainedBox褥赊,既然RenderConstrainedBox沒有命中測(cè)試糕档,那么RenderPointerListener相應(yīng)的就沒有命中測(cè)試,所以命中測(cè)試列表中是沒有RenderPointerListener的拌喉。
所以控制臺(tái)并不會(huì)打印onPointerDown速那。
上面的例子使用的behavior屬性是默認(rèn)的HitTestBehavior.deferToChild,如果修改一下behavior屬性會(huì)有什么奇妙的效果呢尿背?
一端仰、behavior:
behavior表示命中測(cè)試(Hit Test)過程中的表現(xiàn)策略。它是一個(gè)枚舉田藐,提供了三個(gè)值荔烧,
分別是HitTestBehavior.deferToChild、HitTestBehavior.opaque汽久、HitTestBehavior.translucent
/// How to behave during hit tests.
enum HitTestBehavior {
///事件是否處理取決于自己的子類
deferToChild,
/// Opaque targets can be hit by hit tests, causing them to both receive
/// events within their bounds and prevent targets visually behind them from
/// also receiving events.
/// 自己可以命中hitTest鹤竭,又在視覺上阻止位于其后方的目標(biāo)也接收事件。
opaque,
/// Translucent targets both receive events within their bounds and permit
/// targets visually behind them to also receive events.
///半透明目標(biāo)既可以接收其范圍內(nèi)的事件景醇,也可以在視覺上允許目標(biāo)后面的目標(biāo)也接收事件臀稚。
translucent,
}
- 源碼引用順序 Listener->_PointerListener->RenderPointerListener->RenderProxyBoxWithHitTestBehavior
源碼分析:
RenderProxyBoxWithHitTestBehavior源碼,代碼很少三痰,但邏輯就是在這里了
/// A RenderProxyBox subclass that allows you to customize the
/// hit-testing behavior.
abstract class RenderProxyBoxWithHitTestBehavior extends RenderProxyBox {
@override
bool hitTest(BoxHitTestResult result, { Offset position }) {
bool hitTarget = false;
// position在自己范圍內(nèi)
if (size.contains(position)) {
// 判斷子類和自己是否命中吧寺,這是普通邏輯窜管,沒什么特別的
hitTarget =
hitTestChildren(result, position: position) || hitTestSelf(position);
// 如果是HitTestBehavior.translucent,強(qiáng)行將自己命中hittest稚机,參與事件消費(fèi)的隊(duì)列中幕帆,這里hitTestChildren和hitTestSelf的結(jié)果就不重要了
if (hitTarget || behavior == HitTestBehavior.translucent)
result.add(BoxHitTestEntry(this, position));
}
return hitTarget;
}
//如果Behavior是opaque,且沒有被子類重寫赖条,那就是返回true失乾,也即是參與到事件消費(fèi)的隊(duì)列中
@override
bool hitTestSelf(Offset position) => behavior == HitTestBehavior.opaque;
}
所以,當(dāng)我們用GestureDetector監(jiān)聽事件時(shí)谋币,最后都會(huì)走到RenderProxyBoxWithHitTestBehavior里仗扬,只要behavio是opaque或translucent都會(huì)將自己加入到事件消費(fèi)的隊(duì)列, 而behavior默認(rèn)是HitTestBehavior.deferToChild蕾额,當(dāng)點(diǎn)擊空白處時(shí)早芭,
hitTestChildren(result, position: position)返回false
hitTestSelf(hitTestSelf) 也返回false
二、背景色:
接下來看第二個(gè)問題诅蝶,為什么Container設(shè)置任意背景色也可以響應(yīng)點(diǎn)擊事件退个?
Container其實(shí)是個(gè)StateLessWidget,它本身并沒有RenderObject對(duì)應(yīng)调炬,可以理解為是個(gè)配置項(xiàng)语盈,真實(shí)渲染的render是其他配置引進(jìn)的,比如color對(duì)應(yīng)的ColoredBox
ColoredBox->_RenderColoredBox
// 一眼就看到了缰泡,強(qiáng)制設(shè)置為opaque了刀荒,答案和第一個(gè)問題一樣了
class _RenderColoredBox extends RenderProxyBoxWithHitTestBehavior {
_RenderColoredBox({@required Color color})
: _color = color,
//看這里!!!!!!!!!!!!!!!!!!!!!
super(behavior: HitTestBehavior.opaque);
}
所以,Container設(shè)置任意背景色棘钞,可以響應(yīng)點(diǎn)擊事件缠借,因?yàn)樵O(shè)置了color后,返回的widget里包含了ColoredBox宜猜,ColoredBox對(duì)應(yīng)的RenderObject是_RenderColoredBox泼返,_RenderColoredBox繼承自RenderProxyBoxWithHitTestBehavior并強(qiáng)制指定了behavior是HitTestBehavior.opaque。
下面例子進(jìn)一步印證:
return Stack(
children: <Widget>[
Listener(
child: ConstrainedBox(
constraints: BoxConstraints.tight(Size(300.0, 300.0)),
child: DecoratedBox(decoration: BoxDecoration(color: Colors.red)),
),
onPointerDown: (event) => print("first child"),
),
Listener(
child: ConstrainedBox(
constraints: BoxConstraints.tight(Size(200.0, 200.0)),
child: Center(child: Text("左上角200*200范圍內(nèi)-空白區(qū)域點(diǎn)擊")),
),
onPointerDown: (event) => print("second child"),
//放開此行注釋后姨拥,單詞點(diǎn)擊 first ,second都會(huì)響應(yīng)绅喉,HitTestBehavior.opaque是不行的
// behavior: HitTestBehavior.translucent,
)
],
);
當(dāng)點(diǎn)擊左上角,非文本區(qū)域時(shí)叫乌,只會(huì)響應(yīng)first child柴罐,當(dāng)把這句代碼注釋打開
// behavior: HitTestBehavior.translucent,
會(huì)先輸出second child,再輸出first child憨奸。原因還是在RenderProxyBoxWithHitTestBehavior的hitTest方法
@override
bool hitTest(BoxHitTestResult result, { Offset position }) {
bool hitTarget = false;
if (size.contains(position)) {
hitTarget = hitTestChildren(result, position: position) || hitTestSelf(position);
//如果是translucent革屠,盡快自身加入了命中測(cè)試隊(duì)列,但返回的結(jié)果還是false,
//但如果是opaque屠阻,子類不重寫hitTestSelf,那hitTarget肯定就是true了
if (hitTarget || behavior == HitTestBehavior.translucent)
result.add(BoxHitTestEntry(this, position));
}
return hitTarget;
}
對(duì)于Stack來說额各,對(duì)應(yīng)的Render是RenderStack
RenderStack hitTestChildren
@override
bool hitTestChildren(BoxHitTestResult result, { Offset position }) {
return defaultHitTestChildren(result, position: position);
}
最終走到了RenderBoxContainerDefaultsMixin的defaultHitTestChildren
bool defaultHitTestChildren(BoxHitTestResult result, { Offset position }) {
// The x, y parameters have the top left of the node's box as the origin.
// 從最上層的子類開始遍歷
ChildType child = lastChild;
while (child != null) {
final ParentDataType childParentData = child.parentData as ParentDataType;
final bool isHit = result.addWithPaintOffset(
offset: childParentData.offset,
position: position,
hitTest: (BoxHitTestResult result, Offset transformed) {
assert(transformed == position - childParentData.offset);
return child.hitTest(result, position: transformed);
},
);
// 第一個(gè)命中国觉,就直接返回了,后續(xù)的子類不再執(zhí)行命中測(cè)試虾啦,所以translucent能透?jìng)髀榫鳎驗(yàn)楸凰揎椀腖istener,返回的結(jié)果是false
if (isHit)
return true;
child = childParentData.previousSibling;
}
return false;
}
結(jié)論:
所以想要解決開頭拋出的問題,方法如下:
1傲醉、GestureDetector的behavior設(shè)置為opaque或者translucent才行蝇闭,
2、Container設(shè)置任意背景色
總結(jié):
opaque和translucent的區(qū)別:
RenderStack的hitTestChildren返回了true硬毕,它就不會(huì)再去檢測(cè)第二個(gè)child呻引。
opaque: 第一個(gè)Listener是否命中測(cè)試” ,即意味著如果第一個(gè)child的hitTest返回true(例如opaque)的話Stack就不會(huì)再把指針事件傳給第二個(gè)child吐咳,即不能透?jìng)?/strong>逻悠,
translucent: 如果第一個(gè)child的hitTest返回false(例如translucent)則點(diǎn)擊事件會(huì)被傳遞到第二個(gè)child,即能透?jìng)?/strong>
Stack
參考:
https://juejin.cn/post/6844904079106277383
https://juejin.cn/post/6908365134365491208