Skip to content

Commit 85df65b

Browse files
committed
feat(pressable-feedback): handle animation prop
1 parent f654d39 commit 85df65b

File tree

4 files changed

+190
-60
lines changed

4 files changed

+190
-60
lines changed

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ 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-[150px] w-full items-center justify-center">
15+
<PressableFeedback
16+
className="bg-surface rounded-2xl h-[150px] w-full items-center justify-center"
17+
animation={false}
18+
>
1619
<StyledIonicons
1720
name="checkmark"
1821
size={32}

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

Lines changed: 68 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,24 @@
1+
import type { ViewStyle } from 'react-native';
12
import { Gesture } from 'react-native-gesture-handler';
23
import {
3-
Easing,
44
interpolate,
55
useAnimatedStyle,
66
useSharedValue,
77
withTiming,
88
} from 'react-native-reanimated';
99
import { useUniwind } from 'uniwind';
10-
import { colorKit, useThemeColor } from '../../helpers/theme';
1110
import { createContext } from '../../helpers/utils';
1211
import {
1312
getAnimationState,
1413
getAnimationValueMergedConfig,
1514
getAnimationValueProperty,
15+
getStyleTransform,
1616
} from '../../helpers/utils/animation';
1717
import type {
18+
PressableFeedbackAnimation,
1819
PressableFeedbackAnimationContextValue,
19-
PressableFeedbackHighlightAnimation,
20+
PressableFeedbackHighlightRootAnimation,
21+
PressableFeedbackVariant,
2022
} from './pressable-feedback.types';
2123

2224
const [PressableFeedbackAnimationProvider, usePressableFeedbackAnimation] =
@@ -30,9 +32,31 @@ export { PressableFeedbackAnimationProvider, usePressableFeedbackAnimation };
3032

