77//
88
99#import " RNGestureHandlerButton.h"
10+ #import " Handlers/RNNativeViewHandler.h"
1011
1112#if !TARGET_OS_OSX
1213#import < UIKit/UIKit.h>
1819#import < React/RCTConversions.h>
1920#import < React/RCTFabricComponentsPlugins.h>
2021
22+ @interface RNGestureHandlerButton () <RNGHNativeViewHandlerStateObserver>
23+ @end
24+
2125/* *
2226 * Gesture Handler Button components overrides standard mechanism used by RN
2327 * to determine touch target, which normally would reurn the UIView that is placed
@@ -50,6 +54,14 @@ @implementation RNGestureHandlerButton {
5054 BOOL _isHovered;
5155 BOOL _isPressed;
5256 dispatch_block_t _pendingHoverOutBlock;
57+
58+ // Press event state machine, driven by the state of the managed handler.
59+ // Cannot rely on the pointerInside flag alone because it may change multiple
60+ // times between the dispatched events.
61+ BOOL _lastEventWasInside;
62+ BOOL _longPressDetected;
63+ dispatch_block_t _pendingLongPressBlock;
64+ RNGestureHandlerEventExtraData *_lastObservedExtraData;
5365#if TARGET_OS_OSX
5466 NSTrackingArea *_hoverTrackingArea;
5567#endif
@@ -87,6 +99,12 @@ - (void)commonInit
8799 _isHovered = NO ;
88100 _isPressed = NO ;
89101 _pendingHoverOutBlock = nil ;
102+ _hasLongPressHandler = NO ;
103+ _managedHandlerTag = nil ;
104+ _lastEventWasInside = NO ;
105+ _longPressDetected = NO ;
106+ _pendingLongPressBlock = nil ;
107+ _lastObservedExtraData = nil ;
90108#if TARGET_OS_OSX
91109 self.wantsLayer = YES ; // Crucial for macOS layer-backing
92110#endif
@@ -148,6 +166,10 @@ - (void)prepareForRecycle
148166 // when defaults are unchanged between mounts.
149167 [self cancelPendingPressOutAnimation ];
150168 [self cancelPendingHoverOut ];
169+ [self cancelPendingLongPress ];
170+ _lastEventWasInside = NO ;
171+ _longPressDetected = NO ;
172+ _lastObservedExtraData = nil ;
151173
152174 RNGHUIView *target = self.animationTarget ?: self;
153175 target.layer .transform = CATransform3DIdentity;
@@ -172,12 +194,16 @@ - (void)viewWillMoveToWindow:(RNGHWindow *)newWindow
172194 if (newWindow == nil ) {
173195 [self cancelPendingPressOutAnimation ];
174196 [self cancelPendingHoverOut ];
197+ [self cancelPendingLongPress ];
175198 [self applyStartAnimationState ];
176199 _isTouchInsideBounds = NO ;
177200 _suppressSuperControlActionDispatch = NO ;
178201 _pressInTimestamp = 0 ;
179202 _isHovered = NO ;
180203 _isPressed = NO ;
204+ _lastEventWasInside = NO ;
205+ _longPressDetected = NO ;
206+ _lastObservedExtraData = nil ;
181207 }
182208}
183209#else
@@ -187,12 +213,16 @@ - (void)willMoveToWindow:(RNGHWindow *)newWindow
187213 if (newWindow == nil ) {
188214 [self cancelPendingPressOutAnimation ];
189215 [self cancelPendingHoverOut ];
216+ [self cancelPendingLongPress ];
190217 [self applyStartAnimationState ];
191218 _isTouchInsideBounds = NO ;
192219 _suppressSuperControlActionDispatch = NO ;
193220 _pressInTimestamp = 0 ;
194221 _isHovered = NO ;
195222 _isPressed = NO ;
223+ _lastEventWasInside = NO ;
224+ _longPressDetected = NO ;
225+ _lastObservedExtraData = nil ;
196226 }
197227}
198228#endif
@@ -566,6 +596,110 @@ - (void)handleAnimatePressOut
566596 }
567597}
568598
599+ #pragma mark - Press event state machine
600+
601+ - (void )onHandlerUpdate:(RNGestureHandlerEventExtraData *)extraData
602+ {
603+ if (_managedHandlerTag == nil ) {
604+ return ;
605+ }
606+
607+ _lastObservedExtraData = extraData;
608+ BOOL pointerInside = [extraData.data[@" pointerInside" ] boolValue ];
609+
610+ if (pointerInside == _lastEventWasInside) {
611+ return ;
612+ }
613+
614+ if (pointerInside) {
615+ [self dispatchButtonEvent: RNGHButtonEventTypePressIn withExtraData: extraData];
616+ } else {
617+ [self dispatchButtonEvent: RNGHButtonEventTypePressOut withExtraData: extraData];
618+ [self cancelPendingLongPress ];
619+ }
620+ }
621+
622+ - (void )onHandlerStateChange:(RNGestureHandlerState)newState
623+ prevState:(RNGestureHandlerState)prevState
624+ extraData:(RNGestureHandlerEventExtraData *)extraData
625+ {
626+ if (_managedHandlerTag == nil ) {
627+ return ;
628+ }
629+
630+ _lastObservedExtraData = extraData;
631+
632+ // Capture a local copy, since dispatching the events changes the flag.
633+ // Specifically the PressOut -> Press scenario on the END state.
634+ BOOL localLastEventWasInside = _lastEventWasInside;
635+ BOOL isFinished = newState == RNGestureHandlerStateEnd || newState == RNGestureHandlerStateFailed ||
636+ newState == RNGestureHandlerStateCancelled;
637+
638+ if (newState == RNGestureHandlerStateBegan) {
639+ [self dispatchButtonEvent: RNGHButtonEventTypePressIn withExtraData: extraData];
640+ _longPressDetected = NO ;
641+
642+ // Duration 0 fires the long press immediately; -1 (unset) disables it.
643+ if (_hasLongPressHandler && _longPressDuration >= 0 ) {
644+ [self scheduleLongPress ];
645+ }
646+ }
647+
648+ if (isFinished) {
649+ if (localLastEventWasInside) {
650+ [self dispatchButtonEvent: RNGHButtonEventTypePressOut withExtraData: extraData];
651+ }
652+
653+ [self cancelPendingLongPress ];
654+ }
655+
656+ if (newState == RNGestureHandlerStateEnd && !_longPressDetected && localLastEventWasInside) {
657+ [self dispatchButtonEvent: RNGHButtonEventTypePress withExtraData: extraData];
658+ }
659+
660+ if (isFinished) {
661+ [self dispatchButtonEvent: RNGHButtonEventTypeInteractionFinished withExtraData: extraData];
662+ }
663+ }
664+
665+ - (void )dispatchButtonEvent:(RNGHButtonEventType)type withExtraData:(RNGestureHandlerEventExtraData *)extraData
666+ {
667+ [self .pressEventDelegate dispatchButtonEvent: type withExtraData: extraData];
668+
669+ if (type == RNGHButtonEventTypePressIn) {
670+ _lastEventWasInside = YES ;
671+ } else if (type == RNGHButtonEventTypePressOut) {
672+ _lastEventWasInside = NO ;
673+ }
674+ }
675+
676+ - (void )scheduleLongPress
677+ {
678+ [self cancelPendingLongPress ];
679+
680+ __weak auto weakSelf = self;
681+ _pendingLongPressBlock = dispatch_block_create (DISPATCH_BLOCK_ASSIGN_CURRENT , ^{
682+ __strong auto strongSelf = weakSelf;
683+ if (strongSelf) {
684+ strongSelf->_pendingLongPressBlock = nil ;
685+ strongSelf->_longPressDetected = YES ;
686+ [strongSelf dispatchButtonEvent: RNGHButtonEventTypeLongPress withExtraData: strongSelf->_lastObservedExtraData];
687+ }
688+ });
689+ dispatch_after (
690+ dispatch_time (DISPATCH_TIME_NOW , (int64_t )(_longPressDuration * NSEC_PER_MSEC )),
691+ dispatch_get_main_queue (),
692+ _pendingLongPressBlock);
693+ }
694+
695+ - (void )cancelPendingLongPress
696+ {
697+ if (_pendingLongPressBlock) {
698+ dispatch_block_cancel (_pendingLongPressBlock);
699+ _pendingLongPressBlock = nil ;
700+ }
701+ }
702+
569703#if !TARGET_OS_OSX && !TARGET_OS_TV
570704- (void )handleHover:(UIHoverGestureRecognizer *)recognizer
571705{
0 commit comments