GestureDetector
Flutter里有個(gè)神奇的組件——GestureDetector帝簇,學(xué)習(xí)過flutter的朋友應(yīng)該知道澈蚌,flutter的組件機(jī)制還是跟Android原生有很多不同的地方,flutter講究一切皆組件,這方面很解耦,套起來也方便淑翼,比如Text組件,在android里只要是控件就可以設(shè)置setOnclickListener品追,或者重寫View的onTouch事件去處理點(diǎn)擊玄括,但flutter不是這樣,它大部分組件并沒有點(diǎn)擊的屬性肉瓦,需要套一個(gè)GestureDetector組件來實(shí)現(xiàn)遭京,關(guān)于源碼中對(duì)GestureDetector是這么描述的:
/// * [GestureDetector], a less flexible but much simpler widget that does the same thing.
意思就是一個(gè)不太靈活但更簡(jiǎn)單更方便使用的一個(gè)組件,也就是封裝過的泞莉,看源碼也確實(shí)是RawGestureDetector的封裝哪雕,暫且不談,下面我們來看看這個(gè)組件有哪些屬性:
GestureDetector({
Key key, //唯一標(biāo)識(shí)
this.child,//起作用的子組件
this.onTapDown,//觸摸屏幕按下(子組件區(qū)域)
this.onTapUp,//觸摸抬起(子組件區(qū)域)
this.onTap,//發(fā)生了點(diǎn)擊事件戒财,觸發(fā)順序是onTapDown->onTapUp->onTap热监,中斷則不觸發(fā),事件傳遞樹的最末端
this.onTapCancel,//按下在onTapUp事件發(fā)生前饮寞,手指離開組件區(qū)域觸發(fā)
this.onDoubleTap,//雙擊孝扛,當(dāng)設(shè)有onTap單擊事件時(shí),不響應(yīng)
this.onLongPress,//長(zhǎng)按幽崩,當(dāng)響應(yīng)長(zhǎng)按事件苦始,onTap回調(diào)cancel事件
this.onLongPressStart,//長(zhǎng)按開始
this.onLongPressEnd,//長(zhǎng)按結(jié)束
this.onLongPressUp,//長(zhǎng)按手抬起
this.onVerticalDragDown,//設(shè)置有長(zhǎng)按事件或者無拖動(dòng)抬起,則會(huì)回調(diào)cancel事件慌申,
this.onVerticalDragStart,//手指觸摸屏幕并開始垂直拖動(dòng)陌选,同時(shí)onTap回調(diào)cancel事件,onVerticalDrag和onHorizontalDrag只可響應(yīng)一個(gè)蹄溉,誰先誰占有事件
this.onVerticalDragUpdate,//手指觸摸屏幕并垂直拖動(dòng)中
this.onVerticalDragEnd,//垂直拖動(dòng)結(jié)束
this.onVerticalDragCancel,//垂直拖動(dòng)事件取消咨油,參考o(jì)nVerticalDragDown說明
this.onHorizontalDragDown,//設(shè)置有長(zhǎng)按事件或者無拖動(dòng)抬起,則會(huì)回調(diào)cancel事件柒爵,
this.onHorizontalDragStart,//手指觸摸屏幕并開始水平拖動(dòng)役电,同時(shí)onTap回調(diào)cancel事件,onVerticalDrag和onHorizontalDrag只可響應(yīng)一個(gè)棉胀,誰先誰占有事件
this.onHorizontalDragUpdate,//水平拖動(dòng)中
this.onHorizontalDragEnd,//水平拖動(dòng)結(jié)束
this.onHorizontalDragCancel,//垂直拖動(dòng)事件取消法瑟,參考o(jì)nHorizontalDragDown說明
// onPan可以取代onVerticalDrag或者onHorizontalDrag,三者不能并存
this.onPanDown,//手指接觸屏幕
this.onPanStart,//手指在屏幕上開始移動(dòng)
this.onPanUpdate,//手指在屏幕上一直移動(dòng)
this.onPanEnd,//手指離開屏幕
this.onPanCancel,//當(dāng)設(shè)有LongPress事件時(shí)唁奢,會(huì)直接走此事件回調(diào)
//手指屏幕按壓霎挟,需要壓力傳感器,模擬器無法觸發(fā)
this.onForcePressStart, //手指按壓屏幕麻掸,當(dāng)力量達(dá)到ForcePressGestureRecognizer.startPressure觸發(fā)
this.onForcePressPeak,//手指按壓屏幕酥夭,當(dāng)力量達(dá)到ForcePressGestureRecognizer.peakPressure觸發(fā)
this.onForcePressUpdate,//手指按壓屏幕,按壓力量變化時(shí)觸發(fā)
this.onForcePressEnd,//手指離開屏幕
//兩指觸摸屏幕
this.onSecondaryTapDown,//兩指觸摸屏幕
this.onSecondaryTapUp,//兩指離開屏幕
this.onSecondaryTapCancel, //當(dāng)設(shè)有LongPress事件時(shí),會(huì)直接走此事件回調(diào)
// onScale可以取代onVerticalDrag或者onHorizontalDrag熬北,三者不能并存千所,不能與onPan并存,會(huì)報(bào)錯(cuò)
this.onScaleStart,//縮放拖動(dòng)
this.onScaleUpdate,//縮放比例變化
this.onScaleEnd,//縮放手指抬起
this.behavior, //點(diǎn)擊測(cè)試行為規(guī)則,有三個(gè)值分別是:HitTestBehavior.deferToChild,HitTestBehavior.opaque,HitTestBehavior.translucent,文章后面會(huì)介紹
this.excludeFromSemantics = false蒜埋,//是否排除手勢(shì)
})
GestureDetector是檢測(cè)手勢(shì)的widget,同樣能響應(yīng)touch事件的還有Ink,InkWell和InkResponse組件,其中Ink沒有點(diǎn)擊的回調(diào)事件淫痰。
Flutter控件分類(交互的維度)
Flutter里控件按交互維度區(qū)分,分為自帶交互的控件和不帶交互的控件
- 自帶交互的控件
自帶交互的控件整份,如RaisedButton,OutlineButton等待错,內(nèi)部會(huì)有點(diǎn)擊事件的回調(diào),如
Center(
child: OutlineButton(onPressed: () {
print("點(diǎn)擊了button");
}, child: Text("點(diǎn)擊")),
上面代碼就可以搞定OutlineButton的點(diǎn)擊了
- 不帶交互的控件
不帶交互的控件烈评,如最基本的Text火俄,這類widget本身并沒有手勢(shì)點(diǎn)擊之類的監(jiān)聽屬性
Text(
this.data, {
Key key,
this.style,
this.strutStyle,
this.textAlign,
this.textDirection,
this.locale,
this.softWrap,
this.overflow,
this.textScaleFactor,
this.maxLines,
this.semanticsLabel,
this.textWidthBasis,
this.textHeightBehavior,
})
如果你想實(shí)現(xiàn)單擊、雙擊讲冠、滑動(dòng)監(jiān)聽之類的交互瓜客,可以通過在外層套用GestureDetector來實(shí)現(xiàn)
GestureDetector(
child: Text(
"我是一個(gè)小小小小鳥",
style: TextStyle(color: _color, fontSize: 18),
),
onTap:(){
print("點(diǎn)擊一下");
}
...
)
InkWell和InkResponse組件也能實(shí)現(xiàn),區(qū)別在于GestureDetector提供了最全的手勢(shì)監(jiān)聽竿开,如單擊谱仪、雙擊、長(zhǎng)按否彩、垂直橫向滑動(dòng)疯攒、縮放這些手勢(shì),還有命中測(cè)試行為列荔,而InkWell和InkResponse提供部分手勢(shì)和高亮觸摸效果敬尺,自帶水波紋動(dòng)畫
InkWell({
Key key,
Widget child,
GestureTapCallback onTap,
GestureTapCallback onDoubleTap,
GestureLongPressCallback onLongPress,
GestureTapDownCallback onTapDown,
GestureTapCancelCallback onTapCancel,
ValueChanged<bool> onHighlightChanged,
ValueChanged<bool> onHover,
Color focusColor,
Color hoverColor,
Color highlightColor,
Color splashColor,
InteractiveInkFeatureFactory splashFactory,
double radius,
BorderRadius borderRadius,
ShapeBorder customBorder,
bool enableFeedback = true,
bool excludeFromSemantics = false,
FocusNode focusNode,
bool canRequestFocus = true,
ValueChanged<bool> onFocusChange,
bool autofocus = false,
}
InkResponse({
Key key,
this.child,
this.onTap,
this.onTapDown,
this.onTapCancel,
this.onDoubleTap,
this.onLongPress,
this.onHighlightChanged,
this.onHover,
this.containedInkWell = false,
this.highlightShape = BoxShape.circle,
this.radius,
this.borderRadius,
this.customBorder,
this.focusColor,
this.hoverColor,
this.highlightColor,
this.splashColor,
this.splashFactory,
this.enableFeedback = true,
this.excludeFromSemantics = false,
this.focusNode,
this.canRequestFocus = true,
this.onFocusChange,
this.autofocus = false,
})
手勢(shì)的事件傳遞起源
無論是原生android還是flutter,UI事件傳遞的起始點(diǎn)都跟window脫不了關(guān)系
// android 中 Activity.dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {//getWindow()就是window實(shí)例
return true;
}
return onTouchEvent(ev);
}
//flutter GestureBinding 中的initInstances方法
@override
void initInstances() {
super.initInstances();
_instance = this;
//事件由ui.window.onPointerDataPacket產(chǎn)生贴浙,把事件傳給GestureBinding._handlePointerDataPacket方法
window.onPointerDataPacket = _handlePointerDataPacket;
}
從上面代碼中很容易就可以看出砂吞,android和flutter的起點(diǎn)都來自于window,當(dāng)然崎溃,雖然起點(diǎn)一致蜻直,但傳遞思路卻并不相同。
首先笨奠,我們來看一下android是怎樣設(shè)計(jì)的袭蝗,話不多說唤殴,直接上圖
從圖可以看出般婆,android傳遞的線路上Activity、PhoneWindow朵逝、DecorView蔚袍、ContentView,自上而下,逐層傳遞啤咽,很像我們?nèi)粘9ぷ鞯臅r(shí)候的場(chǎng)景:一天晋辆,大領(lǐng)導(dǎo)(Activity)分派一個(gè)任務(wù)下來,技術(shù)總監(jiān)(PhoneWindow)把任務(wù)整理一下又給了技術(shù)經(jīng)理(DecorView)宇整,技術(shù)經(jīng)理很忙瓶佳,又把任務(wù)派給了各個(gè)技術(shù)組長(zhǎng)(ContentView),然后組長(zhǎng)派任務(wù)給底下的組員來做鳞青,組員能完成最好霸饲,如果不能,又會(huì)逐級(jí)向上傳遞臂拓,直到事件結(jié)束厚脉,當(dāng)然具體做事情決不會(huì)這么簡(jiǎn)單,中間的細(xì)節(jié)就不說了胶惰,可以去找一些專門的技術(shù)文章去了解傻工,我們重點(diǎn)講講flutter是怎么設(shè)計(jì)的
Flutter 事件傳遞源碼流程
先從GestureBinding的initInstances方法開始吧
@override
void initInstances() {
super.initInstances();
_instance = this;
window.onPointerDataPacket = _handlePointerDataPacket;
}
ui.Window get window => ui.window;
ui.window.onPointerDataPacket相當(dāng)于系統(tǒng)的一個(gè)接口,專門去接收硬件傳遞過來的事件方法孵滞,具體事件處理交給GestureBinding類的_handlePointerDataPacket方法中捆,接下來看一下方法
final Queue<PointerEvent> _pendingPointerEvents = Queue<PointerEvent>();
...
void _handlePointerDataPacket(ui.PointerDataPacket packet) {
_pendingPointerEvents.addAll(PointerEventConverter.expand(packet.data, window.devicePixelRatio));
if (!locked)
_flushPointerEventQueue();
}
void _flushPointerEventQueue() {
assert(!locked);
while (_pendingPointerEvents.isNotEmpty)
_handlePointerEvent(_pendingPointerEvents.removeFirst());
}
從上面代碼可以看到,_pendingPointerEvents是個(gè)隊(duì)列坊饶,隊(duì)列里裝著一系列的指針事件轨香,通過指針事件轉(zhuǎn)換器把從底層傳出的物理坐標(biāo)邏輯像素根據(jù)設(shè)備像素比例參數(shù)轉(zhuǎn)換成指針事件屏幕坐標(biāo),隔離設(shè)備相關(guān)性幼东,然后通過while循環(huán)取出隊(duì)列的第一個(gè)指針事件元素進(jìn)行處理臂容,繼續(xù)看_handlePointerEvent這個(gè)方法
void _handlePointerEvent(PointerEvent event) {
assert(!locked);
HitTestResult hitTestResult;
if (event is PointerDownEvent || event is PointerSignalEvent) {
assert(!_hitTests.containsKey(event.pointer));
hitTestResult = HitTestResult();
//確定命中測(cè)試的結(jié)果,通過hitTest方法來計(jì)算出HitTestResult
hitTest(hitTestResult, event.position);
if (event is PointerDownEvent) {
//event.pointer是事件的唯一標(biāo)識(shí)符根蟹,相當(dāng)于事件的id,以id為key將hitTestResult存到_hitTests中
_hitTests[event.pointer] = hitTestResult;
}
assert(() {
if (debugPrintHitTestResults)
debugPrint('$event: $hitTestResult');
return true;
}());
} else if (event is PointerUpEvent || event is PointerCancelEvent) {
//事件結(jié)束標(biāo)記脓杉,將hitTestResult從_hitTests取出并移除
hitTestResult = _hitTests.remove(event.pointer);
} else if (event.down) {
//復(fù)用PointerDownEvent事件中的hitTestResult
hitTestResult = _hitTests[event.pointer];
}
assert(() {
if (debugPrintMouseHoverEvents && event is PointerHoverEvent)
debugPrint('$event');
return true;
}());
if (hitTestResult != null ||
event is PointerHoverEvent ||
event is PointerAddedEvent ||
event is PointerRemovedEvent) {
dispatchEvent(event, hitTestResult);
}
}
_handlePointerEvent方法會(huì)對(duì)每個(gè)取出的PointerEvent進(jìn)行處理,然后交給dispatchEvent進(jìn)行分發(fā)
@override // from HitTestDispatcher
void dispatchEvent(PointerEvent event, HitTestResult hitTestResult) {
assert(!locked);
// No hit test information implies that this is a hover or pointer
// add/remove event.
if (hitTestResult == null) {
assert(event is PointerHoverEvent || event is PointerAddedEvent || event is PointerRemovedEvent);
try {
pointerRouter.route(event);
} catch (exception, stack) {
...
}
return;
}
for (final HitTestEntry entry in hitTestResult.path) {
try {
entry.target.handleEvent(event.transformed(entry.transform), entry);
} catch (exception, stack) {
...
}
}
}
@override // from HitTestTarget
void handleEvent(PointerEvent event, HitTestEntry entry) {
pointerRouter.route(event);
if (event is PointerDownEvent) {
gestureArena.close(event.pointer);
} else if (event is PointerUpEvent) {
gestureArena.sweep(event.pointer);
} else if (event is PointerSignalEvent) {
pointerSignalResolver.resolve(event);
}
}
在事件分發(fā)時(shí)简逮,如果hitTestResult為空球散,就從路由表里遍歷尋找相應(yīng)的事件,如果包含這個(gè)事件就會(huì)繼續(xù)進(jìn)行分發(fā)散庶,如果hitTestResult不為空蕉堰,就遍歷hitTestResult的path上的所有HitTestEntry,取出事件交給事件路由表繼續(xù)遍歷尋找悲龟,找到之后繼續(xù)分發(fā)事件屋讶。
RenderBinding
以上的部分都基于GestureBinding進(jìn)行分析的,其實(shí)還有個(gè)RenderBinding類
class WidgetsFlutterBinding extends BindingBase with GestureBinding, ServicesBinding, SchedulerBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding {
static WidgetsBinding ensureInitialized() {
if (WidgetsBinding.instance == null)
WidgetsFlutterBinding();
return WidgetsBinding.instance;
}
}
這里進(jìn)行了一系列的綁定操作须教,有熟悉的gestureBinding皿渗,還有RenderBinding(渲染綁定類)
//渲染樹與flutter引擎之間的膠水類
mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureBinding, SemanticsBinding, HitTestable{
...
}
這里mixin與on關(guān)鍵字結(jié)合使用斩芭,使用on關(guān)鍵字,則表示該mixin只能在那個(gè)類的子類使用了乐疆,mixin中可以調(diào)用那個(gè)類定義的方法划乖、屬性,在on后面發(fā)現(xiàn)了GestureBinding這個(gè)熟悉的類挤土,那么RenderBinding也會(huì)重寫GestureBinding類的一些方法和屬性琴庵,比如hitTest
@override
void hitTest(HitTestResult result, Offset position) {
assert(renderView != null);
renderView.hitTest(result, position: position);
super.hitTest(result, position);
}
繼續(xù)追蹤renderView中的hitTest方法
bool hitTest(HitTestResult result, { Offset position }) {
if (child != null)
child.hitTest(BoxHitTestResult.wrap(result), position: position);
result.add(HitTestEntry(this));
return true;
}
child是RenderBox對(duì)象,追蹤進(jìn)RenderBox的hitTest方法
bool hitTest(BoxHitTestResult result, { @required Offset position }) {
...
//如果區(qū)域內(nèi)包含這個(gè)坐標(biāo)仰美,同時(shí)對(duì)child或者自身命中測(cè)試通過就添加到hitTestResult中
if (_size.contains(position)) {
if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
result.add(BoxHitTestEntry(this, position));
return true;
}
}
return false;
}
現(xiàn)在回到hitTest方法中细卧,可以發(fā)現(xiàn),方法是先對(duì)child進(jìn)行命中測(cè)試筒占,把符合條件的測(cè)試結(jié)果通過遞歸添加完畢贪庙,再把自身添加到hitTestResult中,事件通過冒泡的形式由下向上傳遞翰苫,而且不能中途中斷冒泡止邮,這個(gè)時(shí)候HitTestResult的路徑順序就是:
目標(biāo)節(jié)點(diǎn)-->父節(jié)點(diǎn)-->根節(jié)點(diǎn)-->GestureBinding
接著PointerDown,PointerMove奏窑,PointerUp导披,PointerCancel等事件分發(fā),事件都是從目標(biāo)Widget往RenderView(根節(jié)點(diǎn))都根據(jù)這個(gè)順序來遍歷調(diào)用它們的handleEvent方法埃唯,就像瀏覽器事件的冒泡過程一樣撩匕。到此,框架的事件流分析完了墨叛。該回到GestureDetector的懷抱了止毕。
GestureDetector源碼分析
GestureDetector里封裝了大量指針手勢(shì)事件,比如單擊漠趁、雙擊扁凛、橫向縱向滑動(dòng)、縮放等闯传,具體屬性在文章的開頭已經(jīng)有了一個(gè)詳細(xì)的介紹谨朝,下面我們來看這個(gè)組件內(nèi)部是怎么處理這些事件的
class GestureDetector extends StatelessWidget {
...
}
一開頭,都一個(gè)套路甥绿,沒什么看頭字币,我們直接看build方法
@override
Widget build(BuildContext context) {
final Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{};
...
return RawGestureDetector(
gestures: gestures,
behavior: behavior,
excludeFromSemantics: excludeFromSemantics,
child: child,
);
}
中間我省去了很大一部分代碼,篇幅有點(diǎn)長(zhǎng)共缕,其實(shí)就是根據(jù)注冊(cè)回調(diào)洗出,添加對(duì)應(yīng)的GestureRecognizer,并傳遞到RawGestureDetector的gesture屬性骄呼,可以理解為GestureDetector其實(shí)就是對(duì)RawGestureDetector的一次封裝共苛,讓我們用起來更方便更直觀,繼續(xù)看RawGestureDetector是個(gè)什么東東
class RawGestureDetector extends StatefulWidget {
...
class RawGestureDetectorState extends State<RawGestureDetector> {
...
//默認(rèn)的命中測(cè)試行為蜓萄,沒有子元素隅茎,則組件背后也能接收到事件,如果有嫉沽,就讓子元素先響應(yīng)事件
HitTestBehavior get _defaultBehavior {
return widget.child == null ? HitTestBehavior.translucent : HitTestBehavior.deferToChild;
}
@override
Widget build(BuildContext context) {
Widget result = Listener(
onPointerDown: _handlePointerDown,
behavior: widget.behavior ?? _defaultBehavior,
child: widget.child,
);
if (!widget.excludeFromSemantics)
result = _GestureSemantics(
child: result,
assignSemantics: _updateSemanticsForRenderObject,
);
return result;
}
}
}
直接看state的build方法辟犀,發(fā)現(xiàn)了一個(gè)Listener,看看源碼里是怎么說的
/// * [Listener], a widget that reports raw pointer events.
翻譯過來就是一個(gè)響應(yīng)原始指針事件的組件绸硕,這個(gè)就不往下研究了堂竟,繼續(xù)看_handlePointerDown方法
void _handlePointerDown(PointerDownEvent event) {
assert(_recognizers != null);
for (final GestureRecognizer recognizer in _recognizers.values)
recognizer.addPointer(event);
}
void addPointer(PointerDownEvent event) {
_pointerToKind[event.pointer] = event.kind;
if (isPointerAllowed(event)) {
addAllowedPointer(event);
} else {
handleNonAllowedPointer(event);
}
}
如果設(shè)備被認(rèn)可,那么輸入事件就會(huì)被手勢(shì)識(shí)別器接受玻佩,給每個(gè)手勢(shì)識(shí)別器都添加PointDown事件出嘹,這個(gè)addPointer方法是手勢(shì)識(shí)別器的父類里的,接下來我們?nèi)テ渲械囊粋€(gè)擴(kuò)展子類識(shí)別器看看具體怎么操作的咬崔,比如HorizontalDragGestureRecognizer