rn-bounceable
Version:
Native bounceable effect for any React Native component. Built with Reanimated. Compatible with Expo.
121 lines (120 loc) • 3.15 kB
JavaScript
import * as React from 'react';
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
runOnJS,
} from 'react-native-reanimated';
import {TapGestureHandler, State} from 'react-native-gesture-handler';
import {useMemo} from 'react';
export const Bounceable = ({
children,
disabled = false,
noBounce = false,
onPress,
activeScale = 0.95,
springConfig = {
damping: 10,
mass: 1,
stiffness: 300,
},
contentContainerStyle,
delayLongPress = 800,
delayActiveScale = 0,
onLongPress,
immediatePress = true,
}) => {
const onLongPressTimeoutId = useSharedValue(null);
const scale = useSharedValue(1);
const isActive = useSharedValue(0);
const sz = useAnimatedStyle(() => {
'worklet';
return {
transform: [
{
scale: scale.value,
},
],
};
});
const beginScale = () => {
scale.value = withSpring(activeScale, springConfig);
};
const endScale = () => {
// clearing up
isActive.value = 0;
if (onLongPressTimeoutId.value !== null) {
clearTimeout(Number(onLongPressTimeoutId.value));
onLongPressTimeoutId.value = null;
}
scale.value = withSpring(1, springConfig);
};
const Children = useMemo(
() => React.createElement(Animated.View, {style: [contentContainerStyle, sz]}, children),
[contentContainerStyle, sz, children],
);
if (noBounce) {
return Children;
}
return React.createElement(
TapGestureHandler,
{
maxDurationMs: 99999999,
shouldCancelWhenOutside: true,
onHandlerStateChange: ({nativeEvent}) => {
if (disabled) {
return;
}
const {state} = nativeEvent;
if (state === State.BEGAN) {
isActive.value = 1;
// delaying scale beginning
if (delayActiveScale <= 0) {
beginScale();
} else {
setTimeout(() => {
if (isActive.value === 1) {
beginScale();
}
}, delayActiveScale);
}
// onLongPress
if (onLongPress) {
onLongPressTimeoutId.value = setTimeout(() => {
if (isActive.value === 1) {
endScale();
runOnJS(onLongPress)();
}
}, delayLongPress + delayActiveScale);
}
return;
}
if (state === State.END) {
if (onPress && isActive.value === 1) {
// mimicing bounce effect if delay active scale is set
if (delayActiveScale > 0) {
beginScale();
}
setTimeout(() => {
endScale();
if (!immediatePress) {
runOnJS(onPress)();
}
}, 50);
if (immediatePress) {
runOnJS(onPress)();
}
return;
}
endScale(); // ending scaling here just in case
return;
}
if (state === State.UNDETERMINED || state === State.FAILED || state === State.CANCELLED) {
endScale();
return;
}
},
},
Children,
);
};