3133
/**
3234
* Animation hook for PressableFeedback root component
33-
* Handles ripple gesture and shared values
35+
* Handles gesture, shared values, and scale animation
3436
*/
35-
export function usePressableFeedbackRootAnimation() {
37+
export function usePressableFeedbackRootAnimation(options: {
38+
variant: PressableFeedbackVariant;
39+
animation: PressableFeedbackAnimation | undefined;
40+
style: ViewStyle | undefined;
41+
}) {
42+
const { variant, animation, style } = options;
43+
44+
const { animationConfig, isAnimationDisabled } = getAnimationState(animation);
45+
46+
// Scale animation values
47+
const scaleValue = getAnimationValueProperty({
48+
animationValue: animationConfig?.scale,
49+
property: 'value',
50+
defaultValue: 0.99,
51+
});
52+
53+
const scaleTimingConfig = getAnimationValueMergedConfig({
54+
animationValue: animationConfig?.scale,
55+
property: 'timingConfig',
56+
defaultValue: { duration: 200 },
57+
});
58+
59+
// Shared values
3660
const isPressed = useSharedValue(false);
3761
const scale = useSharedValue(0);
3862
const pressedCenterX = useSharedValue(0);
@@ -41,34 +65,55 @@ export function usePressableFeedbackRootAnimation() {
4165
const containerHeight = useSharedValue(0);
4266
const rippleProgress = useSharedValue(0);
4367

68+
// Gesture handling
4469
const gesture = Gesture.Pan()
4570
.onBegin((event) => {
71+
isPressed.set(true);
72+
scale.set(withTiming(1, scaleTimingConfig));
73+
74+
if (variant === 'highlight') return;
75+
4676
rippleProgress.set(0);
4777
pressedCenterX.set(event.x);
4878
pressedCenterY.set(event.y);
49-
isPressed.set(true);
50-
scale.set(withTiming(1, { duration: 250 }));
5179
if (rippleProgress.get() === 0) {
5280
rippleProgress.set(withTiming(1, { duration: 250 }));
5381
}
5482
})
5583
.onFinalize(() => {
5684
isPressed.set(false);
57-
scale.set(withTiming(0, { duration: 250 }));
85+
scale.set(withTiming(0, scaleTimingConfig));
86+
87+
if (variant === 'ripple') return;
88+
5889
rippleProgress.set(withTiming(2, { duration: 400 }));
5990
});
6091

92+
const styleTransform = getStyleTransform(style);
93+
6194
const rContainerStyle = useAnimatedStyle(() => {
6295
const baseWidth = 300;
6396
const coefficient =
6497
containerWidth.get() > 0 ? baseWidth / containerWidth.get() : 1;
65-
const adjustedScaleValue = 1 - (1 - 0.99) * coefficient;
98+
const adjustedScaleValue = 1 - (1 - scaleValue) * coefficient;
99+
100+
if (isAnimationDisabled) {
101+
return {
102+
transform: [
103+
{
104+
scale: 1,
105+
},
106+
...styleTransform,
107+
],
108+
};
109+
}
66110

67111
return {
68112
transform: [
69113
{
70114
scale: interpolate(scale.get(), [0, 1], [1, adjustedScaleValue]),
71115
},
116+
...styleTransform,
72117
],
73118
};
74119
});
@@ -92,40 +137,36 @@ export function usePressableFeedbackRootAnimation() {
92137
* Handles opacity and background color animations for the highlight effect
93138
*/
94139
export function usePressableFeedbackHighlightAnimation(options: {
95-
animation: PressableFeedbackHighlightAnimation | undefined;
140+
animation: PressableFeedbackHighlightRootAnimation | undefined;
96141
}) {
97142
const { animation } = options;
98143

99144
const { theme } = useUniwind();
100-
const themeColorBackground = useThemeColor('background');
101145

102146
const { isPressed } = usePressableFeedbackAnimation();
103147

104148
const { animationConfig, isAnimationDisabled } = getAnimationState(animation);
105149

150+
// Background color
151+
const defaultColor = theme === 'dark' ? 'white' : 'gray';
152+
153+
const backgroundColor = getAnimationValueProperty({
154+
animationValue: animationConfig?.highlight?.backgroundColor,
155+
property: 'value',
156+
defaultValue: defaultColor,
157+
});
158+
106159
// Opacity animation
107160
const opacityValue = getAnimationValueProperty({
108-
animationValue: animationConfig?.opacity,
161+
animationValue: animationConfig?.highlight?.opacity,
109162
property: 'value',
110-
defaultValue: [0, 0.1] as [number, number],
163+
defaultValue: [0, 0.05] as [number, number],
111164
});
112165

113166
const opacityTimingConfig = getAnimationValueMergedConfig({
114-
animationValue: animationConfig?.opacity,
167+
animationValue: animationConfig?.highlight?.opacity,
115168
property: 'timingConfig',
116-
defaultValue: { duration: 200, easing: Easing.inOut(Easing.quad) },
117-
});
118-
119-
// Background color
120-
const defaultColor =
121-
theme === 'dark'
122-
? colorKit.brighten(themeColorBackground, 0.05).hex()
123-
: colorKit.darken(themeColorBackground, 0.05).hex();
124-
125-
const backgroundColor = getAnimationValueProperty({
126-
animationValue: animationConfig?.backgroundColor,
127-
property: 'value',
128-
defaultValue: defaultColor,
169+
defaultValue: { duration: 200 },
129170
});
130171

131172
const rContainerStyle = useAnimatedStyle(() => {

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

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { forwardRef, useCallback, useMemo, type FC } from 'react';
2-
import { Pressable, StyleSheet, type LayoutChangeEvent } from 'react-native';
2+
import {
3+
Pressable,
4+
StyleSheet,
5+
type LayoutChangeEvent,
6+
type ViewStyle,
7+
} from 'react-native';
38

49
import Animated from 'react-native-reanimated';
510
import Svg, { Defs, RadialGradient, Rect, Stop } from 'react-native-svg';
@@ -15,7 +20,11 @@ import {
1520
} from './pressable-feedback.animation';
1621
import { DISPLAY_NAME } from './pressable-feedback.constants';
1722
import pressableFeedbackStyles from './pressable-feedback.styles';
18-
import type { PressableFeedbackProps } from './pressable-feedback.types';
23+
import type {
24+
PressableFeedbackHighlightRootAnimation,
25+
PressableFeedbackProps,
26+
PressableFeedbackRippleAnimation,
27+
} from './pressable-feedback.types';
1928

2029
const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
2130

@@ -24,9 +33,11 @@ const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
2433
const PressableFeedback = forwardRef<PressableRef, PressableFeedbackProps>(
2534
(props, ref) => {
2635
const {
36+
variant = 'highlight',
2737
isDisabled = false,
2838
className,
2939
style,
40+
animation,
3041
children,
3142
onLayout,
3243
...restProps
@@ -43,7 +54,11 @@ const PressableFeedback = forwardRef<PressableRef, PressableFeedbackProps>(
4354
rippleProgress,
4455
gesture,
4556
rContainerStyle,
46-
} = usePressableFeedbackRootAnimation();
57+
} = usePressableFeedbackRootAnimation({
58+
variant,
59+
animation,
60+
style: style as ViewStyle | undefined,
61+
});
4762

4863
const handleLayout = useCallback(
4964
(event: LayoutChangeEvent) => {
@@ -85,8 +100,12 @@ const PressableFeedback = forwardRef<PressableRef, PressableFeedbackProps>(
85100
onLayout={handleLayout}
86101
{...restProps}
87102
>
103+
{variant === 'highlight' && (
104+
<PressableFeedbackHighlight
105+
animation={animation as PressableFeedbackHighlightRootAnimation}
106+
/>
107+
)}
88108
{children}
89-
<PressableFeedbackRipple />
90109
</AnimatedPressable>
91110
</GestureDetector>
92111
</PressableFeedbackAnimationProvider>
@@ -97,7 +116,7 @@ const PressableFeedback = forwardRef<PressableRef, PressableFeedbackProps>(
97116
// --------------------------------------------------
98117

99118
const PressableFeedbackHighlight: FC<{
100-
animation: PressableFeedbackProps['animation'];
119+
animation: PressableFeedbackHighlightRootAnimation | undefined;
101120
}> = ({ animation }) => {
102121
const { rContainerStyle } = usePressableFeedbackHighlightAnimation({
103122
animation,
@@ -113,7 +132,9 @@ const PressableFeedbackHighlight: FC<{
113132

114133
// --------------------------------------------------
115134

116-
const PressableFeedbackRipple: FC<{}> = () => {
135+
const PressableFeedbackRipple: FC<{
136+
animation: PressableFeedbackRippleAnimation;
137+
}> = () => {
117138
const { rContainerStyle } = usePressableFeedbackRippleAnimation();
118139

119140
const themeColorSurfaceSecondary = useThemeColor('on-surface-hover');

0 commit comments

Comments
 (0)