UNPKG

rn-bounceable

Version:

Native bounceable effect for any React Native component. Built with Reanimated. Compatible with Expo.

121 lines (120 loc) 3.15 kB
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, ); };