Skip to content

Commit 77ff2c0

Browse files
committed
Attach native gesture directly to button on the native side
1 parent c9b9a18 commit 77ff2c0

5 files changed

Lines changed: 162 additions & 37 deletions

File tree

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1064,6 +1064,7 @@ open class GestureHandler {
10641064
const val DIRECTION_LEFT = 2
10651065
const val DIRECTION_UP = 4
10661066
const val DIRECTION_DOWN = 8
1067+
const val ACTION_TYPE_NONE = 0
10671068
const val ACTION_TYPE_REANIMATED_WORKLET = 1
10681069
const val ACTION_TYPE_NATIVE_ANIMATED_EVENT = 2
10691070
const val ACTION_TYPE_JS_FUNCTION_OLD_API = 3

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

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,11 @@ import android.view.accessibility.AccessibilityNodeInfo
2626
import androidx.core.view.children
2727
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
2828
import com.facebook.react.R
29+
import com.facebook.react.bridge.Arguments
2930
import com.facebook.react.bridge.Dynamic
3031
import com.facebook.react.bridge.ReactContext
3132
import com.facebook.react.bridge.ReadableArray
33+
import com.facebook.react.bridge.ReadableMap
3234
import com.facebook.react.module.annotations.ReactModule
3335
import com.facebook.react.uimanager.BackgroundStyleApplicator
3436
import com.facebook.react.uimanager.LengthPercentage
@@ -66,6 +68,85 @@ class RNGestureHandlerButtonViewManager :
6668

6769
public override fun createViewInstance(context: ThemedReactContext) = ButtonViewGroup(context)
6870

71+
override fun onDropViewInstance(view: ButtonViewGroup) {
72+
view.managedHandlerTag?.let { tag ->
73+
getGestureHandlerModule(view).dropGestureHandler(tag)
74+
}
75+
76+
super.onDropViewInstance(view)
77+
}
78+
79+
private fun getGestureHandlerModule(view: ButtonViewGroup) =
80+
(view.context as ReactContext).getNativeModule(RNGestureHandlerModule::class.java)!!
81+
82+
private fun updateManagedHandlerConfig(view: ButtonViewGroup, config: ReadableMap) {
83+
view.managedHandlerTag?.let { getGestureHandlerModule(view).updateGestureHandlerConfig(it, config) }
84+
}
85+
86+
@ReactProp(name = "handlerTag")
87+
override fun setHandlerTag(view: ButtonViewGroup, handlerTag: Double) {
88+
if (view.managedHandlerTag == handlerTag) {
89+
return
90+
}
91+
92+
view.managedHandlerTag?.let { oldTag ->
93+
getGestureHandlerModule(view).dropGestureHandler(oldTag)
94+
}
95+
96+
view.managedHandlerTag = handlerTag
97+
val module = getGestureHandlerModule(view)
98+
99+
module.createGestureHandler(
100+
"NativeViewGestureHandler",
101+
handlerTag,
102+
Arguments.createMap().apply {
103+
putBoolean("shouldActivateOnStart", false)
104+
putBoolean("disallowInterruption", true)
105+
putBoolean("yieldsToContinuousGestures", true)
106+
putBoolean("enabled", view.isEnabled)
107+
view.managedHandlerCancelOnLeave?.let { putBoolean("shouldCancelWhenOutside", it) }
108+
view.managedHandlerTestID?.let { putString("testID", it) }
109+
view.managedHandlerHitSlop?.let { putMap("hitSlop", it) }
110+
},
111+
)
112+
module.attachGestureHandler(handlerTag, view.id.toDouble(), GestureHandler.ACTION_TYPE_NONE.toDouble())
113+
}
114+
115+
@ReactProp(name = "cancelOnLeave")
116+
override fun setCancelOnLeave(view: ButtonViewGroup, cancelOnLeave: Boolean) {
117+
view.managedHandlerCancelOnLeave = cancelOnLeave
118+
updateManagedHandlerConfig(
119+
view,
120+
Arguments.createMap().apply {
121+
putBoolean("shouldCancelWhenOutside", cancelOnLeave)
122+
},
123+
)
124+
}
125+
126+
@ReactProp(name = "gestureTestID")
127+
override fun setGestureTestID(view: ButtonViewGroup, gestureTestID: String?) {
128+
view.managedHandlerTestID = gestureTestID
129+
updateManagedHandlerConfig(
130+
view,
131+
Arguments.createMap().apply {
132+
putString("testID", gestureTestID)
133+
},
134+
)
135+
}
136+
137+
@ReactProp(name = "gestureHitSlop")
138+
override fun setGestureHitSlop(view: ButtonViewGroup, gestureHitSlop: ReadableMap?) {
139+
view.managedHandlerHitSlop = gestureHitSlop
140+
if (gestureHitSlop != null) {
141+
updateManagedHandlerConfig(
142+
view,
143+
Arguments.createMap().apply {
144+
putMap("hitSlop", gestureHitSlop)
145+
},
146+
)
147+
}
148+
}
149+
69150
@ReactProp(name = "hasLongPressHandler")
70151
override fun setHasLongPressHandler(view: ButtonViewGroup, hasLongPressHandler: Boolean) {
71152
view.hasLongPressHandler = hasLongPressHandler
@@ -89,6 +170,13 @@ class RNGestureHandlerButtonViewManager :
89170
@ReactProp(name = "enabled")
90171
override fun setEnabled(view: ButtonViewGroup, enabled: Boolean) {
91172
view.isEnabled = enabled
173+
174+
updateManagedHandlerConfig(
175+
view,
176+
Arguments.createMap().apply {
177+
putBoolean("enabled", enabled)
178+
},
179+
)
92180
}
93181

94182
@ReactProp(name = "borderWidth")
@@ -405,6 +493,16 @@ class RNGestureHandlerButtonViewManager :
405493
}
406494
var useBorderlessDrawable = false
407495

496+
var managedHandlerTag: Double? = null
497+
498+
// Config props may be applied before `handlerTag` (prop order is not guaranteed) and
499+
// `updateManagedHandlerConfig` no-ops until the handler exists, so the values are cached
500+
// here and seeded into the config when `setHandlerTag` creates the handler. This also
501+
// re-applies them when the handler is recreated for a new tag.
502+
var managedHandlerCancelOnLeave: Boolean? = null
503+
var managedHandlerTestID: String? = null
504+
var managedHandlerHitSlop: ReadableMap? = null
505+
408506
var exclusive = true
409507
var hasLongPressHandler = false
410508
var tapAnimationInDuration: Int = 50
@@ -538,7 +636,7 @@ class RNGestureHandlerButtonViewManager :
538636
private var lastEventWasInside = false
539637

540638
override fun onHandlerUpdate(handler: NativeViewGestureHandler) {
541-
if (handler.isWithinBounds == lastEventWasInside) {
639+
if (managedHandlerTag == null || handler.isWithinBounds == lastEventWasInside) {
542640
return
543641
}
544642

@@ -555,6 +653,10 @@ class RNGestureHandlerButtonViewManager :
555653
}
556654

557655
override fun onHandlerStateChange(handler: NativeViewGestureHandler, newState: Int, prevState: Int) {
656+
if (managedHandlerTag == null) {
657+
return
658+
}
659+
558660
// Capture local copy, since lastEventWasInside can change during this method
559661
// Specifically PressOut -> Press scenario on STATE_END
560662
val localLastEventWasInside = lastEventWasInside

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,17 @@ export interface ButtonProps extends ViewProps, AccessibilityProps {
2222
enabled?: boolean | undefined;
2323

2424
hasLongPressHandler?: boolean | undefined;
25+
handlerTag?: number | undefined;
26+
cancelOnLeave?: boolean | undefined;
27+
gestureTestID?: string | undefined;
28+
gestureHitSlop?:
29+
| {
30+
top?: number | undefined;
31+
left?: number | undefined;
32+
bottom?: number | undefined;
33+
right?: number | undefined;
34+
}
35+
| undefined;
2536

2637
/**
2738
* Defines if more than one button could be pressed simultaneously. By default

packages/react-native-gesture-handler/src/specs/RNGestureHandlerButtonNativeComponent.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,16 @@ interface NativeProps extends ViewProps {
2727
| undefined;
2828

2929
hasLongPressHandler?: CodegenTypes.WithDefault<boolean, false>;
30+
handlerTag?: CodegenTypes.Double | undefined;
31+
cancelOnLeave?: CodegenTypes.WithDefault<boolean, true>;
32+
gestureTestID?: string;
33+
gestureHitSlop?: Readonly<{
34+
top?: CodegenTypes.Double | undefined;
35+
left?: CodegenTypes.Double | undefined;
36+
bottom?: CodegenTypes.Double | undefined;
37+
right?: CodegenTypes.Double | undefined;
38+
}>;
39+
3040
exclusive?: CodegenTypes.WithDefault<boolean, true>;
3141
foreground?: boolean;
3242
borderless?: boolean;

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

Lines changed: 37 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
1-
import React, { use, useCallback, useRef } from 'react';
1+
import React, { use, useCallback, useRef, useState } from 'react';
22
import type { NativeSyntheticEvent } from 'react-native';
33
import { Platform } from 'react-native';
44

55
import GestureHandlerButton, {
66
type ButtonEvent,
77
} from '../../../components/GestureHandlerButton';
88
import { getTVProps } from '../../../components/utils';
9-
import { NativeDetector } from '../../detectors/NativeDetector';
10-
import { useNativeGesture } from '../../hooks';
9+
import { getNextHandlerTag } from '../../../handlers/getNextHandlerTag';
1110
import {
1211
isKeyboardDismissingTap,
1312
JSResponderContext,
@@ -85,6 +84,7 @@ export const Touchable = (props: TouchableProps) => {
8584
ref,
8685
...rest
8786
} = props;
87+
const [handlerTag] = useState(() => getNextHandlerTag());
8888

8989
const resolvedDurations = resolveAnimationDuration(animationDuration);
9090
const resolvedDelayLongPress = sanitizeDuration(delayLongPress);
@@ -150,17 +150,6 @@ export const Touchable = (props: TouchableProps) => {
150150
dropKeyboardTapRef.current = null;
151151
}, []);
152152

153-
const nativeGesture = useNativeGesture({
154-
hitSlop: props.hitSlop,
155-
testID: props.testID,
156-
enabled: !disabled,
157-
shouldCancelWhenOutside: cancelOnLeave,
158-
disableReanimated: true,
159-
shouldActivateOnStart: false,
160-
disallowInterruption: true,
161-
yieldsToContinuousGestures: true,
162-
});
163-
164153
const rippleProps = shouldUseNativeRipple
165154
? {
166155
rippleColor: androidRipple?.color,
@@ -172,28 +161,40 @@ export const Touchable = (props: TouchableProps) => {
172161

173162
const tvProps = getTVProps(rest);
174163

164+
const hitSlop =
165+
typeof props.hitSlop === 'number'
166+
? {
167+
top: props.hitSlop,
168+
left: props.hitSlop,
169+
bottom: props.hitSlop,
170+
right: props.hitSlop,
171+
}
172+
: (props.hitSlop ?? undefined);
173+
175174
return (
176-
<NativeDetector gesture={nativeGesture}>
177-
<GestureHandlerButton
178-
{...rest}
179-
{...tvProps}
180-
{...rippleProps}
181-
{...resolvedDurations}
182-
ref={ref ?? null}
183-
enabled={!disabled}
184-
defaultOpacity={defaultOpacity}
185-
defaultUnderlayOpacity={defaultUnderlayOpacity}
186-
activeUnderlayOpacity={activeUnderlayOpacity}
187-
underlayColor={underlayColor}
188-
longPressDuration={resolvedDelayLongPress}
189-
hasLongPressHandler={onLongPress !== undefined}
190-
onPress={internalOnPress}
191-
onPressIn={internalOnPressIn}
192-
onPressOut={internalOnPressOut}
193-
onLongPress={internalOnLongPress}
194-
onInteractionFinished={internalOnInteractionFinished}>
195-
{children}
196-
</GestureHandlerButton>
197-
</NativeDetector>
175+
<GestureHandlerButton
176+
{...rest}
177+
{...tvProps}
178+
{...rippleProps}
179+
{...resolvedDurations}
180+
ref={ref ?? null}
181+
enabled={!disabled}
182+
handlerTag={handlerTag}
183+
cancelOnLeave={cancelOnLeave}
184+
gestureTestID={props.testID}
185+
gestureHitSlop={hitSlop}
186+
defaultOpacity={defaultOpacity}
187+
defaultUnderlayOpacity={defaultUnderlayOpacity}
188+
activeUnderlayOpacity={activeUnderlayOpacity}
189+
underlayColor={underlayColor}
190+
longPressDuration={resolvedDelayLongPress}
191+
hasLongPressHandler={onLongPress !== undefined}
192+
onPress={internalOnPress}
193+
onPressIn={internalOnPressIn}
194+
onPressOut={internalOnPressOut}
195+
onLongPress={internalOnLongPress}
196+
onInteractionFinished={internalOnInteractionFinished}>
197+
{children}
198+
</GestureHandlerButton>
198199
);
199200
};

0 commit comments

Comments
 (0)