theme: cyanosis
highlight: androidstudio
前言
我在 Flutter 重識 NestedScrollView (juejin.cn) 中留下 增大點擊范圍 的挑戰(zhàn),時間已經(jīng)過了一個星期,不知道大家思考的怎么樣了?今天說了一下對于 增大點擊范圍 我個人的的思路。
調(diào)試源碼
首先我們先順一順,Flutter 中手勢相關(guān)事件這些東西是從何而來的。
事件從何而來
- 首先找到我們經(jīng)常使用的一個組件
Listener,注冊一個事件,打一個斷點。
return Listener(
onPointerDown: (PointerDownEvent value) {
showToast('$text:onTap${i++}',
duration: const Duration(milliseconds: 500));
},
child: mockButtonUI(text),
);
我們可以看到整個 call stack 信息,我們反推回去。
-
Listener暴露一些原始的指針事件的回調(diào), 最終處理的類RenderPointerListener。
const Listener({
Key? key,
this.onPointerDown,
this.onPointerMove,
this.onPointerUp,
this.onPointerHover,
this.onPointerCancel,
this.onPointerSignal,
this.behavior = HitTestBehavior.deferToChild,
Widget? child,
})
...省略部分代碼
@override
RenderPointerListener createRenderObject(BuildContext context) {
return RenderPointerListener(
onPointerDown: onPointerDown,
onPointerMove: onPointerMove,
onPointerUp: onPointerUp,
onPointerHover: onPointerHover,
onPointerCancel: onPointerCancel,
onPointerSignal: onPointerSignal,
behavior: behavior,
);
}
- RenderPointerListener.handleEvent 方法中觸發(fā)了
onPointerDown回調(diào)
class RenderPointerListener extends RenderProxyBoxWithHitTestBehavior {
/// Creates a render object that forwards pointer events to callbacks.
///
/// The [behavior] argument defaults to [HitTestBehavior.deferToChild].
RenderPointerListener({
this.onPointerDown,
this.onPointerMove,
this.onPointerUp,
this.onPointerHover,
this.onPointerCancel,
this.onPointerSignal,
HitTestBehavior behavior = HitTestBehavior.deferToChild,
RenderBox? child,
}) : super(behavior: behavior, child: child);
// 省略一些代碼
...
@override
Size computeSizeForNoChild(BoxConstraints constraints) {
return constraints.biggest;
}
@override
void handleEvent(PointerEvent event, HitTestEntry entry) {
assert(debugHandleEvent(event, entry));
if (event is PointerDownEvent)
return onPointerDown?.call(event);
if (event is PointerMoveEvent)
return onPointerMove?.call(event);
if (event is PointerUpEvent)
return onPointerUp?.call(event);
if (event is PointerHoverEvent)
return onPointerHover?.call(event);
if (event is PointerCancelEvent)
return onPointerCancel?.call(event);
if (event is PointerSignalEvent)
return onPointerSignal?.call(event);
}
}
-
GestureBinding.dispatchEvent方法中的對hitTestResult分發(fā)事件
void dispatchEvent(PointerEvent event, HitTestResult? hitTestResult) {
...省略一部分代碼
for (final HitTestEntry entry in hitTestResult.path) {
try {
entry.target.handleEvent(event.transformed(entry.transform), entry);
}
-
RendererBinding.dispatchEvent中調(diào)用super.dispatchEvent(event, hitTestResult)
@override // from GestureBinding
void dispatchEvent(PointerEvent event, HitTestResult? hitTestResult) {
if (hitTestResult != null ||
event is PointerAddedEvent ||
event is PointerRemovedEvent) {
assert(event.position != null);
_mouseTracker!.updateWithEvent(event, () => hitTestResult ?? renderView.hitTestMouseTrackers(event.position));
}
super.dispatchEvent(event, hitTestResult);
}
- 再次回到中
GestureBinding通過一些內(nèi)部方法,最終_handlePointerDataPacket來注冊的原生回調(diào)的地方。
graph TD
GestureBinding._handlePointerEventImmediately --> GestureBinding.handlePointerEvent --> GestureBinding._flushPointerEventQueue --> GestureBinding._handlePointerDataPacket
mixin GestureBinding on BindingBase implements HitTestable, HitTestDispatcher, HitTestTarget {
@override
void initInstances() {
super.initInstances();
_instance = this;
window.onPointerDataPacket = _handlePointerDataPacket;
}
- window 里面的回調(diào)實際上是
PlatformDispatcher.onPointerDataPacket,并且在_dispatchPointerDataPacket調(diào)用,把引擎?zhèn)鬟f過來的數(shù)據(jù)轉(zhuǎn)換成PointerDataPacket
// Called from the engine, via hooks.dart
void _dispatchPointerDataPacket(ByteData packet) {
if (onPointerDataPacket != null) {
_invoke1<PointerDataPacket>(
onPointerDataPacket,
_onPointerDataPacketZone,
_unpackPointerDataPacket(packet),
);
}
}
-
PointerDataPacket是 通過_unpackPointerDataPacket方法把引擎?zhèn)鬟f的數(shù)據(jù)轉(zhuǎn)換成為下面的數(shù)據(jù)結(jié)構(gòu)。
/// 從原生傳遞過來的原始指針的一些信息
/// A sequence of reports about the state of pointers.
class PointerDataPacket {
/// Creates a packet of pointer data reports.
const PointerDataPacket({ this.data = const <PointerData>[] }) : assert(data != null);
/// Data about the individual pointers in this packet.
///
/// This list might contain multiple pieces of data about the same pointer.
final List<PointerData> data;
}
/// 原始指針包含的一些信息
/// Information about the state of a pointer.
class PointerData {
/// Creates an object that represents the state of a pointer.
const PointerData({
this.embedderId = 0,
this.timeStamp = Duration.zero,
this.change = PointerChange.cancel,
this.kind = PointerDeviceKind.touch,
this.signalKind,
this.device = 0,
this.pointerIdentifier = 0,
this.physicalX = 0.0,
this.physicalY = 0.0,
this.physicalDeltaX = 0.0,
this.physicalDeltaY = 0.0,
this.buttons = 0,
this.obscured = false,
this.synthesized = false,
this.pressure = 0.0,
this.pressureMin = 0.0,
this.pressureMax = 0.0,
this.distance = 0.0,
this.distanceMax = 0.0,
this.size = 0.0,
this.radiusMajor = 0.0,
this.radiusMinor = 0.0,
this.radiusMin = 0.0,
this.radiusMax = 0.0,
this.orientation = 0.0,
this.tilt = 0.0,
this.platformData = 0,
this.scrollDeltaX = 0.0,
this.scrollDeltaY = 0.0,
});
- 最后來到 hooks.dart
https://github.com/flutter/flutter/blob/stable/bin/cache/pkg/sky_engine/lib/ui/hooks.dart
@pragma('vm:entry-point')
// ignore: unused_element
void _dispatchPointerDataPacket(ByteData packet) {
PlatformDispatcher.instance._dispatchPointerDataPacket(packet);
}
- 這就是我們整個獲取事件的一個流程
HitTest
從上面的流程,我們能知道點擊事件是從哪里來的,那么 Flutter 又是怎么知道我是點擊的哪個位置呢?還記得我在前面有留提示,GestureBinding.dispatchEvent 方法中的對 hitTestResult 分發(fā)事件,那我們看看 hitTestResult 又是從何而來的呢?
找到 https://github.com/flutter/flutter/blob/stable/packages/flutter/lib/src/gestures/binding.dart
中 GestureBinding.dispatchEvent 方法??梢钥吹?hitTestResult 是作為參數(shù)傳遞進(jìn)來的,那我們再向上找。
@override // from HitTestDispatcher
void dispatchEvent(PointerEvent event, HitTestResult? hitTestResult) {
assert(!locked);
// No hit test information implies that this is a [PointerHoverEvent],
// [PointerAddedEvent], or [PointerRemovedEvent]. These events are specially
// routed here; other events will be routed through the `handleEvent` below.
if (hitTestResult == null) {
assert(event is PointerAddedEvent || event is PointerRemovedEvent);
try {
pointerRouter.route(event);
} catch (exception, stack) {
FlutterError.reportError(FlutterErrorDetailsForPointerEventDispatcher(
exception: exception,
stack: stack,
library: 'gesture library',
context: ErrorDescription('while dispatching a non-hit-tested pointer event'),
event: event,
hitTestEntry: null,
informationCollector: () sync* {
yield DiagnosticsProperty<PointerEvent>('Event', event, style: DiagnosticsTreeStyle.errorProperty);
},
));
}
return;
}
for (final HitTestEntry entry in hitTestResult.path) {
try {
entry.target.handleEvent(event.transformed(entry.transform), entry);
} catch (exception, stack) {
FlutterError.reportError(FlutterErrorDetailsForPointerEventDispatcher(
exception: exception,
stack: stack,
library: 'gesture library',
context: ErrorDescription('while dispatching a pointer event'),
event: event,
hitTestEntry: entry,
informationCollector: () sync* {
yield DiagnosticsProperty<PointerEvent>('Event', event, style: DiagnosticsTreeStyle.errorProperty);
yield DiagnosticsProperty<HitTestTarget>('Target', entry.target, style: DiagnosticsTreeStyle.errorProperty);
},
));
}
}
}
-
GestureBinding._handlePointerEventImmediately, 如果是
if (event is PointerDownEvent || event is PointerSignalEvent || event is PointerHoverEvent)成立,就創(chuàng)建一個HitTestResult,并且調(diào)用
hitTest(hitTestResult, event.position) 方法。
void _handlePointerEventImmediately(PointerEvent event) {
HitTestResult? hitTestResult;
if (event is PointerDownEvent || event is PointerSignalEvent || event is PointerHoverEvent) {
assert(!_hitTests.containsKey(event.pointer));
hitTestResult = HitTestResult();
// 由于是根,所以直接把自己加進(jìn) HitTestResult 當(dāng)中
hitTest(hitTestResult, event.position);
// 保存
if (event is PointerDownEvent) {
_hitTests[event.pointer] = hitTestResult;
}
assert(() {
if (debugPrintHitTestResults)
debugPrint('$event: $hitTestResult');
return true;
}());
}
// up 或者 cancel 的時候移除掉
else if (event is PointerUpEvent || event is PointerCancelEvent) {
hitTestResult = _hitTests.remove(event.pointer);
} else if (event.down) {
// Because events that occur with the pointer down (like
// [PointerMoveEvent]s) should be dispatched to the same place that their
// initial PointerDownEvent was, we want to re-use the path we found when
// the pointer went down, rather than do hit detection each time we get
// such an event.
hitTestResult = _hitTests[event.pointer];
}
assert(() {
if (debugPrintMouseHoverEvents && event is PointerHoverEvent)
debugPrint('$event');
return true;
}());
if (hitTestResult != null ||
// 第一次觸發(fā)的為 PointerAddedEvent 進(jìn)入 dispatchEvent
event is PointerAddedEvent ||
event is PointerRemovedEvent) {
assert(event.position != null);
// 分發(fā)
dispatchEvent(event, hitTestResult);
`}`
}
- 第一次觸發(fā)的為
PointerAddedEvent進(jìn)入dispatchEvent, 由于hitTestResult為null,直接調(diào)用renderView.hitTestMouseTrackers(event.position).
@override // from GestureBinding
void dispatchEvent(PointerEvent event, HitTestResult? hitTestResult) {
if (hitTestResult != null ||
event is PointerAddedEvent ||
event is PointerRemovedEvent) {
assert(event.position != null);
_mouseTracker!.updateWithEvent(event, () => hitTestResult ?? renderView.hitTestMouseTrackers(event.position));
}
super.dispatchEvent(event, hitTestResult);
}
之后將從父節(jié)點一個一個向下去調(diào)用 hitTest 和 hitTestChildren 方法
-
RenderBox.hitTest,我們遇到了處理增大點擊范圍的一個判斷_size!.contains(position),點擊區(qū)域必須在自己的大小范圍內(nèi)才會繼續(xù)取判斷hitTestChildren和hitTestSelf。 那是不是如果我們把這個判斷去掉,這樣child或者children就能接受hitTest測試了呢?
bool hitTest(BoxHitTestResult result, { required Offset position }) {
... 省略部分代碼
if (_size!.contains(position)) {
if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
result.add(BoxHitTestEntry(this, position));
return true;
}
}
return false;
}
- RenderBoxContainerDefaultsMixin.hitTestChildren,這是默認(rèn)的多孩子的
hitTest,注意一下,是從lastChild開始判斷的。不知道你們有沒有zIndex的概念,F(xiàn)lutter 里面Stack,Row,Column等組件的children當(dāng)中后添加的child會先接受hitTest測試,給人的感覺就是lastChild是在最上層。
@override
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
return defaultHitTestChildren(result, position: position);
}
bool defaultHitTestChildren(BoxHitTestResult result, { required 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!);
},
);
if (isHit)
return true;
child = childParentData.previousSibling;
}
return false;
}
小結(jié)
- 引擎通知 Flutter
GestureBinding -
GestureBinding通過hitTest方法確定哪些RenderOject通過了hitTest測試, 并且加入BoxHitTestResult。
關(guān)鍵點:
- size 限制
- children 接受
hitTest的順序
- 對
BoxHitTestResult中的結(jié)果進(jìn)行事件分發(fā) - 通過
GestureDetector,RawGestureDetector等組件對Listener獲取的事件監(jiān)聽進(jìn)行轉(zhuǎn)換,轉(zhuǎn)換成我們更容易接受的各種事件。
解決
A,B 兩個按鈕都跟附近的組件緊挨著。就是說如果要增大點擊區(qū)域,必然需要考慮它們個附近的組件。
偽代碼,大致的結(jié)構(gòu)是這樣的。我們怎么樣才能讓 ButtonA 和 ButtonB 的點擊區(qū)域擴(kuò)大呢?
Row(children: <Widget>[
Text(''),
Column(children: <Widget>[
Row(children: <Widget>[
ButtonA(),
Text(''),
ButtonB(),
],),
Text(''),
],),
Text(''),
],)
如果擴(kuò)大點擊范圍為下圖的話,你的第一反應(yīng)是什么?
- 我的第一反應(yīng)是利用
stack繪制出一個看不見的區(qū)域來接收hitTest。但是其實很早就有聽到過說stack中溢出的部分是不會接收到hitTest的,想想也是,溢出的部分已經(jīng)超出size了。
return Stack(
clipBehavior: Clip.none,
children: <Widget>[
mockButtonUI(text),
Positioned(
left: -16,
right: -16,
top: -16,
bottom: -16,
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
showToast('$text:onTap${i++}',
duration: const Duration(milliseconds: 500));
},
// 使用看不見的顏色來占位來接收 hitTest
child: const ColoredBox(
color: Color(0x00100000),
),
),
),
],
);
RenderBoxHitTestWithoutSizeLimit
我們先來創(chuàng)建一個 mixin 用來解除 hitTest 關(guān)于 size 的限制。
mixin RenderBoxHitTestWithoutSizeLimit on RenderBox {
@override
bool hitTest(BoxHitTestResult result, {required Offset position}) {
assert(() {
if (!hasSize) {
if (debugNeedsLayout) {
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary(
'Cannot hit test a render box that has never been laid out.'),
describeForError(
'The hitTest() method was called on this RenderBox'),
ErrorDescription(
"Unfortunately, this object's geometry is not known at this time, "
'probably because it has never been laid out. '
'This means it cannot be accurately hit-tested.'),
ErrorHint('If you are trying '
'to perform a hit test during the layout phase itself, make sure '
"you only hit test nodes that have completed layout (e.g. the node's "
'children, after their layout() method has been called).'),
]);
}
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('Cannot hit test a render box with no size.'),
describeForError('The hitTest() method was called on this RenderBox'),
ErrorDescription(
'Although this node is not marked as needing layout, '
'its size is not set.'),
ErrorHint('A RenderBox object must have an '
'explicit size before it can be hit-tested. Make sure '
'that the RenderBox in question sets its size during layout.'),
]);
}
return true;
}());
if (contains(position)) {
if (hitTestChildren(result, position: position) ||
hitTestSelf(position)) {
result.add(BoxHitTestEntry(this, position));
return true;
}
}
return false;
}
// 永遠(yuǎn)為 true
bool contains(Offset position) => true;
// size.contains(position);
}
StackHitTestWithoutSizeLimit
復(fù)制 Stack 的源碼,為 RenderStack 混入 RenderBoxHitTestWithoutSizeLimit。
class StackHitTestWithoutSizeLimit extends Stack {
/// Creates a stack layout widget.
///
/// By default, the non-positioned children of the stack are aligned by their
/// top left corners.
StackHitTestWithoutSizeLimit({
Key? key,
AlignmentDirectional alignment = AlignmentDirectional.topStart,
TextDirection? textDirection,
StackFit fit = StackFit.loose,
Clip clipBehavior = Clip.hardEdge,
List<Widget> children = const <Widget>[],
}) : super(
key: key,
children: children,
alignment: alignment,
textDirection: textDirection,
fit: fit,
clipBehavior: clipBehavior,
);
bool _debugCheckHasDirectionality(BuildContext context) {
if (alignment is AlignmentDirectional && textDirection == null) {
assert(
debugCheckHasDirectionality(context,
why: 'to resolve the \'alignment\' argument',
hint: alignment == AlignmentDirectional.topStart
? 'The default value for \'alignment\' is AlignmentDirectional.topStart, which requires a text direction.'
: null,
alternative:
'Instead of providing a Directionality widget, another solution would be passing a non-directional \'alignment\', or an explicit \'textDirection\', to the $runtimeType.'),
);
}
return true;
}
@override
RenderStack createRenderObject(BuildContext context) {
assert(_debugCheckHasDirectionality(context));
return RenderStackHitTestWithoutSizeLimit(
alignment: alignment,
textDirection: textDirection ?? Directionality.of(context),
fit: fit,
clipBehavior: clipBehavior,
);
}
}
class RenderStackHitTestWithoutSizeLimit extends RenderStack
with RenderBoxHitTestWithoutSizeLimit {
RenderStackHitTestWithoutSizeLimit({
List<RenderBox>? children,
AlignmentGeometry alignment = AlignmentDirectional.topStart,
TextDirection? textDirection,
StackFit fit = StackFit.loose,
Clip clipBehavior = Clip.hardEdge,
}) : super(
alignment: alignment,
children: children,
textDirection: textDirection,
fit: fit,
clipBehavior: clipBehavior,
);
}
RowHitTestWithoutSizeLimit,ColumnHitTestWithoutSizeLimit
Row(children: <Widget>[
Text(''),
Column(children: <Widget>[
Row(children: <Widget>[
ButtonA(),
Text(''),
ButtonB(),
],),
Text(''),
],),
Text(''),
],)
由于 Stack 溢出的部分已經(jīng)達(dá)到 Row 和 Column 的中其他 child 的區(qū)域了,所以我們對 Row 和 Column 也需要進(jìn)行特殊的處理。
class RowHitTestWithoutSizeLimit extends Row
with FlexHitTestWithoutSizeLimitmixin {
RowHitTestWithoutSizeLimit({
Key? key,
MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start,
MainAxisSize mainAxisSize = MainAxisSize.max,
CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center,
TextDirection? textDirection,
VerticalDirection verticalDirection = VerticalDirection.down,
TextBaseline?
textBaseline, // NO DEFAULT: we don't know what the text's baseline should be
List<Widget> children = const <Widget>[],
}) : super(
children: children,
key: key,
mainAxisAlignment: mainAxisAlignment,
mainAxisSize: mainAxisSize,
crossAxisAlignment: crossAxisAlignment,
textDirection: textDirection,
verticalDirection: verticalDirection,
textBaseline: textBaseline,
);
}
mixin FlexHitTestWithoutSizeLimitmixin on Flex {
@override
RenderFlex createRenderObject(BuildContext context) {
return RenderFlexHitTestWithoutSizeLimit(
direction: direction,
mainAxisAlignment: mainAxisAlignment,
mainAxisSize: mainAxisSize,
crossAxisAlignment: crossAxisAlignment,
textDirection: getEffectiveTextDirection(context),
verticalDirection: verticalDirection,
textBaseline: textBaseline,
clipBehavior: clipBehavior,
);
}
}
class RenderFlexHitTestWithoutSizeLimit extends RenderFlex
with
RenderBoxHitTestWithoutSizeLimit,
RenderBoxChildrenHitTestWithoutSizeLimit {
RenderFlexHitTestWithoutSizeLimit({
List<RenderBox>? children,
Axis direction = Axis.horizontal,
MainAxisSize mainAxisSize = MainAxisSize.max,
MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start,
CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center,
TextDirection? textDirection,
VerticalDirection verticalDirection = VerticalDirection.down,
TextBaseline? textBaseline,
Clip clipBehavior = Clip.none,
}) : super(
children: children,
direction: direction,
mainAxisSize: mainAxisSize,
mainAxisAlignment: mainAxisAlignment,
crossAxisAlignment: crossAxisAlignment,
textDirection: textDirection,
verticalDirection: verticalDirection,
textBaseline: textBaseline,
clipBehavior: clipBehavior,
);
@override
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
return hitTestChildrenWithoutSizeLimit(
result,
position: position,
children: getChildrenAsList().reversed,
);
}
}
由于 children 默認(rèn)是反序接受 hitTest ,我們需要讓 RenderBoxHitTestWithoutSizeLimit 優(yōu)先接受 hitTest。
mixin RenderBoxChildrenHitTestWithoutSizeLimit {
bool hitTestChildrenWithoutSizeLimit(
BoxHitTestResult result, {
required Offset position,
required Iterable<RenderBox> children,
}) {
final List<RenderBox> normal = <RenderBox>[];
for (final RenderBox child in children) {
if ((child is RenderBoxHitTestWithoutSizeLimit) &&
childIsHit(result, child, position: position)) {
return true;
} else {
normal.insert(0, child);
}
}
for (final RenderBox child in normal) {
if (childIsHit(result, child, position: position)) {
return true;
}
}
return false;
}
bool childIsHit(BoxHitTestResult result, RenderBox child,
{required Offset position}) {
final ContainerParentDataMixin<RenderBox> childParentData =
child.parentData as ContainerParentDataMixin<RenderBox>;
final Offset offset = (childParentData as BoxParentData).offset;
final bool isHit = result.addWithPaintOffset(
offset: offset,
position: position,
hitTest: (BoxHitTestResult result, Offset transformed) {
assert(transformed == position - offset);
return child.hitTest(result, position: transformed);
},
);
return isHit;
}
}
我們將寫好的新組件替換掉之前的,就可以達(dá)到增大點擊范圍的效果了。
RowHitTestWithoutSizeLimit(children: <Widget>[
Text(''),
ColumnHitTestWithoutSizeLimit(children: <Widget>[
RowHitTestWithoutSizeLimit(children: <Widget>[
ButtonA(),
Text(''),
ButtonB(),
],),
Text(''),
],),
Text(''),
],)
Widget ButtonA()
{
return StackHitTestWithoutSizeLimit(
clipBehavior: Clip.none,
children: <Widget>[
mockButtonUI(text),
Positioned(
left: -16,
right: -16,
top: -16,
bottom: -16,
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
showToast('$text:onTap${i++}',
duration: const Duration(milliseconds: 500));
},
// 使用看不見的顏色來占位來接收 hitTest
child: const ColoredBox(
color: Color(0x00100000),
),
),
),
],
);
}
extra_hittest_area | Flutter Package (flutter-io.cn)
為了方便大家使用,我將常用的組件封裝了一下供大家使用。
Parent widgets
跟官方的 widgets 一樣,使用它們來保證,當(dāng)額外 hitTest 區(qū)域超出了父 widget的大小的時候,一樣能接收到 hitTest。
StackHitTestWithoutSizeLimit-
RowHitTestWithoutSizeLimit,ColumnHitTestWithoutSizeLimit,FlexHitTestWithoutSizeLimit SizedBoxHitTestWithoutSizeLimit
監(jiān)聽點擊事件的 widgets
GestureDetectorHitTestWithoutSizeLimitRawGestureDetectorHitTestWithoutSizeLimitListenerHitTestWithoutSizeLimit
| parameter | description | default |
|---|---|---|
| extraHitTestArea | 額外增加的 hitTest 區(qū)域 | EdgeInsets.zero |
| debugHitTestAreaColor | 用于 debug 的 hitTest 區(qū)域背景色 | null |
你可以設(shè)置 ExtraHitTestBase.debugGlobalHitTestAreaColor 來替代在每個監(jiān)聽 widget 中單獨設(shè)置 debugHitTestAreaColor
實現(xiàn)其他的 HitTestWithoutSizeLimit
如果這個 package 沒有你需要的 widgets , 你可以使用下面的類自己實現(xiàn)。
RenderBoxHitTestWithoutSizeLimit, RenderBoxChildrenHitTestWithoutSizeLimit
結(jié)語
這次我們嘗試解決了實際開發(fā)中遇到的一個問題,重要的是理解了 Flutter 中手勢事件的由來。至于從引擎?zhèn)鬟f過來的 raw 的 event 怎么轉(zhuǎn)換成 Tap,onLongPress,Scale 等我們熟悉的事件,可以再開一篇了。
FlutterChallenges qq 群 321954965 喜歡折騰自己的童鞋歡迎加群,歡迎大家提供新的挑戰(zhàn)或者解決挑戰(zhàn)
。
愛 Flutter,愛糖果,歡迎加入[Flutter Candies]
最最后放上 Flutter Candies 全家桶,真香。