Last active
August 23, 2024 10:06
-
-
Save cjmling/84e271ea61d89cd7ef429a4b95f57b9e to your computer and use it in GitHub Desktop.
react-native animated vertical select
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { useRef, useState } from 'react'; | |
import { View, StyleSheet, ScrollView } from 'react-native'; | |
import Animated, { | |
useSharedValue, | |
interpolate, | |
useAnimatedStyle, | |
Extrapolation, | |
} from 'react-native-reanimated'; | |
// Changeable | |
const FONT_SIZE = 20; | |
const SCROLL_HEIGHT = 400; | |
const ITEM_BACKGROUND = "#FFF"; | |
const ITEM_WIDTH = 100; | |
// Should not change | |
// 5 Because we want to only show 5 item in the scroll list. Should not change this value as it is hard coded and used in interpolate function. But one day if we want to change then it should be odd number. | |
const ITEM_HEIGHT = SCROLL_HEIGHT / 5; // | |
const TOTAL_ITEM_PER_SCROLL_VIEW = Math.round(SCROLL_HEIGHT / ITEM_HEIGHT); | |
const MIDDLE_ITEM_OF_SCROLL_VIEW = Math.round(TOTAL_ITEM_PER_SCROLL_VIEW / 2); | |
export const VerticalSelectScrollView = ({onChange, datas}: {onChange:(value: any) => void, datas: {id: string, title: string}[]}) => { | |
const contentOffset = useSharedValue(0); | |
const firstFocusedItemInScroll = useSharedValue(0); | |
const scrollViewRef = useRef<any>(null); | |
// Add 2 items in the start and end because they aren't selectable in scroll | |
const EXTRA_OFFSET_ITEMS_PER_SIDE = 2; | |
const EXTENDED_ITEMS = [...[{id: '-2', title: ''}, {id: '-1', title: ''}],...datas, ...[{id: '-3', title: ''}, {id: '-4', title: ''}]] | |
const Item = ({ | |
item, | |
index, | |
}: { | |
item: { id: string; title: string }; | |
index: number; | |
}) => { | |
const viewStyle = useAnimatedStyle(() => { | |
const inputRange = [index - 2, index - 1, index, index + 1, index + 2]; | |
let outputRange = [0.2, 0.5, 1, 0.5, 0.2]; | |
const activeItem = | |
firstFocusedItemInScroll.value + MIDDLE_ITEM_OF_SCROLL_VIEW - 1; // ActiveItem are the middleone in visible view. -1 Because MIDDLE_ITEM_OF_SCROLL_VIEW value get rounded up above. | |
const opacity = interpolate( | |
activeItem, | |
inputRange, | |
outputRange, | |
Extrapolation.CLAMP | |
); | |
return { | |
opacity, | |
}; | |
}); | |
const fontStyle = useAnimatedStyle(() => { | |
const inputRange = [index - 2, index - 1, index, index + 1, index + 2]; | |
const activeItem = | |
firstFocusedItemInScroll.value + MIDDLE_ITEM_OF_SCROLL_VIEW - 1; // ActiveItem are the middleone in visible view | |
const fontSize = interpolate( | |
activeItem, | |
inputRange, | |
[ | |
0.8 * FONT_SIZE, | |
0.9 * FONT_SIZE, | |
1 * FONT_SIZE, | |
0.9 * FONT_SIZE, | |
0.8 * FONT_SIZE, | |
], | |
Extrapolation.CLAMP | |
); | |
return { | |
fontSize, | |
}; | |
}); | |
return ( | |
<Animated.View | |
style={[ | |
{ | |
backgroundColor: ITEM_BACKGROUND, | |
height: ITEM_HEIGHT, | |
width: ITEM_WIDTH, | |
flex: 1, | |
justifyContent: 'center', | |
alignItems: 'center', | |
}, | |
viewStyle, | |
]}> | |
<Animated.Text style={[{ fontSize: 20 }, fontStyle]}> | |
{item.title} | |
</Animated.Text> | |
</Animated.View> | |
); | |
}; | |
const onMomentumScrollEnd = () => { | |
const distanceFromTop = contentOffset.value; | |
const itemFromTop = Math.round(distanceFromTop / ITEM_HEIGHT); | |
onChange(EXTENDED_ITEMS[itemFromTop + EXTRA_OFFSET_ITEMS_PER_SIDE]); | |
}; | |
return ( | |
<View style={styles.container}> | |
<ScrollView | |
onScroll={(event) => { | |
contentOffset.value = event.nativeEvent.contentOffset.y; | |
const scrollDistanceFromTop = Math.abs(contentOffset.value); | |
// Item distance from top | |
firstFocusedItemInScroll.value = scrollDistanceFromTop / ITEM_HEIGHT; //Should not round number | |
}} | |
decelerationRate={'normal'} | |
snapToOffsets={EXTENDED_ITEMS.map((ITEM, index) => ITEM_HEIGHT * index)} | |
ref={scrollViewRef} | |
onMomentumScrollEnd={onMomentumScrollEnd} | |
showsVerticalScrollIndicator={false} | |
snapToAlignment="center"> | |
{EXTENDED_ITEMS.map((ITEM, index) => ( | |
<Item item={ITEM} index={index} /> | |
))} | |
</ScrollView> | |
</View> | |
); | |
}; | |
const styles = StyleSheet.create({ | |
container: { | |
alignItems: 'center', | |
justifyContent: 'center', | |
height: SCROLL_HEIGHT, | |
backgroundColor: '#EEE', | |
}, | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Expo Snacks: https://snack.expo.dev/@cjmling/vertical-select-option-using-scroll-view