Skip to content

Commit 16fc67a

Browse files
committed
feat(pressable-feedback): add usePressableFeedbackRootAnimation hook
1 parent d9504f1 commit 16fc67a

File tree

3 files changed

+106
-64
lines changed

3 files changed

+106
-64
lines changed

example/src/app/(home)/components/pressable-feedback.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const DefaultContent = () => {
1212
return (
1313
<View className="flex-1 px-5 items-center justify-center">
1414
<View className="flex-row items-center justify-center gap-4">
15-
<PressableFeedback className="bg-surface rounded-2xl h-36 w-36 items-center justify-center">
15+
<PressableFeedback className="bg-surface rounded-2xl h-[150px] w-full items-center justify-center">
1616
<StyledIonicons
1717
name="checkmark"
1818
size={32}

src/components/pressable-feedback/pressable-feedback.animation.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import { Easing, useAnimatedStyle, withTiming } from 'react-native-reanimated';
1+
import { Gesture } from 'react-native-gesture-handler';
2+
import {
3+
Easing,
4+
useAnimatedStyle,
5+
useSharedValue,
6+
withTiming,
7+
} from 'react-native-reanimated';
28
import { useUniwind } from 'uniwind';
39
import { colorKit, useThemeColor } from '../../helpers/theme';
410
import { createContext } from '../../helpers/utils';
@@ -21,6 +27,44 @@ export { PressableFeedbackAnimationProvider, usePressableFeedbackAnimation };
2127

2228
// --------------------------------------------------
2329

30+
/**
31+
* Animation hook for PressableFeedback root component
32+
* Handles ripple gesture and shared values
33+
*/
34+
export function usePressableFeedbackRootAnimation() {
35+
const isPressed = useSharedValue(false);
36+
const pressedCenterX = useSharedValue(0);
37+
const pressedCenterY = useSharedValue(0);
38+
const containerWidth = useSharedValue(0);
39+
const containerHeight = useSharedValue(0);
40+
const rippleProgress = useSharedValue(0);
41+
42+
const gesture = Gesture.Pan()
43+
.onBegin((event) => {
44+
rippleProgress.set(0);
45+
pressedCenterX.set(event.x);
46+
pressedCenterY.set(event.y);
47+
isPressed.set(true);
48+
rippleProgress.set(withTiming(1, { duration: 200 }));
49+
})
50+
.onFinalize(() => {
51+
isPressed.set(false);
52+
rippleProgress.set(withTiming(2, { duration: 400 }));
53+
});
54+
55+
return {
56+
isPressed,
57+
pressedCenterX,
58+
pressedCenterY,
59+
containerWidth,
60+
containerHeight,
61+
rippleProgress,
62+
gesture,
63+
};
64+
}
65+
66+
// --------------------------------------------------
67+
2468
/**
2569
* Animation hook for PressableFeedback highlight overlay
2670
* Handles opacity and background color animations for the highlight effect

src/components/pressable-feedback/pressable-feedback.tsx

Lines changed: 60 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,21 @@
11
import { forwardRef, useCallback, useMemo, type FC } from 'react';
2-
import {
3-
Pressable,
4-
StyleSheet,
5-
type GestureResponderEvent,
6-
type LayoutChangeEvent,
7-
} from 'react-native';
2+
import { Pressable, StyleSheet, type LayoutChangeEvent } from 'react-native';
83

94
import Animated, {
105
interpolate,
116
useAnimatedStyle,
12-
useSharedValue,
137
withTiming,
148
} from 'react-native-reanimated';
9+
import Svg, { Defs, RadialGradient, Rect, Stop } from 'react-native-svg';
1510

11+
import { GestureDetector } from 'react-native-gesture-handler';
1612
import { useThemeColor } from '../../helpers/theme';
1713
import type { PressableRef } from '../../helpers/types';
1814
import {
1915
PressableFeedbackAnimationProvider,
2016
usePressableFeedbackAnimation,
2117
usePressableFeedbackHighlightAnimation,
18+
usePressableFeedbackRootAnimation,
2219
} from './pressable-feedback.animation';
2320
import { DISPLAY_NAME } from './pressable-feedback.constants';
2421
import pressableFeedbackStyles from './pressable-feedback.styles';
@@ -34,46 +31,21 @@ const PressableFeedback = forwardRef<PressableRef, PressableFeedbackProps>(
3431
isDisabled = false,
3532
className,
3633
children,
37-
onPress,
38-
onPressIn,
39-
onPressOut,
4034
onLayout,
4135
...restProps
4236
} = props;
4337

4438
const tvStyles = pressableFeedbackStyles({ className });
4539

46-
const isPressed = useSharedValue(false);
47-
const pressedCenterX = useSharedValue(0);
48-
const pressedCenterY = useSharedValue(0);
49-
const containerWidth = useSharedValue(0);
50-
const containerHeight = useSharedValue(0);
51-
const rippleProgress = useSharedValue(0);
52-
53-
const handlePressIn = useCallback(
54-
(event: GestureResponderEvent) => {
55-
rippleProgress.set(0);
56-
isPressed.set(true);
57-
pressedCenterX.set(event.nativeEvent.locationX);
58-
pressedCenterY.set(event.nativeEvent.locationY);
59-
rippleProgress.set(withTiming(1, { duration: 200 }));
60-
// @ts-ignore
61-
onPressIn?.(event);
62-
},
63-
// eslint-disable-next-line react-hooks/exhaustive-deps
64-
[isPressed, onPressIn]
65-
);
66-
67-
const handlePressOut = useCallback(
68-
(event: GestureResponderEvent) => {
69-
rippleProgress.set(withTiming(2, { duration: 400 }));
70-
isPressed.set(false);
71-
// @ts-ignore
72-
onPressOut?.(event);
73-
},
74-
// eslint-disable-next-line react-hooks/exhaustive-deps
75-
[isPressed, onPressOut]
76-
);
40+
const {
41+
isPressed,
42+
pressedCenterX,
43+
pressedCenterY,
44+
containerWidth,
45+
containerHeight,
46+
rippleProgress,
47+
gesture,
48+
} = usePressableFeedbackRootAnimation();
7749

7850
const handleLayout = useCallback(
7951
(event: LayoutChangeEvent) => {
@@ -106,20 +78,19 @@ const PressableFeedback = forwardRef<PressableRef, PressableFeedbackProps>(
10678

10779
return (
10880
<PressableFeedbackAnimationProvider value={animationContextValue}>
109-
<AnimatedPressable
110-
ref={ref}
111-
disabled={isDisabled}
112-
className={tvStyles}
113-
onPress={onPress}
114-
onPressIn={handlePressIn}
115-
onPressOut={handlePressOut}
116-
onLayout={handleLayout}
117-
{...restProps}
118-
>
119-
{/* <PressableFeedbackHighlight animation={animation} /> */}
120-
<PressableFeedbackRipple />
121-
{children}
122-
</AnimatedPressable>
81+
<GestureDetector gesture={gesture}>
82+
<AnimatedPressable
83+
ref={ref}
84+
disabled={isDisabled}
85+
className={tvStyles}
86+
onLayout={handleLayout}
87+
{...restProps}
88+
>
89+
{/* <PressableFeedbackHighlight animation={animation} /> */}
90+
<PressableFeedbackRipple />
91+
{children}
92+
</AnimatedPressable>
93+
</GestureDetector>
12394
</PressableFeedbackAnimationProvider>
12495
);
12596
}
@@ -156,9 +127,8 @@ const PressableFeedbackRipple: FC<{}> = () => {
156127
const themeColorSurfaceSecondary = useThemeColor('on-surface-hover');
157128

158129
const rContainerStyle = useAnimatedStyle(() => {
159-
const circleRadius = Math.sqrt(
160-
containerWidth.get() ** 2 + containerHeight.get() ** 2
161-
);
130+
const circleRadius =
131+
Math.sqrt(containerWidth.get() ** 2 + containerHeight.get() ** 2) * 1.25;
162132

163133
const translateX = pressedCenterX.get() - circleRadius;
164134
const translateY = pressedCenterY.get() - circleRadius;
@@ -169,9 +139,8 @@ const PressableFeedbackRipple: FC<{}> = () => {
169139
borderRadius: circleRadius,
170140
opacity: withTiming(
171141
interpolate(rippleProgress.get(), [0, 1, 2], [0, 1, 0]),
172-
{ duration: 50 }
142+
{ duration: 40 }
173143
),
174-
backgroundColor: themeColorSurfaceSecondary,
175144
transform: [
176145
{
177146
translateX,
@@ -182,7 +151,7 @@ const PressableFeedbackRipple: FC<{}> = () => {
182151
{
183152
scale: withTiming(
184153
interpolate(rippleProgress.get(), [0, 1, 2], [0, 1, 1]),
185-
{ duration: 50 }
154+
{ duration: 40 }
186155
),
187156
},
188157
],
@@ -194,7 +163,36 @@ const PressableFeedbackRipple: FC<{}> = () => {
194163
pointerEvents="none"
195164
className="absolute top-0 left-0"
196165
style={rContainerStyle}
197-
/>
166+
>
167+
<Svg width="100%" height="100%">
168+
<Defs>
169+
<RadialGradient id="rippleGradient" cx="50%" cy="50%" r="50%">
170+
<Stop
171+
offset="0%"
172+
stopColor={themeColorSurfaceSecondary}
173+
stopOpacity="1"
174+
/>
175+
<Stop
176+
offset="75%"
177+
stopColor={themeColorSurfaceSecondary}
178+
stopOpacity="0.75"
179+
/>
180+
<Stop
181+
offset="100%"
182+
stopColor={themeColorSurfaceSecondary}
183+
stopOpacity="0"
184+
/>
185+
</RadialGradient>
186+
</Defs>
187+
<Rect
188+
x="0"
189+
y="0"
190+
width="100%"
191+
height="100%"
192+
fill="url(#rippleGradient)"
193+
/>
194+
</Svg>
195+
</Animated.View>
198196
);
199197
};
200198

0 commit comments

Comments
 (0)