Skip to content

Instantly share code, notes, and snippets.

@likern
Created August 25, 2020 12:24
Show Gist options
  • Save likern/857b5b092959c87d4922af7ddfae93a7 to your computer and use it in GitHub Desktop.
Save likern/857b5b092959c87d4922af7ddfae93a7 to your computer and use it in GitHub Desktop.
import React, { useCallback, useState } from 'react';
import Animated, {
measure,
cancelAnimation,
useAnimatedRef,
useSharedValue,
useDerivedValue,
withTiming,
Easing,
useAnimatedStyle,
interpolate,
Extrapolate,
useAnimatedGestureHandler
} from 'react-native-reanimated';
import { PanGestureHandler } from 'react-native-gesture-handler';
import { ViewStyleProp } from '../../../../types';
import { PropsWithChildren } from 'react';
import { StyleSheet } from 'react-native';
import { Icon } from '../..';
const DEFAULTS = Object.freeze({
SWIPE_THRESHOLD: 0.5,
ACTIVATION_THRESHOLD: 8,
ANIMATION_DURATION: 0.05,
BORDER_RADIUS: 16,
LEFT_ICON_COLOR: 'rgba(255, 255, 255, 1)',
RIGHT_ICON_COLOR: 'rgba(255, 255, 255, 1)'
});
type Callback = () => void;
export type SwipableRowProps = {
threshold?: number;
onSwipeLeft?: Callback;
onSwipeRight?: Callback;
} & ViewStyleProp;
export const SwipableRow = ({
threshold = DEFAULTS.SWIPE_THRESHOLD,
children,
style,
onSwipeLeft: _onSwipeLeft,
onSwipeRight: _onSwipeRight
}: PropsWithChildren<SwipableRowProps>) => {
const viewRef = useAnimatedRef();
const rowHeight = useSharedValue<undefined | number>(undefined);
const positionX = useSharedValue(0);
const swipeDirection = useSharedValue<'left' | 'right' | undefined>(
undefined
);
const _setInternalState = useState({})[1];
const forceRerender = useCallback(() => {
_setInternalState({});
}, [_setInternalState]);
const callbackWrapper = useCallback(
(callback: Callback) => {
forceRerender();
callback();
},
[forceRerender]
);
const onSwipeLeft = useCallback(() => {
_onSwipeLeft && callbackWrapper(_onSwipeLeft);
}, [_onSwipeLeft, callbackWrapper]);
const onSwipeRight = useCallback(() => {
_onSwipeRight && callbackWrapper(_onSwipeRight);
}, [_onSwipeRight, callbackWrapper]);
const leftIconOpacity = useDerivedValue(() => {
return positionX.value > 0 ? 1 : 0;
});
const rightIconOpacity = useDerivedValue(() => {
return positionX.value < 0 ? 1 : 0;
});
const leftBorderRadius = useDerivedValue(() => {
return interpolate(
positionX.value,
[0, DEFAULTS.ACTIVATION_THRESHOLD],
[0, DEFAULTS.BORDER_RADIUS],
Extrapolate.CLAMP
);
});
const callback = useCallback(() => {
console.log(`callback: start`);
setImmediate(() => {
console.log(`callback: should be called immediately`);
});
console.log(`callback: end`);
}, []);
const magic = () => {
console.log(`magic`);
let num = 0;
const w = setInterval(() => {
if (num === 1) {
clearInterval(w);
}
num++;
}, 0);
};
const rightBorderRadius = useDerivedValue(() => {
return interpolate(
positionX.value,
[-DEFAULTS.ACTIVATION_THRESHOLD, 0],
[DEFAULTS.BORDER_RADIUS, 0],
Extrapolate.CLAMP
);
});
const gestureHandler = useAnimatedGestureHandler({
onStart: (_, ctx) => {
cancelAnimation(positionX);
ctx.position = positionX.value;
},
onActive: (event, ctx) => {
let translation = event.translationX;
const useThreshold =
Math.abs(ctx.position + translation) < DEFAULTS.ACTIVATION_THRESHOLD;
if (!ctx.swipeStarted) {
if (useThreshold) {
translation = 0;
} else {
ctx.swipeStarted = true;
}
}
positionX.value = ctx.position + translation;
},
onEnd: (event, ctx) => {
const { width, height } = measure(viewRef);
rowHeight.value = height;
const moveDirection = Math.sign(event.velocityX);
const impulse = event.velocityX * DEFAULTS.ANIMATION_DURATION;
const possibleProgress = interpolate(
positionX.value + impulse,
[-width, width],
[-1, 1],
Extrapolate.CLAMP
);
const swipeThresholdPassed = Math.abs(possibleProgress) > threshold;
let toValue = 0;
if (swipeThresholdPassed) {
if (moveDirection === 0) {
toValue = Math.sign(possibleProgress) * width;
} else {
if (positionX.value > 0 && moveDirection > 0) {
toValue = moveDirection * width;
} else if (positionX.value < 0 && moveDirection < 0) {
toValue = moveDirection * width;
}
}
}
const isRunAnimation = positionX.value !== toValue;
positionX.value = withTiming(
toValue,
{
duration: isRunAnimation ? 250 : 0,
easing: Easing.out(Easing.ease)
},
(isFinished) => {
if (isFinished) {
ctx.swipeStarted = false;
callback();
magic();
// if (Math.abs(toValue) === width) {
// rowHeight.value = 0;
// }
// const sign = Math.sign(toValue);
// if (sign === 1) {
// swipeDirection.value = 'right';
// } else if (sign === -1) {
// swipeDirection.value = 'left';
// }
}
}
);
}
});
const foregroundStyle = useAnimatedStyle(() => {
return {
borderTopLeftRadius: leftBorderRadius.value,
borderBottomLeftRadius: leftBorderRadius.value,
borderTopRightRadius: rightBorderRadius.value,
borderBottomRightRadius: rightBorderRadius.value,
transform: [{ translateX: positionX.value }]
};
});
const backgroundStyle = useAnimatedStyle(() => {
return {
backgroundColor:
positionX.value === 0
? 'transparent'
: positionX.value > 0
? 'rgba(45, 154, 252, 0.5)'
: 'rgba(252, 119, 110, 1)'
};
});
const containerStyle = useAnimatedStyle(() => {
return {
height:
rowHeight.value !== undefined
? withTiming(
rowHeight.value,
{
duration: 250,
easing: Easing.out(Easing.ease)
},
(isFinished) => {
if (isFinished) {
if (swipeDirection.value === 'left') {
onSwipeLeft();
} else if (swipeDirection.value === 'right') {
onSwipeRight();
}
}
}
)
: undefined
};
});
const leftIconStyle = useAnimatedStyle(() => {
return {
opacity: leftIconOpacity.value
};
});
const rightIconStyle = useAnimatedStyle(() => {
return {
opacity: rightIconOpacity.value
};
});
return (
<Animated.View style={[styles.container, containerStyle]}>
<Animated.View style={[styles.background, backgroundStyle]}>
<Animated.View style={[styles.leftIcon, leftIconStyle]}>
<Icon iconName='check' iconColor={DEFAULTS.LEFT_ICON_COLOR} />
</Animated.View>
<Animated.View style={[styles.rightIcon, rightIconStyle]}>
<Icon iconName='trash' iconColor={DEFAULTS.RIGHT_ICON_COLOR} />
</Animated.View>
</Animated.View>
<PanGestureHandler onGestureEvent={gestureHandler}>
<Animated.View
ref={viewRef}
style={[style, styles.foreground, foregroundStyle]}
>
{children}
</Animated.View>
</PanGestureHandler>
</Animated.View>
);
};
const styles = StyleSheet.create({
container: { flexDirection: 'row' },
background: {
...StyleSheet.absoluteFillObject,
flexDirection: 'row',
alignItems: 'center',
overflow: 'hidden'
},
foreground: {
width: '100%',
overflow: 'hidden'
},
leftIcon: { position: 'absolute', left: 24 },
rightIcon: { position: 'absolute', right: 24 }
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment