Skip to content

Commit e76de77

Browse files
committed
feat(pressable-feedback): handle riple progress settings
1 parent 6cf8fbf commit e76de77

File tree

4 files changed

+133
-8
lines changed

4 files changed

+133
-8
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const DefaultContent = () => {
1313
<View className="flex-1 px-5 items-center justify-center">
1414
<View className="flex-row items-center justify-center gap-4">
1515
<PressableFeedback
16-
className="bg-surface rounded-2xl h-[100px] w-full items-center justify-center"
16+
className="bg-surface rounded-2xl h-[500px] w-full items-center justify-center"
1717
variant="ripple"
1818
/>
1919
{/* <PressableFeedback className="bg-accent rounded-2xl h-24 w-24 items-center justify-center">

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

Lines changed: 68 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Gesture } from 'react-native-gesture-handler';
33
import {
44
interpolate,
55
useAnimatedStyle,
6+
useDerivedValue,
67
useSharedValue,
78
withTiming,
89
} from 'react-native-reanimated';
@@ -14,6 +15,7 @@ import {
1415
getAnimationValueProperty,
1516
getStyleTransform,
1617
} from '../../helpers/utils/animation';
18+
import { BASE_RIPPLE_PROGRESS_DURATION } from './pressable-feedback.constants';
1719
import type {
1820
PressableFeedbackAnimation,
1921
PressableFeedbackAnimationContextValue,
@@ -48,7 +50,7 @@ export function usePressableFeedbackRootAnimation(options: {
4850
const scaleValue = getAnimationValueProperty({
4951
animationValue: animationConfig?.scale,
5052
property: 'value',
51-
defaultValue: 0.99,
53+
defaultValue: 0.98,
5254
});
5355

5456
const scaleTimingConfig = getAnimationValueMergedConfig({
@@ -57,6 +59,41 @@ export function usePressableFeedbackRootAnimation(options: {
5759
defaultValue: { duration: 200 },
5860
});
5961

62+
const ignoreScaleCoefficient = getAnimationValueProperty({
63+
animationValue: animationConfig?.scale,
64+
property: 'ignoreScaleCoefficient',
65+
defaultValue: false,
66+
});
67+
68+
// Ripple progress animation values
69+
const rippleProgressBaseDuration = getAnimationValueProperty({
70+
animationValue:
71+
variant === 'ripple'
72+
? (
73+
animationConfig as Extract<
74+
PressableFeedbackRippleRootAnimation,
75+
Record<string, any>
76+
>
77+
)?.ripple?.progress
78+
: undefined,
79+
property: 'baseDuration',
80+
defaultValue: BASE_RIPPLE_PROGRESS_DURATION,
81+
});
82+
83+
const ignoreDurationCoefficient = getAnimationValueProperty({
84+
animationValue:
85+
variant === 'ripple'
86+
? (
87+
animationConfig as Extract<
88+
PressableFeedbackRippleRootAnimation,
89+
Record<string, any>
90+
>
91+
)?.ripple?.progress
92+
: undefined,
93+
property: 'ignoreDurationCoefficient',
94+
defaultValue: false,
95+
});
96+
6097
// Shared values
6198
const isPressed = useSharedValue(false);
6299
const scale = useSharedValue(0);
@@ -66,6 +103,19 @@ export function usePressableFeedbackRootAnimation(options: {
66103
const containerHeight = useSharedValue(0);
67104
const rippleProgress = useSharedValue(0);
68105

106+
// Calculate duration coefficient based on diagonal to maintain consistent ripple speed
107+
// across different container sizes. Base diagonal is 450px.
108+
// Can be disabled by setting ignoreDurationCoefficient to true.
109+
const durationCoefficient = useDerivedValue(() => {
110+
if (ignoreDurationCoefficient) return 1;
111+
112+
const baseDiagonal = 450;
113+
const currentDiagonal = Math.sqrt(
114+
containerWidth.get() ** 2 + containerHeight.get() ** 2
115+
);
116+
return currentDiagonal > 0 ? currentDiagonal / baseDiagonal : 1;
117+
});
118+
69119
// Gesture handling
70120
const gesture = Gesture.Pan()
71121
.onBegin((event) => {
@@ -78,7 +128,11 @@ export function usePressableFeedbackRootAnimation(options: {
78128
pressedCenterX.set(event.x);
79129
pressedCenterY.set(event.y);
80130
if (rippleProgress.get() === 0) {
81-
rippleProgress.set(withTiming(1, { duration: 250 }));
131+
const adjustedDuration = Math.min(
132+
rippleProgressBaseDuration * durationCoefficient.get(),
133+
rippleProgressBaseDuration * 2
134+
);
135+
rippleProgress.set(withTiming(1, { duration: adjustedDuration }));
82136
}
83137
})
84138
.onFinalize(() => {
@@ -87,15 +141,23 @@ export function usePressableFeedbackRootAnimation(options: {
87141

88142
if (variant === 'highlight') return;
89143

90-
rippleProgress.set(withTiming(2, { duration: 400 }));
144+
const adjustedDuration = Math.min(
145+
rippleProgressBaseDuration * durationCoefficient.get(),
146+
rippleProgressBaseDuration * 2
147+
);
148+
rippleProgress.set(withTiming(2, { duration: adjustedDuration }));
91149
});
92150

93151
const styleTransform = getStyleTransform(style);
94152

95153
const rContainerStyle = useAnimatedStyle(() => {
96-
const baseWidth = 300;
97-
const coefficient =
98-
containerWidth.get() > 0 ? baseWidth / containerWidth.get() : 1;
154+
// Calculate scale coefficient to maintain consistent scale effect across different sizes
155+
// Can be disabled by setting ignoreScaleCoefficient to true
156+
const coefficient = ignoreScaleCoefficient
157+
? 1
158+
: containerWidth.get() > 0
159+
? 300 / containerWidth.get()
160+
: 1;
99161
const adjustedScaleValue = 1 - (1 - scaleValue) * coefficient;
100162

101163
if (isAnimationDisabled) {

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,5 @@ export const DISPLAY_NAME = {
55
ROOT: 'HeroUINative.PressableFeedback.Root',
66
HIGHLIGHT: 'HeroUINative.PressableFeedback.Highlight',
77
} as const;
8+
9+
export const BASE_RIPPLE_PROGRESS_DURATION = 500;

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

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,35 @@ export type PressableFeedbackVariant = 'highlight' | 'ripple';
1717
export type PressableFeedbackScaleAnimation = AnimationValue<{
1818
/**
1919
* Scale value when pressed
20-
* @default 0.99
20+
* @default 0.98
21+
*
22+
* Note: The actual scale is automatically adjusted based on the container's width
23+
* using a scale coefficient. This ensures the scale effect feels consistent across different
24+
* container sizes:
25+
* - Base width: 300px
26+
* - If container width > 300px: scale adjustment decreases (less noticeable scale down)
27+
* - If container width < 300px: scale adjustment increases (more noticeable scale down)
28+
* - Example: 600px width → 0.5x coefficient → adjustedScale = 1 - (1 - 0.98) * 0.5 = 0.99
29+
* - Example: 150px width → 2x coefficient → adjustedScale = 1 - (1 - 0.98) * 2 = 0.96
30+
*
31+
* This automatic scaling creates the same visual feel on different sized containers
32+
* by adjusting the scale effect relative to the container size.
2133
*/
2234
value?: number;
2335
/**
2436
* Animation timing configuration
2537
* @default { duration: 200 }
2638
*/
2739
timingConfig?: WithTimingConfig;
40+
/**
41+
* Ignore the scale coefficient and use the scale value directly
42+
*
43+
* When set to true, the scale coefficient will return 1, meaning the actual scale
44+
* will always equal the value regardless of the container's width.
45+
*
46+
* @default false
47+
*/
48+
ignoreScaleCoefficient?: boolean;
2849
}>;
2950

3051
/**
@@ -72,6 +93,46 @@ export type PressableFeedbackRippleAnimation = AnimationValue<{
7293
*/
7394
value?: string;
7495
}>;
96+
/**
97+
* Progress animation configuration for the ripple effect
98+
*
99+
* This controls how the ripple progresses over time from the center to the edges.
100+
* The progress is represented as a shared value that animates from 0 to 2:
101+
* - 0 to 1: Initial expansion phase (press begins)
102+
* - 1 to 2: Final expansion and fade out phase (press ends)
103+
*/
104+
progress?: AnimationValue<{
105+
/**
106+
* Base duration for the ripple progress animation in milliseconds
107+
*
108+
* This value controls how fast the ripple progresses across the container.
109+
* Lower values mean faster ripple expansion, higher values mean slower expansion.
110+
*
111+
* @default 500
112+
*
113+
* Note: The actual duration is automatically adjusted based on the container's diagonal size
114+
* using a durationCoefficient. This ensures the ripple feels consistent across different
115+
* container sizes:
116+
* - Base diagonal: 450px
117+
* - If container diagonal > 450px: duration increases proportionally (max 2x baseDuration)
118+
* - If container diagonal < 450px: duration decreases proportionally
119+
* - Example: 900px diagonal → 2x coefficient → duration = baseDuration * 2 (capped at 2x)
120+
* - Example: 225px diagonal → 0.5x coefficient → duration = baseDuration * 0.5
121+
*
122+
* This automatic scaling creates the same visual feel on different sized containers
123+
* by making the ripple travel at a consistent speed relative to the container size.
124+
*/
125+
baseDuration?: number;
126+
/**
127+
* Ignore the duration coefficient and use the base duration directly
128+
*
129+
* When set to true, the durationCoefficient will return 1, meaning the actual duration
130+
* will always equal baseDuration regardless of the container's diagonal size.
131+
*
132+
* @default false
133+
*/
134+
ignoreDurationCoefficient?: boolean;
135+
}>;
75136
/**
76137
* Opacity animation for the ripple effect
77138
*/

0 commit comments

Comments
 (0)