UNPKG

react-native-sortables

Version:

Powerful Sortable Components for Flexible Content Reordering in React Native

196 lines (173 loc) 6.24 kB
import { useCallback, useRef } from 'react'; import { runOnUI } from 'react-native-reanimated'; import { useStableCallback } from '../../hooks'; import { setAnimatedTimeout, useAnimatedDebounce, useMutableValue } from '../../integrations/reanimated'; import type { Dimensions, MeasurementsContextType } from '../../types'; import { areValuesDifferent, resolveDimension } from '../../utils'; import { createProvider } from '../utils'; import { useCommonValuesContext } from './CommonValuesProvider'; import { useMultiZoneContext } from './MultiZoneProvider'; type MeasurementsProviderProps = { itemsCount: number; }; const { MeasurementsProvider, useMeasurementsContext } = createProvider( 'Measurements' )<MeasurementsProviderProps, MeasurementsContextType>(({ itemsCount }) => { const { activeItemDimensions, activeItemKey, containerHeight, containerWidth, controlledContainerDimensions, controlledItemDimensions, itemHeights, itemWidths, usesAbsoluteLayout } = useCommonValuesContext(); const { activeItemDimensions: multiZoneActiveItemDimensions } = useMultiZoneContext() ?? {}; const measuredItemsCount = useMutableValue(0); const previousItemDimensionsRef = useRef<Record<string, Dimensions>>({}); const debounce = useAnimatedDebounce(); const handleItemMeasurement = useStableCallback( (key: string, dimensions: Dimensions) => { const prevDimensions = previousItemDimensionsRef.current[key]; const { height: isHeightControlled, width: isWidthControlled } = controlledItemDimensions; if (isWidthControlled && isHeightControlled) { return; } const changedDimensions: Partial<Dimensions> = {}; if ( !isWidthControlled && areValuesDifferent(prevDimensions?.width, dimensions.width, 1) ) { changedDimensions.width = dimensions.width; } if ( !isHeightControlled && areValuesDifferent(prevDimensions?.height, dimensions.height, 1) ) { changedDimensions.height = dimensions.height; } if (!Object.keys(changedDimensions).length) { return; } previousItemDimensionsRef.current[key] = dimensions; runOnUI(() => { const resolvedWidth = resolveDimension(itemWidths.value, key); const resolvedHeight = resolveDimension(itemHeights.value, key); const isNewItem = resolvedWidth === null || resolvedHeight === null; if (isNewItem) { measuredItemsCount.value += 1; } if (itemWidths.value && typeof itemWidths.value === 'object') { itemWidths.value[key] = dimensions.width; } if (itemHeights.value && typeof itemHeights.value === 'object') { itemHeights.value[key] = dimensions.height; } if (activeItemKey.value === key) { activeItemDimensions.value = dimensions; if (multiZoneActiveItemDimensions) { multiZoneActiveItemDimensions.value = dimensions; } } // Update the array of item dimensions only after all items have been // measured to reduce the number of times animated reactions are triggered if (measuredItemsCount.value === itemsCount) { const updateDimensions = () => { if (!isWidthControlled) itemWidths.modify(); if (!isHeightControlled) itemHeights.modify(); }; if (isNewItem) { // If measurements were triggered because of adding new items and all new // items have been measured, update dimensions immediately to avoid // unnecessary delays updateDimensions(); } else { // Otherwise, debounce the update if the number of items is not changed // to reduce the number of updates if dimensions of items are changed // many times within a short period of time debounce(updateDimensions, 100); } } })(); } ); const removeItemMeasurements = useCallback( (key: string) => { delete previousItemDimensionsRef.current[key]; const { height: isHeightControlled, width: isWidthControlled } = controlledItemDimensions; if (isWidthControlled && isHeightControlled) { return; } runOnUI(() => { if (itemWidths.value && typeof itemWidths.value === 'object') { delete itemWidths.value[key]; } if (itemHeights.value && typeof itemHeights.value === 'object') { delete itemHeights.value[key]; } measuredItemsCount.value -= 1; })(); }, [controlledItemDimensions, itemHeights, itemWidths, measuredItemsCount] ); const handleContainerMeasurement = useCallback( (width: number, height: number) => { 'worklet'; if (!controlledContainerDimensions.width) { containerWidth.value = width; } if (!controlledContainerDimensions.height) { containerHeight.value = height; } }, [controlledContainerDimensions, containerHeight, containerWidth] ); const applyControlledContainerDimensions = useCallback( (dimensions: Partial<Dimensions>) => { 'worklet'; if ( controlledContainerDimensions.width && dimensions.width !== undefined ) { containerWidth.value = dimensions.width; } if ( controlledContainerDimensions.height && dimensions.height !== undefined ) { containerHeight.value = dimensions.height; } if (!usesAbsoluteLayout.value) { // Add timeout for safety, to prevent too many layout recalculations // in a short period of time (this may cause issues on low end devices) setAnimatedTimeout(() => { usesAbsoluteLayout.value = true; }, 100); } }, [ containerHeight, containerWidth, controlledContainerDimensions, usesAbsoluteLayout ] ); return { value: { applyControlledContainerDimensions, handleContainerMeasurement, handleItemMeasurement, removeItemMeasurements } }; }); export { MeasurementsProvider, useMeasurementsContext };