Skip to content

Commit 9c91942

Browse files
authored
Reimplement Incubator.TouchableOpacity with reanimatedv2 (#1380)
* Reimplement Incubator.TouchableOpacity with reanimatedv2 * Update gesture-handler version * Fix long press behavior and cancel event * Remove commented code * Minor refactor with repeating code * Fix array deps for onPress callbacks * Fix deps array
1 parent c2c5b91 commit 9c91942

File tree

5 files changed

+244
-0
lines changed

5 files changed

+244
-0
lines changed

demo/src/screens/incubatorScreens/TouchableOpacityScreen.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,19 @@ class TouchableOpacityScreen extends Component {
5151
{this.renderExample('feedbackColor', {backgroundColor: Colors.red30, feedbackColor: Colors.red10})}
5252
{this.renderExample('activeScale', {activeScale: 0.95})}
5353
{this.renderExample('activeOpacity', {activeOpacity: 0.6})}
54+
55+
<Incubator.TouchableOpacity2
56+
marginT-20
57+
onPress={this.onPress}
58+
onLongPress={this.onLongPress}
59+
backgroundColor={Colors.blue30}
60+
feedbackColor={Colors.blue50}
61+
style={{alignItems: 'center', paddingHorizontal: 20, paddingVertical: 8, borderRadius: 50}}
62+
activeOpacity={1}
63+
activeScale={0.98}
64+
>
65+
<Text white>TouchableOpacity2</Text>
66+
</Incubator.TouchableOpacity2>
5467
</View>
5568
);
5669
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import React from 'react';
2+
import { LayoutChangeEvent } from 'react-native';
3+
import { State } from 'react-native-gesture-handler';
4+
import { ViewProps } from '../components/view';
5+
export declare type TouchableOpacityProps = {
6+
/**
7+
* Background color
8+
*/
9+
backgroundColor?: string;
10+
/**
11+
* Background color when actively pressing the touchable
12+
*/
13+
feedbackColor?: string;
14+
/**
15+
* Opacity value when actively pressing the touchable
16+
*/
17+
activeOpacity?: number;
18+
/**
19+
* Scale value when actively pressing the touchable
20+
*/
21+
activeScale?: number;
22+
/**
23+
* Callback for when tapping the touchable
24+
*/
25+
onPress?: (props: any) => void;
26+
/**
27+
* Callback for when long pressing the touchable
28+
*/
29+
onLongPress?: (props: any) => void;
30+
/**
31+
* Pass controlled pressState to track gesture state changes
32+
*/
33+
pressState?: State;
34+
/**
35+
* If true, disable all interactions for this component.
36+
*/
37+
disabled?: boolean;
38+
/**
39+
* Pass custom style
40+
*/
41+
style?: ViewProps['style'];
42+
/**
43+
* Custom value of any type to pass on to TouchableOpacity and receive back in onPress callback
44+
*/
45+
customValue?: any;
46+
onLayout?: (event: LayoutChangeEvent) => void;
47+
testID?: string;
48+
};
49+
declare const _default: React.ComponentClass<TouchableOpacityProps & {
50+
useCustomTheme?: boolean | undefined;
51+
}, any>;
52+
export default _default;

generatedTypes/incubator/index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export { default as TabController } from './TabController';
22
export { default as TextField, TextFieldProps, FieldContextType } from './TextField';
33
export { default as TouchableOpacity, TouchableOpacityProps } from './TouchableOpacity';
4+
export { default as TouchableOpacity2 } from './TouchableOpacity2';
45
export { default as WheelPicker, WheelPickerProps } from './WheelPicker';

src/incubator/TouchableOpacity2.tsx

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
import React, {PropsWithChildren, useCallback, useMemo} from 'react';
2+
import {LayoutChangeEvent} from 'react-native';
3+
import Reanimated, {
4+
useAnimatedGestureHandler,
5+
useAnimatedStyle,
6+
useSharedValue,
7+
withTiming,
8+
interpolate,
9+
interpolateColor,
10+
runOnJS
11+
} from 'react-native-reanimated';
12+
import {TapGestureHandler, LongPressGestureHandler, State} from 'react-native-gesture-handler';
13+
import {asBaseComponent, forwardRef, BaseComponentInjectedProps, ForwardRefInjectedProps} from '../commons/new';
14+
import {ViewProps} from '../components/view';
15+
16+
export type TouchableOpacityProps = {
17+
/**
18+
* Background color
19+
*/
20+
backgroundColor?: string;
21+
/**
22+
* Background color when actively pressing the touchable
23+
*/
24+
feedbackColor?: string;
25+
/**
26+
* Opacity value when actively pressing the touchable
27+
*/
28+
activeOpacity?: number;
29+
/**
30+
* Scale value when actively pressing the touchable
31+
*/
32+
activeScale?: number;
33+
/**
34+
* Callback for when tapping the touchable
35+
*/
36+
onPress?: (props: any) => void;
37+
/**
38+
* Callback for when long pressing the touchable
39+
*/
40+
onLongPress?: (props: any) => void;
41+
/**
42+
* Pass controlled pressState to track gesture state changes
43+
*/
44+
pressState?: State;
45+
/**
46+
* If true, disable all interactions for this component.
47+
*/
48+
disabled?: boolean;
49+
/**
50+
* Pass custom style
51+
*/
52+
style?: ViewProps['style'];
53+
/**
54+
* Custom value of any type to pass on to TouchableOpacity and receive back in onPress callback
55+
*/
56+
customValue?: any;
57+
onLayout?: (event: LayoutChangeEvent) => void;
58+
testID?: string;
59+
};
60+
61+
type Props = PropsWithChildren<TouchableOpacityProps & BaseComponentInjectedProps & ForwardRefInjectedProps>;
62+
63+
/**
64+
* @description: a Better, enhanced TouchableOpacity component
65+
* @modifiers: flex, margin, padding, background
66+
* @example: https://github.com/wix/react-native-ui-lib/blob/master/demo/src/screens/incubatorScreens/TouchableOpacityScreen.js
67+
*/
68+
function TouchableOpacity(props: Props) {
69+
const {
70+
children,
71+
modifiers,
72+
style,
73+
disabled,
74+
forwardedRef,
75+
feedbackColor,
76+
activeOpacity = 0.2,
77+
activeScale = 1,
78+
...others
79+
} = props;
80+
const {borderRadius, paddings, margins, alignments, flexStyle} = modifiers;
81+
82+
const isActive = useSharedValue(0);
83+
/* This flag is for fixing an issue with long press triggering twice
84+
TODO: Consider revisiting this issue to see if it still occurs */
85+
const isLongPressed = useSharedValue(false);
86+
87+
const backgroundColor = useMemo(() => {
88+
return props.backgroundColor || modifiers.backgroundColor;
89+
}, [props.backgroundColor, modifiers.backgroundColor]);
90+
91+
const onPress = useCallback(() => {
92+
props.onPress?.(props);
93+
}, [props.onPress, props.customValue]);
94+
95+
const onLongPress = useCallback(() => {
96+
props.onLongPress?.(props);
97+
}, [props.onLongPress, props.customValue]);
98+
99+
const toggleActive = (value: number) => {
100+
'worklet';
101+
isActive.value = withTiming(value, {duration: 200});
102+
};
103+
104+
const tapGestureHandler = useAnimatedGestureHandler({
105+
onStart: () => {
106+
toggleActive(1);
107+
},
108+
onEnd: () => {
109+
toggleActive(0);
110+
111+
runOnJS(onPress)();
112+
},
113+
onFail: () => {
114+
toggleActive(0);
115+
}
116+
});
117+
118+
const longPressGestureHandler = useAnimatedGestureHandler({
119+
onActive: () => {
120+
if (!isLongPressed.value) {
121+
isLongPressed.value = true;
122+
toggleActive(0);
123+
runOnJS(onLongPress)();
124+
}
125+
},
126+
onFinish: () => {
127+
isLongPressed.value = false;
128+
}
129+
});
130+
131+
const animatedStyle = useAnimatedStyle(() => {
132+
const activeColor = feedbackColor || backgroundColor;
133+
const opacity = interpolate(isActive.value, [0, 1], [1, activeOpacity]);
134+
const scale = interpolate(isActive.value, [0, 1], [1, activeScale]);
135+
136+
return {
137+
backgroundColor: interpolateColor(isActive.value, [0, 1], [backgroundColor, activeColor]),
138+
opacity,
139+
transform: [{scale}]
140+
};
141+
}, [backgroundColor, feedbackColor]);
142+
143+
return (
144+
<TapGestureHandler
145+
// @ts-expect-error
146+
onGestureEvent={tapGestureHandler}
147+
shouldCancelWhenOutside
148+
enabled={!disabled}
149+
>
150+
<Reanimated.View>
151+
{/* @ts-expect-error */}
152+
<LongPressGestureHandler onGestureEvent={longPressGestureHandler} shouldCancelWhenOutside>
153+
<Reanimated.View
154+
{...others}
155+
ref={forwardedRef}
156+
style={[
157+
borderRadius && {borderRadius},
158+
flexStyle,
159+
paddings,
160+
margins,
161+
alignments,
162+
backgroundColor && {backgroundColor},
163+
style,
164+
animatedStyle
165+
]}
166+
>
167+
{children}
168+
</Reanimated.View>
169+
</LongPressGestureHandler>
170+
</Reanimated.View>
171+
</TapGestureHandler>
172+
);
173+
}
174+
175+
TouchableOpacity.displayName = 'Incubator.TouchableOpacity';
176+
177+
export default asBaseComponent<TouchableOpacityProps>(forwardRef<Props>(TouchableOpacity));

src/incubator/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@
22
export {default as TabController} from './TabController';
33
export {default as TextField, TextFieldProps, FieldContextType} from './TextField';
44
export {default as TouchableOpacity, TouchableOpacityProps} from './TouchableOpacity';
5+
export {default as TouchableOpacity2} from './TouchableOpacity2';
56
export {default as WheelPicker, WheelPickerProps} from './WheelPicker';

0 commit comments

Comments
 (0)