Skip to content

Commit 9d671cb

Browse files
authored
Make buttons activate immediately (#4036)
## Description `Native` gesture is specific and its behavior differs across platforms. This leads to strange workarounds in our codebase (e.g. [buttons](https://github.com/software-mansion/react-native-gesture-handler/blob/4a7639d8c83d5d467403cac702ee14ca0a479c4e/packages/react-native-gesture-handler/src/components/GestureButtons.tsx#L71)). In this PR unifies buttons behavior by changing Native gesture. ## Test plan Tested on expo-example app (buttons / Pressable)
1 parent 768d75d commit 9d671cb

6 files changed

Lines changed: 65 additions & 46 deletions

File tree

packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/NativeViewGestureHandler.kt

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,10 @@ class NativeViewGestureHandler : GestureHandler() {
123123

124124
hook.afterGestureEnd(event)
125125
} else if (state == STATE_UNDETERMINED || state == STATE_BEGAN) {
126+
if (state != STATE_BEGAN && hook.canBegin(event)) {
127+
begin()
128+
}
129+
126130
when {
127131
shouldActivateOnStart -> {
128132
tryIntercept(view, event)
@@ -138,12 +142,6 @@ class NativeViewGestureHandler : GestureHandler() {
138142
hook.wantsToHandleEventBeforeActivation() -> {
139143
hook.handleEventBeforeActivation(event)
140144
}
141-
142-
state != STATE_BEGAN -> {
143-
if (hook.canBegin(event)) {
144-
begin()
145-
}
146-
}
147145
}
148146
} else if (state == STATE_ACTIVE) {
149147
hook.sendTouchEvent(view, event)

packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,12 @@ - (void)bindToView:(UIView *)view
146146
action:@selector(handleTouchUpInside:forEvent:)
147147
forControlEvents:UIControlEventTouchUpInside];
148148
[control addTarget:self action:@selector(handleDragExit:forEvent:) forControlEvents:UIControlEventTouchDragExit];
149+
[control addTarget:self
150+
action:@selector(handleDragInside:forEvent:)
151+
forControlEvents:UIControlEventTouchDragInside];
152+
[control addTarget:self
153+
action:@selector(handleDragOutside:forEvent:)
154+
forControlEvents:UIControlEventTouchDragOutside];
149155
[control addTarget:self action:@selector(handleDragEnter:forEvent:) forControlEvents:UIControlEventTouchDragEnter];
150156
[control addTarget:self action:@selector(handleTouchCancel:forEvent:) forControlEvents:UIControlEventTouchCancel];
151157
} else {
@@ -161,8 +167,14 @@ - (void)bindToView:(UIView *)view
161167

162168
- (void)unbindFromView
163169
{
170+
UIView *view = self.recognizer.view;
171+
172+
if ([view isKindOfClass:[UIControl class]]) {
173+
[(UIControl *)view removeTarget:self action:NULL forControlEvents:UIControlEventAllEvents];
174+
}
175+
164176
// Restore the React Native's overriden behavor for not delaying content touches
165-
UIScrollView *scrollView = [self retrieveScrollView:self.recognizer.view];
177+
UIScrollView *scrollView = [self retrieveScrollView:view];
166178
scrollView.delaysContentTouches = NO;
167179

168180
[super unbindFromView];
@@ -184,6 +196,12 @@ - (void)handleTouchDown:(UIView *)sender forEvent:(UIEvent *)event
184196
}
185197
}
186198

199+
[self sendEventsInState:RNGestureHandlerStateBegan
200+
forViewWithTag:sender.reactTag
201+
withExtraData:[RNGestureHandlerEventExtraData forPointerInside:YES
202+
withNumberOfTouches:event.allTouches.count
203+
withPointerType:_pointerType]];
204+
187205
[self sendEventsInState:RNGestureHandlerStateActive
188206
forViewWithTag:sender.reactTag
189207
withExtraData:[RNGestureHandlerEventExtraData forPointerInside:YES
@@ -211,22 +229,21 @@ - (void)handleTouchUpInside:(UIView *)sender forEvent:(UIEvent *)event
211229

212230
- (void)handleDragExit:(UIView *)sender forEvent:(UIEvent *)event
213231
{
232+
RNGestureHandlerState newState = RNGestureHandlerStateActive;
233+
214234
// Pointer is moved outside of the view bounds, we cancel button when `shouldCancelWhenOutside` is set
215235
if (self.shouldCancelWhenOutside) {
216236
UIControl *control = (UIControl *)sender;
217237
[control cancelTrackingWithEvent:event];
218-
[self sendEventsInState:RNGestureHandlerStateEnd
219-
forViewWithTag:sender.reactTag
220-
withExtraData:[RNGestureHandlerEventExtraData forPointerInside:NO
221-
withNumberOfTouches:event.allTouches.count
222-
withPointerType:_pointerType]];
223-
} else {
224-
[self sendEventsInState:RNGestureHandlerStateActive
225-
forViewWithTag:sender.reactTag
226-
withExtraData:[RNGestureHandlerEventExtraData forPointerInside:NO
227-
withNumberOfTouches:event.allTouches.count
228-
withPointerType:_pointerType]];
238+
239+
newState = RNGestureHandlerStateEnd;
229240
}
241+
242+
[self sendEventsInState:newState
243+
forViewWithTag:sender.reactTag
244+
withExtraData:[RNGestureHandlerEventExtraData forPointerInside:NO
245+
withNumberOfTouches:event.allTouches.count
246+
withPointerType:_pointerType]];
230247
}
231248

232249
- (void)handleDragEnter:(UIView *)sender forEvent:(UIEvent *)event
@@ -238,6 +255,24 @@ - (void)handleDragEnter:(UIView *)sender forEvent:(UIEvent *)event
238255
withPointerType:_pointerType]];
239256
}
240257

258+
- (void)handleDragInside:(UIView *)sender forEvent:(UIEvent *)event
259+
{
260+
[self sendEventsInState:RNGestureHandlerStateActive
261+
forViewWithTag:sender.reactTag
262+
withExtraData:[RNGestureHandlerEventExtraData forPointerInside:YES
263+
withNumberOfTouches:event.allTouches.count
264+
withPointerType:_pointerType]];
265+
}
266+
267+
- (void)handleDragOutside:(UIView *)sender forEvent:(UIEvent *)event
268+
{
269+
[self sendEventsInState:RNGestureHandlerStateActive
270+
forViewWithTag:sender.reactTag
271+
withExtraData:[RNGestureHandlerEventExtraData forPointerInside:NO
272+
withNumberOfTouches:event.allTouches.count
273+
withPointerType:_pointerType]];
274+
}
275+
241276
- (void)handleTouchCancel:(UIView *)sender forEvent:(UIEvent *)event
242277
{
243278
[self sendEventsInState:RNGestureHandlerStateCancelled

packages/react-native-gesture-handler/src/components/GestureButtons.tsx

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,7 @@ class InnerBaseButton extends React.Component<BaseButtonWithRefProps> {
6666
this.props.onPress(pointerInside);
6767
}
6868

69-
if (
70-
!this.lastActive &&
71-
// NativeViewGestureHandler sends different events based on platform
72-
state === (Platform.OS !== 'android' ? State.ACTIVE : State.BEGAN) &&
73-
pointerInside
74-
) {
69+
if (!this.lastActive && state === State.BEGAN && pointerInside) {
7570
this.longPressDetected = false;
7671
if (this.props.onLongPress) {
7772
this.longPressTimeout = setTimeout(

packages/react-native-gesture-handler/src/components/Pressable/stateDefinitions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ function getIosStatesConfig(
5757
eventName: StateMachineEvent.LONG_PRESS_TOUCHES_DOWN,
5858
},
5959
{
60-
eventName: StateMachineEvent.NATIVE_START,
60+
eventName: StateMachineEvent.NATIVE_BEGIN,
6161
callback: handlePressIn,
6262
},
6363
{

packages/react-native-gesture-handler/src/v3/components/GestureButtons.tsx

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export const RawButton = createNativeWrapper<
1919
RawButtonProps
2020
>(GestureHandlerButton, {
2121
shouldCancelWhenOutside: false,
22-
shouldActivateOnStart: false,
22+
shouldActivateOnStart: true,
2323
});
2424

2525
export const BaseButton = (props: BaseButtonProps) => {
@@ -38,28 +38,21 @@ export const BaseButton = (props: BaseButtonProps) => {
3838
};
3939

4040
const onBegin = (e: CallbackEventType) => {
41-
if (Platform.OS === 'android' && e.pointerInside) {
42-
longPressDetected.current = false;
43-
if (onLongPress) {
44-
longPressTimeout.current = setTimeout(wrappedLongPress, delayLongPress);
45-
}
46-
47-
props.onBegin?.(e);
41+
if (!e.pointerInside) {
42+
return;
4843
}
49-
};
5044

51-
const onActivate = (e: CallbackEventType) => {
5245
onActiveStateChange?.(true);
5346

54-
if (Platform.OS !== 'android' && e.pointerInside) {
55-
longPressDetected.current = false;
56-
if (onLongPress) {
57-
longPressTimeout.current = setTimeout(wrappedLongPress, delayLongPress);
58-
}
59-
60-
props.onBegin?.(e);
47+
longPressDetected.current = false;
48+
if (onLongPress) {
49+
longPressTimeout.current = setTimeout(wrappedLongPress, delayLongPress);
6150
}
6251

52+
props.onBegin?.(e);
53+
};
54+
55+
const onActivate = (e: CallbackEventType) => {
6356
if (!e.pointerInside && longPressTimeout.current !== undefined) {
6457
clearTimeout(longPressTimeout.current);
6558
longPressTimeout.current = undefined;

packages/react-native-gesture-handler/src/v3/components/Pressable.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,7 @@ const Pressable = (props: PressableProps) => {
308308
stateMachine.handleEvent(StateMachineEvent.NATIVE_BEGIN);
309309
},
310310
onActivate: () => {
311-
if (Platform.OS !== 'android') {
311+
if (Platform.OS !== 'android' && Platform.OS !== 'ios') {
312312
// Native.onActivate is broken with Android + hitSlop
313313
stateMachine.handleEvent(StateMachineEvent.NATIVE_START);
314314
}
@@ -323,9 +323,7 @@ const Pressable = (props: PressableProps) => {
323323
success ? StateMachineEvent.FINALIZE : StateMachineEvent.CANCEL
324324
);
325325

326-
if (Platform.OS !== 'ios') {
327-
handleFinalize();
328-
}
326+
handleFinalize();
329327
},
330328
enabled: disabled !== true,
331329
disableReanimated: true,

0 commit comments

Comments
 (0)