Here is my approach:
at give time render 3 elements on the screen, 2 of them will be visible on screen and one will be rendered outside the screen, the third screen will be used to animate when the first element is panned, so that we don't see empty screen.
the entire project can be found here: GitHub Link
values:
const isPanning = useSharedValue(false);
const activeIndex = useSharedValue(0);
const posY = useSharedValue(0);
const scondPosY = useSharedValue(0);
const thirdPosY = useSharedValue(-windowHeight / 2);
create pan gesture handler by using the new gesture detector from gesture handler:
const handler = Gesture.Pan()
.onStart(() => {
isPanning.value = true;
})
.onChange((event) => {
posY.value = event.translationY;
})
.onEnd(() => {
isPanning.value = false;
if (posY.value > 50) {
posY.value = withTiming(windowHeight);
scondPosY.value = withTiming(windowHeight / 2);
activeIndex.value += 1;
thirdPosY.value = withTiming(0, {}, (finished) => {
if (finished) {
runOnJS(renderNextItems)(activeIndex.value);
}
});
} else {
posY.value = withTiming(0);
}
});
Note: above the index is changed only when animation is finished so that we can change the rendered items, before updating the state to render next set of elements reset all the animation values
file:
export const Screen = (props: Props) => {
const [itemsToRender, setItemsToRender] = useState<string[]>(
props.items.slice(0, 3)
);
// Layout stuff, gets copied from the LayoutHelper.getStyles style object
const size = useWindowDimensions();
const windowHeight = size.height;
const isPanning = useSharedValue(false);
const activeIndex = useSharedValue(0);
const posY = useSharedValue(0);
const scondPosY = useSharedValue(0);
const thirdPosY = useSharedValue(-windowHeight / 2);
const renderNextItems = useCallback((value: number) => {
posY.value = 0;
scondPosY.value = 0;
thirdPosY.value = -windowHeight / 2;
setItemsToRender(props.items.slice(value, value + 3));
}, []);
const handler = Gesture.Pan()
.onStart(() => {
isPanning.value = true;
})
.onChange((event) => {
posY.value = event.translationY;
})
.onEnd(() => {
isPanning.value = false;
if (posY.value > 50) {
posY.value = withTiming(windowHeight);
scondPosY.value = withTiming(windowHeight / 2);
activeIndex.value += 1;
thirdPosY.value = withTiming(0, {}, (finished) => {
if (finished) {
runOnJS(renderNextItems)(activeIndex.value);
}
});
} else {
posY.value = withTiming(0);
}
});
const currentItemTransform = useAnimatedStyle(
() => ({
transform: [
{
translateY: windowHeight / 2 + posY.value,
},
],
}),
[itemsToRender.join(", ")]
);
const nextItemTransform = useAnimatedStyle(
() => ({
transform: [
{
translateY: scondPosY.value,
},
],
}),
[itemsToRender.join(", ")]
);
const thirdTransform = useAnimatedStyle(
() => ({
transform: [
{
translateY: thirdPosY.value,
},
],
}),
[itemsToRender.join(", ")]
);
const itemStyles: any[] = [];
if (itemsToRender[0]) {
itemStyles.push([
styles.item,
{
transform: [
{
translateY: windowHeight / 2,
},
],
},
currentItemTransform,
]);
if (itemsToRender[1]) {
itemStyles.push([styles.item, nextItemTransform]);
}
if (itemsToRender[2]) {
itemStyles.push([styles.item, thirdTransform]);
}
}
return (
<View style={styles.container}>
<GestureDetector gesture={handler}>
<Animated.View style={styles.itemContainer}>
{(itemsToRender.length === 3
? [2, 1, 0]
: itemsToRender.length === 1
? [0]
: []
).map((i) => {
const style = itemStyles[i];
return (
<Animated.View key={itemsToRender[i]} style={style}>
<Text style={styles.text}>{itemsToRender[i]}</Text>
</Animated.View>
);
})}
</Animated.View>
</GestureDetector>
</View>
);
};
here is the example gif how it will work when there is load in js thread:
