Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,12 @@ - (void)bindToView:(UIView *)view
action:@selector(handleTouchUpInside:forEvent:)
forControlEvents:UIControlEventTouchUpInside];
[control addTarget:self action:@selector(handleDragExit:forEvent:) forControlEvents:UIControlEventTouchDragExit];
[control addTarget:self
action:@selector(handleDragInside:forEvent:)
forControlEvents:UIControlEventTouchDragInside];
[control addTarget:self
action:@selector(handleDragOutside:forEvent:)
forControlEvents:UIControlEventTouchDragOutside];
Comment on lines +149 to +154
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this means that ios buttons will also spam events when pointer moves when pressed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Basically yes.

[control addTarget:self action:@selector(handleDragEnter:forEvent:) forControlEvents:UIControlEventTouchDragEnter];
[control addTarget:self action:@selector(handleTouchCancel:forEvent:) forControlEvents:UIControlEventTouchCancel];
} else {
Expand Down Expand Up @@ -184,7 +190,7 @@ - (void)handleTouchDown:(UIView *)sender forEvent:(UIEvent *)event
}
}

[self sendEventsInState:RNGestureHandlerStateActive
[self sendEventsInState:RNGestureHandlerStateBegan
forViewWithTag:sender.reactTag
withExtraData:[RNGestureHandlerEventExtraData forPointerInside:YES
Comment on lines +193 to 195
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, maybe the "correct" approach would be to send begin and active immediately when pressed down (and do the same on other platforms)?

This may be weird that the button isn't "active" unless the pointer moves.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also thought about it. This would also match current web logic.

withNumberOfTouches:event.allTouches.count
Comment on lines +193 to 196
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handleTouchDown now emits RNGestureHandlerStateBegan instead of ...StateActive. With the current JS button implementations (both legacy src/components/GestureButtons.tsx and v3 src/v3/components/GestureButtons.tsx), the “pressed/active” UI feedback is driven by the gesture entering ACTIVE while the finger is down. If iOS stays in BEGAN until drag/finish, onActiveStateChange(true)/pressed underlay/opacity won’t update on touch down (it may only flash at release due to the synthetic ACTIVE sent right before END in sendEventsInState). If the intent is to start sending BEGAN for parity but keep pressed feedback, consider sending BEGAN and then immediately ACTIVE on touch down (or otherwise ensuring iOS enters ACTIVE at press start).

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expand All @@ -211,22 +217,21 @@ - (void)handleTouchUpInside:(UIView *)sender forEvent:(UIEvent *)event

- (void)handleDragExit:(UIView *)sender forEvent:(UIEvent *)event
{
RNGestureHandlerState newState = RNGestureHandlerStateActive;

// Pointer is moved outside of the view bounds, we cancel button when `shouldCancelWhenOutside` is set
if (self.shouldCancelWhenOutside) {
UIControl *control = (UIControl *)sender;
[control cancelTrackingWithEvent:event];
[self sendEventsInState:RNGestureHandlerStateEnd
forViewWithTag:sender.reactTag
withExtraData:[RNGestureHandlerEventExtraData forPointerInside:NO
withNumberOfTouches:event.allTouches.count
withPointerType:_pointerType]];
} else {
[self sendEventsInState:RNGestureHandlerStateActive
forViewWithTag:sender.reactTag
withExtraData:[RNGestureHandlerEventExtraData forPointerInside:NO
withNumberOfTouches:event.allTouches.count
withPointerType:_pointerType]];

newState = RNGestureHandlerStateEnd;
}

[self sendEventsInState:newState
forViewWithTag:sender.reactTag
withExtraData:[RNGestureHandlerEventExtraData forPointerInside:NO
withNumberOfTouches:event.allTouches.count
withPointerType:_pointerType]];
}

- (void)handleDragEnter:(UIView *)sender forEvent:(UIEvent *)event
Expand All @@ -238,6 +243,24 @@ - (void)handleDragEnter:(UIView *)sender forEvent:(UIEvent *)event
withPointerType:_pointerType]];
}

- (void)handleDragInside:(UIView *)sender forEvent:(UIEvent *)event
{
[self sendEventsInState:RNGestureHandlerStateActive
forViewWithTag:sender.reactTag
withExtraData:[RNGestureHandlerEventExtraData forPointerInside:YES
withNumberOfTouches:event.allTouches.count
withPointerType:_pointerType]];
}

- (void)handleDragOutside:(UIView *)sender forEvent:(UIEvent *)event
{
[self sendEventsInState:RNGestureHandlerStateActive
forViewWithTag:sender.reactTag
withExtraData:[RNGestureHandlerEventExtraData forPointerInside:NO
withNumberOfTouches:event.allTouches.count
withPointerType:_pointerType]];
}

- (void)handleTouchCancel:(UIView *)sender forEvent:(UIEvent *)event
{
[self sendEventsInState:RNGestureHandlerStateCancelled
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,7 @@
this.props.onPress(pointerInside);
}

if (
!this.lastActive &&
// NativeViewGestureHandler sends different events based on platform
state === (Platform.OS !== 'android' ? State.ACTIVE : State.BEGAN) &&
pointerInside
) {
if (!this.lastActive && state === State.BEGAN && pointerInside) {
this.longPressDetected = false;
if (this.props.onLongPress) {
this.longPressTimeout = setTimeout(
Expand Down Expand Up @@ -152,14 +147,14 @@
ref,
...props
}: Omit<LegacyBaseButtonProps, 'innerRef'> & {
ref?: React.ForwardedRef<React.ComponentType<any>> | undefined;

Check warning on line 150 in packages/react-native-gesture-handler/src/components/GestureButtons.tsx

View workflow job for this annotation

GitHub Actions / check

Unexpected any. Specify a different type
}) => <InnerBaseButton innerRef={ref} {...props} />;

const AnimatedBaseButton = ({
ref,
...props
}: Animated.AnimatedProps<BaseButtonWithRefProps> & {
ref?: React.ForwardedRef<React.ComponentType<any>> | undefined;

Check warning on line 157 in packages/react-native-gesture-handler/src/components/GestureButtons.tsx

View workflow job for this annotation

GitHub Actions / check

Unexpected any. Specify a different type
}) => <AnimatedInnerBaseButton innerRef={ref} {...props} />;

const btnStyles = StyleSheet.create({
Expand Down Expand Up @@ -187,7 +182,7 @@

private onActiveStateChange = (active: boolean) => {
if (Platform.OS !== 'android') {
this.opacity.setValue(active ? this.props.activeOpacity! : 0);

Check warning on line 185 in packages/react-native-gesture-handler/src/components/GestureButtons.tsx

View workflow job for this annotation

GitHub Actions / check

Forbidden non-null assertion
}

this.props.onActiveStateChange?.(active);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ function getIosStatesConfig(
eventName: StateMachineEvent.LONG_PRESS_TOUCHES_DOWN,
},
{
eventName: StateMachineEvent.NATIVE_START,
eventName: StateMachineEvent.NATIVE_BEGIN,
callback: handlePressIn,
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,26 +38,26 @@ export const BaseButton = (props: BaseButtonProps) => {
};

const onBegin = (e: CallbackEventType) => {
if (Platform.OS === 'android' && e.pointerInside) {
longPressDetected.current = false;
if (onLongPress) {
longPressTimeout.current = setTimeout(wrappedLongPress, delayLongPress);
}
if (!e.pointerInside) {
return;
}

props.onBegin?.(e);
// iOS, macOS. Web has its own implementation of button.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why remove the pointerInside check?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (Platform.OS !== 'android') {
onActiveStateChange?.(true);
}
};

const onActivate = (e: CallbackEventType) => {
onActiveStateChange?.(true);
longPressDetected.current = false;
if (onLongPress) {
longPressTimeout.current = setTimeout(wrappedLongPress, delayLongPress);
}

if (Platform.OS !== 'android' && e.pointerInside) {
longPressDetected.current = false;
if (onLongPress) {
longPressTimeout.current = setTimeout(wrappedLongPress, delayLongPress);
}
props.onBegin?.(e);
};

props.onBegin?.(e);
const onActivate = (e: CallbackEventType) => {
if (Platform.OS === 'android' && e.pointerInside) {
onActiveStateChange?.(true);
}

if (!e.pointerInside && longPressTimeout.current !== undefined) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -302,8 +302,9 @@ const Pressable = (props: PressableProps) => {
stateMachine.handleEvent(StateMachineEvent.NATIVE_BEGIN);
},
onActivate: () => {
if (Platform.OS !== 'android') {
if (Platform.OS !== 'android' && Platform.OS !== 'ios') {
// Native.onActivate is broken with Android + hitSlop
// On iOS, onActivate fires on drag (not touch down), so we use onBegin + LONG_PRESS_TOUCHES_DOWN instead
stateMachine.handleEvent(StateMachineEvent.NATIVE_START);
}
},
Expand All @@ -317,9 +318,7 @@ const Pressable = (props: PressableProps) => {
success ? StateMachineEvent.FINALIZE : StateMachineEvent.CANCEL
);

if (Platform.OS !== 'ios') {
handleFinalize();
}
handleFinalize();
},
enabled: disabled !== true,
disableReanimated: true,
Expand Down
Loading