|
41 | 41 | */ |
42 | 42 | @implementation RNGestureHandlerButton { |
43 | 43 | BOOL _isTouchInsideBounds; |
| 44 | + BOOL _suppressSuperControlActionDispatch; |
44 | 45 | CALayer *_underlayLayer; |
45 | 46 | CGFloat _underlayCornerRadii[8]; // [tlH, tlV, trH, trV, blH, blV, brH, brV] outer radii in points |
46 | 47 | UIEdgeInsets _underlayBorderInsets; // border widths for padding-box inset |
@@ -591,53 +592,101 @@ - (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event |
591 | 592 | return [super beginTrackingWithTouch:touch withEvent:event]; |
592 | 593 | } |
593 | 594 |
|
| 595 | +// Mirrors `sendActionsForControlEvents:` but preserves the real `UIEvent` |
| 596 | +// so target-actions with a `forEvent:` parameter receive the touches. |
| 597 | +// The public `sendActionsForControlEvents:` passes a nil event, which would |
| 598 | +// make handlers reading `event.allTouches.count` observe 0 pointers. |
| 599 | +- (void)rngh_sendActionsForControlEvents:(UIControlEvents)controlEvents withEvent:(UIEvent *)event |
| 600 | +{ |
| 601 | + for (id target in [self allTargets]) { |
| 602 | + for (NSString *actionName in [self actionsForTarget:target forControlEvent:controlEvents]) { |
| 603 | + [self sendAction:NSSelectorFromString(actionName) to:target forEvent:event]; |
| 604 | + } |
| 605 | + } |
| 606 | +} |
| 607 | + |
| 608 | +// UIControl's default `touchesMoved:` / `touchesEnded:` invoke |
| 609 | +// `{continue|end}TrackingWithTouch:` and THEN dispatch their own Drag* / Up* |
| 610 | +// actions via `sendAction:to:forEvent:` using Apple's 70-point retention-offset |
| 611 | +// hit-test. That double-fires our handlers (once from our manual dispatch in |
| 612 | +// the tracking hooks, once from UIControl's retention-offset path). We swallow |
| 613 | +// UIControl's dispatch via this override; the flag is armed by our tracking |
| 614 | +// hooks only after our own dispatch has already run. |
| 615 | +- (void)sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event |
| 616 | +{ |
| 617 | + if (_suppressSuperControlActionDispatch) { |
| 618 | + return; |
| 619 | + } |
| 620 | + [super sendAction:action to:target forEvent:event]; |
| 621 | +} |
| 622 | + |
| 623 | +- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event |
| 624 | +{ |
| 625 | + [super touchesMoved:touches withEvent:event]; |
| 626 | + _suppressSuperControlActionDispatch = NO; |
| 627 | +} |
| 628 | + |
| 629 | +- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event |
| 630 | +{ |
| 631 | + [super touchesEnded:touches withEvent:event]; |
| 632 | + _suppressSuperControlActionDispatch = NO; |
| 633 | +} |
| 634 | + |
594 | 635 | - (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event |
595 | 636 | { |
596 | | - // DO NOT call super. We are entirely taking over the drag event generation. |
| 637 | + // We take over drag event generation to enforce strict hitslop bounds |
| 638 | + // (bypassing Apple's retention offset). After our dispatch, set the |
| 639 | + // suppress flag so UIControl's default post-tracking dispatch in |
| 640 | + // `touchesMoved:` gets swallowed by our `sendAction:to:forEvent:` override. |
597 | 641 |
|
598 | 642 | CGPoint location = [touch locationInView:self]; |
599 | 643 | CGRect hitFrame = UIEdgeInsetsInsetRect(self.bounds, self.hitTestEdgeInsets); |
600 | 644 | BOOL currentlyInside = CGRectContainsPoint(hitFrame, location); |
601 | 645 |
|
602 | 646 | if (currentlyInside) { |
603 | 647 | if (!_isTouchInsideBounds) { |
604 | | - [self sendActionsForControlEvents:UIControlEventTouchDragEnter]; |
| 648 | + [self rngh_sendActionsForControlEvents:UIControlEventTouchDragEnter withEvent:event]; |
605 | 649 | _isTouchInsideBounds = YES; |
606 | 650 | } |
607 | 651 |
|
608 | 652 | // Targets may call `cancelTrackingWithEvent:` in response to DragEnter. |
609 | 653 | if (self.tracking) { |
610 | | - [self sendActionsForControlEvents:UIControlEventTouchDragInside]; |
| 654 | + [self rngh_sendActionsForControlEvents:UIControlEventTouchDragInside withEvent:event]; |
611 | 655 | } |
612 | 656 | } else { |
613 | 657 | if (_isTouchInsideBounds) { |
614 | | - [self sendActionsForControlEvents:UIControlEventTouchDragExit]; |
| 658 | + [self rngh_sendActionsForControlEvents:UIControlEventTouchDragExit withEvent:event]; |
615 | 659 | _isTouchInsideBounds = NO; |
616 | 660 | } |
617 | 661 |
|
618 | 662 | // Targets may call `cancelTrackingWithEvent:` in response to DragExit. |
619 | 663 | if (self.tracking) { |
620 | | - [self sendActionsForControlEvents:UIControlEventTouchDragOutside]; |
| 664 | + [self rngh_sendActionsForControlEvents:UIControlEventTouchDragOutside withEvent:event]; |
621 | 665 | } |
622 | 666 | } |
623 | 667 |
|
| 668 | + _suppressSuperControlActionDispatch = YES; |
| 669 | + |
624 | 670 | // If `cancelTrackingWithEvent` was called, `self.tracking` will be NO. |
625 | 671 | return self.tracking; |
626 | 672 | } |
627 | 673 |
|
628 | 674 | - (void)endTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event |
629 | 675 | { |
630 | | - // Also bypass super here so that the final "up" event respects the |
631 | | - // strict bounds, rather than Apple's 70-point. |
| 676 | + // Same rationale as `continueTrackingWithTouch:` — we dispatch the final |
| 677 | + // Up* event ourselves using strict hitslop bounds, then set the suppress |
| 678 | + // flag so UIControl's default dispatch in `touchesEnded:` gets swallowed. |
632 | 679 |
|
633 | 680 | if (touch != nil) { |
634 | 681 | CGPoint location = [touch locationInView:self]; |
635 | 682 | CGRect hitFrame = UIEdgeInsetsInsetRect(self.bounds, self.hitTestEdgeInsets); |
636 | 683 | if (CGRectContainsPoint(hitFrame, location)) { |
637 | | - [self sendActionsForControlEvents:UIControlEventTouchUpInside]; |
| 684 | + [self rngh_sendActionsForControlEvents:UIControlEventTouchUpInside withEvent:event]; |
638 | 685 | } else { |
639 | | - [self sendActionsForControlEvents:UIControlEventTouchUpOutside]; |
| 686 | + [self rngh_sendActionsForControlEvents:UIControlEventTouchUpOutside withEvent:event]; |
640 | 687 | } |
| 688 | + |
| 689 | + _suppressSuperControlActionDispatch = YES; |
641 | 690 | } |
642 | 691 |
|
643 | 692 | _isTouchInsideBounds = NO; |
|
0 commit comments