UNPKG

react-native-multiswitch-controller

Version:
157 lines (146 loc) 5.96 kB
"use strict"; import { useCallback, useEffect, useRef, useState, useMemo, useImperativeHandle } from 'react'; import { Easing, runOnJS, useAnimatedScrollHandler, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; /** * Helps with animation of SegmentedControl and Tabs. * To avoid fps drops while animating, SegmentedControl/Tabs needs to be animated first, and after that the content may be refreshed. * */ function useControlListState(props, ref) { const { options, defaultOption, onPressCallback, optionGap, optionPadding } = props; const optionsRef = useRef(options); const [activeOption, setActiveOption] = useState(defaultOption); const [forcedOption, setForcedOption] = useState(defaultOption); const [optionLayouts, setOptionLayouts] = useState({}); const optionsJSON = useMemo(() => JSON.stringify(options), [options]); const controlListRef = useRef(null); const animatedTranslateX = useSharedValue(0); const animatedWidth = useSharedValue(0); const animatedScrollX = useSharedValue(0); const animatedActiveOptionIndex = useSharedValue(null); const isFirstRender = useRef(true); useImperativeHandle(ref, () => ({ setForcedOption, activeOption }), [activeOption]); const animatedActiveOptionStyle = useAnimatedStyle(() => ({ transform: [{ translateX: animatedTranslateX.value - animatedScrollX.value }], width: animatedWidth.value })); const scrollHandler = useAnimatedScrollHandler({ onScroll: ({ contentOffset: { x } }) => { animatedScrollX.value = x; } }); const onLayoutOptionItem = useCallback(({ nativeEvent }, index) => { const option = options?.[index]; if (!option) return; setOptionLayouts(previous => ({ ...previous, [index]: { width: nativeEvent?.layout.width, label: option.label } })); }, [options]); const moveActiveOption = useCallback(({ activeOptionValue, afterAnimationCallback, initialAnimationCallback }) => { const activeOptionIndex = options.findIndex(option => option.value === activeOptionValue); const activeOptionLayout = optionLayouts[activeOptionIndex]; if (!activeOptionLayout) return; const sumPreviousWidths = Object.values(optionLayouts).slice(0, activeOptionIndex).reduce((totalWidth, currentWidth) => totalWidth + currentWidth.width, 0); const sumPreviousPaddingAndGaps = optionPadding + optionGap * activeOptionIndex; const translateValue = sumPreviousWidths + sumPreviousPaddingAndGaps; const textWidthValue = activeOptionLayout.width - optionPadding * 2; // Allows to animate based on index e.g. text color inside list items. animatedActiveOptionIndex.value = activeOptionIndex; // Instant animation only for the first time SegmentedControl/Tabs is rendered. if (!!initialAnimationCallback && isFirstRender.current) { animatedWidth.value = textWidthValue; animatedTranslateX.value = translateValue; afterAnimationCallback?.(activeOptionValue); initialAnimationCallback(); } else { animatedWidth.value = withTiming(textWidthValue, { duration: 200, easing: Easing.inOut(Easing.quad) }); animatedTranslateX.value = withTiming(translateValue, { duration: 200, easing: Easing.inOut(Easing.quad) }, () => { if (afterAnimationCallback) runOnJS(afterAnimationCallback)(activeOptionValue); }); } controlListRef.current?.scrollToIndex({ index: activeOptionIndex, animated: true, viewPosition: 0.5 }); }, [optionLayouts, animatedWidth, animatedTranslateX, animatedActiveOptionIndex, optionGap, optionPadding, options]); // Fires after animation is finished. const onChange = useCallback((value, callback) => { setActiveOption(value); setForcedOption(null); callback?.(); onPressCallback?.(value); }, [onPressCallback]); // Allows to call onChange after animation is finished. const onAnimationFinish = useCallback((newValue, initialAnimationCallback) => { moveActiveOption({ activeOptionValue: newValue, afterAnimationCallback: onChange, initialAnimationCallback }); }, [moveActiveOption, onChange]); // Fires after initial animation is finished to allow future setting of initial value, which will force animation. const removeInitialOption = useCallback(() => setForcedOption(null), []); // First time animation, based on forcedOption after layout is calculated. useEffect(() => { if (forcedOption && Object.values(optionLayouts).length === options.length) { removeInitialOption(); onAnimationFinish(forcedOption, () => isFirstRender.current = false); } }, [forcedOption, onAnimationFinish, removeInitialOption, optionLayouts, options]); // Handle edge case when option label is changed e.g. in case of language change. // optionLayouts is calculated synchronously for each option. We need to start animation only when all of labels are updated. useEffect(() => { const optionLayoutsValues = Object.values(optionLayouts); if (optionLayoutsValues.length !== options.length) return; const optionsPropDeepEqual = JSON.stringify(optionsRef.current) === optionsJSON; if (optionsPropDeepEqual) return; const isAllLabelsUpdated = options.every((option, index) => optionLayoutsValues[index]?.label === option.label); if (!isAllLabelsUpdated) return; optionsRef.current = options; onAnimationFinish(activeOption); // eslint-disable-next-line react-hooks/exhaustive-deps }, [optionsJSON, optionLayouts]); return { options, activeOption, animatedActiveOptionIndex, animatedActiveOptionStyle, scrollHandler, controlListRef, onLayoutOptionItem, onAnimationFinish }; } export default useControlListState; //# sourceMappingURL=useControlListState.js.map