Skip to content

Commit c205be2

Browse files
committed
Dispatch press events directly from the native side on iOS
1 parent 0f6398a commit c205be2

3 files changed

Lines changed: 208 additions & 1 deletion

File tree

packages/react-native-gesture-handler/apple/RNGestureHandlerButton.h

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,25 @@
88

99
#import "RNGestureHandler.h"
1010

11+
typedef NS_ENUM(NSInteger, RNGHButtonEventType) {
12+
RNGHButtonEventTypePress,
13+
RNGHButtonEventTypePressIn,
14+
RNGHButtonEventTypePressOut,
15+
RNGHButtonEventTypeLongPress,
16+
RNGHButtonEventTypeInteractionFinished,
17+
};
18+
19+
/*
20+
* Receives press events produced by the button's state machine so that they can
21+
* be dispatched through the component's event emitter.
22+
*/
23+
@protocol RNGHButtonPressEventDelegate <NSObject>
24+
25+
- (void)dispatchButtonEvent:(RNGHButtonEventType)type
26+
withExtraData:(nullable RNGestureHandlerEventExtraData *)extraData;
27+
28+
@end
29+
1130
#if TARGET_OS_OSX
1231

1332
#include <react/renderer/core/LayoutMetrics.h>
@@ -43,6 +62,15 @@
4362
@property (nonatomic, assign) CGFloat hoverScale;
4463
@property (nonatomic, assign) CGFloat hoverUnderlayOpacity;
4564
@property (nonatomic, strong, nullable) RNGHColor *underlayColor;
65+
@property (nonatomic, assign) BOOL hasLongPressHandler;
66+
67+
/**
68+
* Tag of the gesture handler managed by the button component. The press event
69+
* state machine runs only while it's set.
70+
*/
71+
@property (nonatomic, strong, nullable) NSNumber *managedHandlerTag;
72+
73+
@property (nonatomic, weak, nullable) id<RNGHButtonPressEventDelegate> pressEventDelegate;
4674

4775
/**
4876
* The view that press animations are applied to. Defaults to self; set by the

packages/react-native-gesture-handler/apple/RNGestureHandlerButton.mm

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
//
88

99
#import "RNGestureHandlerButton.h"
10+
#import "Handlers/RNNativeViewHandler.h"
1011

1112
#if !TARGET_OS_OSX
1213
#import <UIKit/UIKit.h>
@@ -18,6 +19,9 @@
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
{

packages/react-native-gesture-handler/apple/RNGestureHandlerButtonComponentView.mm

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ static RNGestureHandlerPointerEvents RCTPointerEventsToEnum(facebook::react::Poi
2929
}
3030
}
3131

32-
@interface RNGestureHandlerButtonComponentView () <RCTRNGestureHandlerButtonViewProtocol>
32+
@interface RNGestureHandlerButtonComponentView () <RCTRNGestureHandlerButtonViewProtocol, RNGHButtonPressEventDelegate>
3333
@end
3434

3535
@implementation RNGestureHandlerButtonComponentView {
@@ -63,6 +63,7 @@ - (instancetype)initWithFrame:(CGRect)frame
6363
_moduleId = -1;
6464
_buttonView = [[RNGestureHandlerButton alloc] initWithFrame:self.bounds];
6565
_buttonView.animationTarget = self;
66+
_buttonView.pressEventDelegate = self;
6667

6768
self.contentView = _buttonView;
6869
}
@@ -253,6 +254,49 @@ - (void)finalizeUpdates:(RNComponentViewUpdateMask)updateMask
253254
left:borderMetrics.borderWidths.left];
254255
}
255256

257+
#pragma mark - RNGHButtonPressEventDelegate
258+
259+
- (void)dispatchButtonEvent:(RNGHButtonEventType)type withExtraData:(RNGestureHandlerEventExtraData *)extraData
260+
{
261+
if (_eventEmitter == nullptr) {
262+
return;
263+
}
264+
265+
const auto &eventEmitter = static_cast<const RNGestureHandlerButtonEventEmitter &>(*_eventEmitter);
266+
NSDictionary *data = extraData.data;
267+
268+
// The generated event structs are distinct types with identical fields, hence
269+
// the generic lambda.
270+
auto fillEvent = [&](auto event) {
271+
event.pointerInside = [data[@"pointerInside"] boolValue];
272+
event.x = [data[@"x"] doubleValue];
273+
event.y = [data[@"y"] doubleValue];
274+
event.absoluteX = [data[@"absoluteX"] doubleValue];
275+
event.absoluteY = [data[@"absoluteY"] doubleValue];
276+
event.numberOfPointers = [data[@"numberOfPointers"] intValue];
277+
event.pointerType = [data[@"pointerType"] intValue];
278+
return event;
279+
};
280+
281+
switch (type) {
282+
case RNGHButtonEventTypePress:
283+
eventEmitter.onPress(fillEvent(RNGestureHandlerButtonEventEmitter::OnPress{}));
284+
break;
285+
case RNGHButtonEventTypePressIn:
286+
eventEmitter.onPressIn(fillEvent(RNGestureHandlerButtonEventEmitter::OnPressIn{}));
287+
break;
288+
case RNGHButtonEventTypePressOut:
289+
eventEmitter.onPressOut(fillEvent(RNGestureHandlerButtonEventEmitter::OnPressOut{}));
290+
break;
291+
case RNGHButtonEventTypeLongPress:
292+
eventEmitter.onLongPress(fillEvent(RNGestureHandlerButtonEventEmitter::OnLongPress{}));
293+
break;
294+
case RNGHButtonEventTypeInteractionFinished:
295+
eventEmitter.onInteractionFinished(fillEvent(RNGestureHandlerButtonEventEmitter::OnInteractionFinished{}));
296+
break;
297+
}
298+
}
299+
256300
#pragma mark - RCTComponentViewProtocol
257301

258302
+ (ComponentDescriptorProvider)componentDescriptorProvider
@@ -371,6 +415,7 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &
371415

372416
_moduleId = newProps.moduleId;
373417
_buttonView.userEnabled = newProps.enabled;
418+
_buttonView.hasLongPressHandler = newProps.hasLongPressHandler;
374419
_buttonView.tapAnimationInDuration = newProps.tapAnimationInDuration > 0 ? newProps.tapAnimationInDuration : 0;
375420
_buttonView.tapAnimationOutDuration = newProps.tapAnimationOutDuration > 0 ? newProps.tapAnimationOutDuration : 0;
376421
_buttonView.longPressDuration = newProps.longPressDuration;

0 commit comments

Comments
 (0)