UNPKG

@rakeshv1108/react-native-infinite-wheel-picker

Version:

React Native component to provide infinite wheel picker selection

220 lines (205 loc) 6.55 kB
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Animated, type FlatList, type NativeScrollEvent, type NativeSyntheticEvent, } from 'react-native'; import type { WheelPickerProps } from '..'; import useThrowPropsError from './useThrowPropsError'; /** * Custom hook to handle the wheel picker logic * @param onChangeValue - Function to handle the value change * @param selectedIndex - The index of the selected value * @param data - The data to be displayed in the wheel picker * @param elementHeight - The height of each element in the wheel picker * @param restElements - The number of elements to be displayed above and below the selected element * @param decelerationRate - The deceleration rate of the scroll view * @param loopCount - The number of times the data should be looped * @returns - The necessary props for the wheel picker */ const useWheelPicker = ({ onChangeValue, selectedIndex, data, elementHeight, restElements, decelerationRate, loopCount, infiniteScroll, initialSelectedIndex, }: Required< Pick< WheelPickerProps, | 'loopCount' | 'onChangeValue' | 'selectedIndex' | 'data' | 'restElements' | 'decelerationRate' | 'elementHeight' | 'infiniteScroll' | 'initialSelectedIndex' > >) => { /** * Loop the data to make it infinite * The loopCount is multiplied by 2 to ensure that the data is enough to be looped * The loopedData is memoized to prevent re-rendering */ let loopCountMax = !infiniteScroll ? 1 : loopCount * 2; const loopedData = useMemo(() => { let looped: Array<string | number> = []; for (let i = 0; i < loopCountMax; i++) { looped = [...looped, ...data]; } return looped; }, [loopCountMax, data]); const [arrayData, setArrayData] = useState<Array<string | number>>([ ...loopedData, ]); const flatListRef = useRef<FlatList<string | null>>(null); const [scrollY] = useState(new Animated.Value(0)); /** * Verify if the props are valid or not * If not, throw an error */ useThrowPropsError({ selectedIndex, arrayData, restElements, loopCount, decelerationRate, data, elementHeight, initialSelectedIndex, }); /** * Calculate the height of the container */ const containerHeight = (1 + restElements * 2) * elementHeight; /** * Adding null values to the beginning and end of the data to make the list circular */ const arrayElements = useMemo(() => { const array: (string | null | number)[] = [...arrayData]; for (let i = 0; i < restElements; i++) { array.unshift(null); array.push(null); } return array; }, [restElements, arrayData]); /** * Calculate the offsets for each item in the list * The offsets are memoized to prevent re-rendering */ const offsets = useMemo( () => [...Array(arrayElements.length)].map((_x, i) => i * elementHeight), [arrayElements, elementHeight] ); /** * Calculate the current scroll index */ const currentScrollIndex = useMemo(() => { const scrollIndex = Animated.add( Animated.divide(scrollY, elementHeight), restElements ); return scrollIndex; }, [restElements, scrollY, elementHeight]); let timer: NodeJS.Timeout | null = null; /** * Handle the momentum scroll end event * This function is called when the user stops scrolling * It calculates the selected index based on the current scroll position * and calls the onChangeValue function to update the selected value * @param event - NativeSyntheticEvent<NativeScrollEvent> */ const handleMomentumScrollEnd = ( event: NativeSyntheticEvent<NativeScrollEvent> ) => { const offsetY = Math.min( elementHeight * (arrayData.length - 1), Math.max(event.nativeEvent.contentOffset.y, 0) ); let index = Math.floor(Math.floor(offsetY) / elementHeight); const last = Math.floor(offsetY % elementHeight); if (last > elementHeight / 2) index++; if (index !== selectedIndex) { const value = arrayData[index]; if (value) { if (timer) clearTimeout(timer); timer = setTimeout(() => { const originalIndex = data.findIndex( item => item === arrayData[index] ); onChangeValue(originalIndex, value.toString()); }, 100); } } }; /** * Handle the scroll event * This function is called when the user scrolls the wheel picker * It checks if the user is near the end of the list * If so, it adds more data to the list */ const onScrollListener = useCallback( (event: NativeSyntheticEvent<NativeScrollEvent>) => { const { contentOffset, contentSize, layoutMeasurement } = event.nativeEvent; const detectFromItem = (data.length + 2) * elementHeight; // 2 is the number of extra elements const isNearToEnd = layoutMeasurement.height + contentOffset.y + elementHeight >= contentSize.height - detectFromItem; if (isNearToEnd && infiniteScroll) { setArrayData([...arrayData, 1]); } }, [arrayData, data, elementHeight, infiniteScroll] ); /** * Calculate the initial scroll index * The initial scroll index is the index of the selected value * Here 0 is initially selected 0 index if infiniteScroll is false */ const initialScrollIndex = useMemo(() => { return !infiniteScroll ? initialSelectedIndex ?? 0 : arrayData?.length / 2 + initialSelectedIndex; }, [arrayData, initialSelectedIndex, infiniteScroll]); /** * Set the initial selected value */ useEffect(() => { let count = 0; if (count === 0) { const infiniteDataIndex = data.findIndex( item => item === arrayData[initialScrollIndex] ); const originalIndex = !infiniteScroll ? selectedIndex ?? 0 : infiniteDataIndex; const originalValue = !infiniteScroll ? data[initialSelectedIndex ?? 0]?.toString() : arrayData[initialScrollIndex]?.toString(); onChangeValue(originalIndex ?? 0, originalValue ?? ''); count++; } // eslint-disable-next-line react-hooks/exhaustive-deps }, [initialScrollIndex, infiniteScroll, initialSelectedIndex]); return { currentScrollIndex, containerHeight, flatListRef, arrayElements, scrollY, setArrayData, arrayData, handleMomentumScrollEnd, offsets, initialScrollIndex, onScrollListener, }; }; export default useWheelPicker;