preface

It has been a week since I left a challenge to increase the click range in the Flutter reshuffle NestedScrollView (juejin. Cn). What do you think? Today I talked about my personal ideas about increasing the range of clicks.

Debugging the source code

Let’s start by smoothing out where the gesture-related events in Flutter come from.

Where the incident came from

  • Start by finding a component that we use a lotListenerRegister an event and hit a breakpoint.
    return Listener(
      onPointerDown: (PointerDownEvent value) {
        showToast('$text:onTap${i++}',
            duration: const Duration(milliseconds: 500));
      },
      child: mockButtonUI(text),
    );
Copy the code

So we can see the entire Call Stack information, and we can push back.

  • ListenerExposes some of the original pointer events to the callback, which is ultimately handled by the classRenderPointerListener
  const Listener({
    Key? key,
    this.onPointerDown,
    this.onPointerMove,
    this.onPointerUp,
    this.onPointerHover,
    this.onPointerCancel,
    this.onPointerSignal,
    this.behavior = HitTestBehavior.deferToChild, Widget? child, }) ... Omit some code@override
  RenderPointerListener createRenderObject(BuildContext context) {
    return RenderPointerListener(
      onPointerDown: onPointerDown,
      onPointerMove: onPointerMove,
      onPointerUp: onPointerUp,
      onPointerHover: onPointerHover,
      onPointerCancel: onPointerCancel,
      onPointerSignal: onPointerSignal,
      behavior: behavior,
    );
  }
  
Copy the code
  • RenderPointerListener. Triggered the handleEvent methodonPointerDownThe callback
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);
  // Omit some code.@override
  Size computeSizeForNoChild(BoxConstraints constraints) {
    return constraints.biggest;
  }

  @override
  void handleEvent(PointerEvent event, HitTestEntry entry) {
    assert(debugHandleEvent(event, entry));
    if (event is PointerDownEvent)
      returnonPointerDown? .call(event);if (event is PointerMoveEvent)
      returnonPointerMove? .call(event);if (event is PointerUpEvent)
      returnonPointerUp? .call(event);if (event is PointerHoverEvent)
      returnonPointerHover? .call(event);if (event is PointerCancelEvent)
      returnonPointerCancel? .call(event);if (event is PointerSignalEvent)
      return onPointerSignal?.call(event);
  }
}
Copy the code
  • GestureBinding.dispatchEventPairs of methodshitTestResultDistribute events
  voiddispatchEvent(PointerEvent event, HitTestResult? hitTestResult) { ... Leave out some codefor (final HitTestEntry entry in hitTestResult.path) {
      try {
        entry.target.handleEvent(event.transformed(entry.transform), entry);
      } 
Copy the code
  • RendererBinding.dispatchEventIn the callsuper.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);
  }

Copy the code
  • Back to center againGestureBindingThrough some internal methods, eventually_handlePointerDataPacketWhere to register native callbacks.
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;
  }

Copy the code
  • The callback inside the window is actuallyPlatformDispatcher.onPointerDataPacketAnd, in_dispatchPointerDataPacketCall to convert the data passed by the engine toPointerDataPacket

  // Called from the engine, via hooks.dart
  void _dispatchPointerDataPacket(ByteData packet) {
    if(onPointerDataPacket ! =null) { _invoke1<PointerDataPacket>( onPointerDataPacket, _onPointerDataPacketZone, _unpackPointerDataPacket(packet), ); }}Copy the code
  • PointerDataPacketIs through the_unpackPointerDataPacketMethod converts the data passed by the engine into the following data structure.
/// Some information about the original pointer passed from the native
/// 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;
}

/// Some information contained in the original pointer
/// 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});Copy the code
  • Finally, hooks. Dart

Github.com/flutter/flu…

@pragma('vm:entry-point')
// ignore: unused_element
void _dispatchPointerDataPacket(ByteData packet) {
  PlatformDispatcher.instance._dispatchPointerDataPacket(packet);
}
Copy the code
  • This is the whole process of getting events

HitTest

From the above flow, we can know where the click event came from. How does Flutter know where I clicked? Remember I have leave a tip in front, GestureBinding. DispatchEvent method of hitTestResult distribute events, we see hitTestResult is come from?

Find github.com/flutter/flu… In GestureBinding dispatchEvent method. You can see that hitTestResult is passed in as a parameter, so let’s look up.

  @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); })); }}}Copy the code
  • GestureBinding._handlePointerEventImmediatelyIf it is

If (the event is PointerDownEvent | | event is PointerSignalEvent | | event is PointerHoverEvent) was set up, will create a HitTestResult, and call

HitTest (hitTestResult, event.position) method.

  void _handlePointerEventImmediately(PointerEvent event) {
    HitTestResult? hitTestResult;
    if (event is PointerDownEvent || event is PointerSignalEvent || event is PointerHoverEvent) {
      assert(! _hitTests.containsKey(event.pointer)); hitTestResult = HitTestResult();// Add yourself directly to HitTestResult because it is the root
      hitTest(hitTestResult, event.position);
      / / save
      if (event is PointerDownEvent) {
        _hitTests[event.pointer] = hitTestResult;
      }
      assert(() {
        if (debugPrintHitTestResults)
          debugPrint('$event: $hitTestResult');
        return true; } ()); }// Remove when up or 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 ||
        // dispatchEvent is sent for PointerAddedEvent the first time
        event is PointerAddedEvent ||
        event is PointerRemovedEvent) {
      assert(event.position ! =null);
      / / distribute
      dispatchEvent(event, hitTestResult);
    `}`
  }
Copy the code
  • The first trigger isPointerAddedEventEnter thedispatchEventBecause ofhitTestResultnull, directly callrenderView.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);
  }
Copy the code

The hitTest and hitTestChildren methods are then called one by one from the parent node

  • RenderBox.hitTest, we encountered a judgment that dealt with increasing the range of clicks_size! .contains(position), the click area must be within its own size range to continue the judgmenthitTestChildrenhitTestSelf. Student: Is that if we take this judgment out of the equation, like thischildorchildrenYou can accepthitTestTested?
  bool hitTest(BoxHitTestResult result, { requiredOffset position }) { ... Omit some codeif(_size! .contains(position)) {if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
        result.add(BoxHitTestEntry(this, position));
        return true; }}return false;
  }

Copy the code
  • RenderBoxContainerDefaultsMixin hitTestChildren, this is the default of many childrenhitTestNotice, fromlastChildStart judging. I don’t know if you have anyzIndexThe concept of FlutterStack.Row.ColumnComponents such aschildrenAdded after the middlechildWill I receivehitTestTesting, that’s what it feels likelastChildIt’s on the top.
  @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;
  }  
Copy the code

summary

  1. Engine notification FlutterGestureBinding
  2. GestureBindingthroughhitTestMethod to determine whichRenderOjectThrough thehitTestTest, and joinBoxHitTestResult.

Key points:

  • The size limit
  • The children accepthitTestThe order of
  1. rightBoxHitTestResultFor event distribution
  2. throughGestureDetector.RawGestureDetectorSuch as components ofListenerThe retrieved events listen for conversion to various events that we can more easily accept.

To solve

Buttons A and B are next to components nearby. That is, if you want to increase the click area, you must consider the components nearby.

Pseudo code, the general structure is like this. How can we make ButtonA and ButtonB’s click area bigger?

    Row(children: <Widget>[
      Text(' '),
      Column(children: <Widget>[
        Row(children: <Widget>[
          ButtonA(),
          Text(' '),
          ButtonB(),
        ],),
        Text(' '),
      ],),
      Text(' '),,)Copy the code

What would be your first reaction if you expanded the range to look like the picture below?

  1. My first reaction was to exploitstackDraw an invisible area to receivehitTest. But it has been heard for a long timestackThe overflow part of thehitTestIf you think about it, the overflow part is already oversize.
    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));
            },
            // Use invisible colors as placeholders to receive hittests
            child: const ColoredBox(
              color: Color(0x00100000(() [() [() [()Copy the code

RenderBoxHitTestWithoutSizeLimit

Let’s start by creating a mixin to remove the size constraint from hitTest.

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;
  }
  // Always true
  bool contains(Offset position) => true;
  // size.contains(position);
}
Copy the code

StackHitTestWithoutSizeLimit

Copy the Stack source code, to infiltrate RenderBoxHitTestWithoutSizeLimit RenderStack.

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));
    returnRenderStackHitTestWithoutSizeLimit( 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,
        );
}
Copy the code

RowHitTestWithoutSizeLimit, ColumnHitTestWithoutSizeLimit

    Row(children: <Widget>[
      Text(' '),
      Column(children: <Widget>[
        Row(children: <Widget>[
          ButtonA(),
          Text(' '),
          ButtonB(),
        ],),
        Text(' '),
      ],),
      Text(' '),,)Copy the code

Row and Column also need special processing because the Stack overflows into the other child areas of Row and Column.

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) {
    returnRenderFlexHitTestWithoutSizeLimit( 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}) {
    returnhitTestChildrenWithoutSizeLimit( result, position: position, children: getChildrenAsList().reversed, ); }}Copy the code

Because of the children to accept hitTest order by default, we need to make priority RenderBoxHitTestWithoutSizeLimit hitTest.

mixin RenderBoxChildrenHitTestWithoutSizeLimit {
  bool hitTestChildrenWithoutSizeLimit(
    BoxHitTestResult result, {
    required Offset position,
    required 可迭代<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);
        returnchild.hitTest(result, position: transformed); });returnisHit; }}Copy the code

We can increase the click range by replacing the original component with the new one.

    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));
            },
            // Use invisible colors as placeholders to receive hittests
            child: const ColoredBox(
              color: Color(0x00100000(() [() [() [() }Copy the code

extra_hittest_area | Flutter Package (flutter-io.cn)

In order to facilitate your use, I will commonly used components encapsulated for you to use.

Parent widgets

Like the official widgets, use them to ensure that hittests are received when the additional hitTest area exceeds the size of the parent widget.

  • StackHitTestWithoutSizeLimit
  • RowHitTestWithoutSizeLimit.ColumnHitTestWithoutSizeLimit.FlexHitTestWithoutSizeLimit
  • SizedBoxHitTestWithoutSizeLimit

Listen for widgets for click events

  • GestureDetectorHitTestWithoutSizeLimit
  • RawGestureDetectorHitTestWithoutSizeLimit
  • ListenerHitTestWithoutSizeLimit
parameter description default
extraHitTestArea Additional hitTest area added EdgeInsets.zero
debugHitTestAreaColor Background color of the hitTest area for debug null

. You can set the ExtraHitTestBase debugGlobalHitTestAreaColor instead of in each monitoring widget set debugHitTestAreaColor separately

Implement other HitTestWithoutSizeLimit

If the package doesn’t have the widgets you need, you can implement them yourself using the following classes.

RenderBoxHitTestWithoutSizeLimit, RenderBoxChildrenHitTestWithoutSizeLimit

conclusion

This time we tried to solve a real development problem. It was important to understand the origin of gesture events in Flutter. How to convert raw events from the engine to Tap, onLongPress, Scale, etc.

FlutterChallenges QQ group 321954965 welcome to add new challenges or solve them.

loveFlutterLove,candyWelcome to join usFlutter CandiesTogether to make cute little Flutter candiesQQ group: 181398081

And finally, put Flutter Candies on it. It smells sweet.