react-native-sortables
Version:
Powerful Sortable Components for Flexible Content Reordering in React Native
250 lines (219 loc) • 7.67 kB
text/typescript
import { useCallback, useRef } from 'react';
import type { SharedValue } from 'react-native-reanimated';
import { runOnUI } from 'react-native-reanimated';
import { useStableCallback } from '../../hooks';
import {
setAnimatedTimeout,
useAnimatedDebounce,
useMutableValue
} from '../../integrations/reanimated';
import type {
Dimension,
Dimensions,
ItemSizes,
MeasurementsContextType
} from '../../types';
import { areValuesDifferent, resolveDimension } from '../../utils';
import { createProvider } from '../utils';
import { useCommonValuesContext } from './CommonValuesProvider';
import { useItemsContext } from './ItemsProvider';
import { useMultiZoneContext } from './MultiZoneProvider';
type StateContextType = {
measuredItemKeys: Set<string>;
queuedMeasurements: Map<string, Dimensions>;
};
type MeasurementsProviderProps = {
measureDebounceDelay: number;
};
const { MeasurementsProvider, useMeasurementsContext } = createProvider(
'Measurements'
)<MeasurementsProviderProps, MeasurementsContextType>(({
measureDebounceDelay
}) => {
const {
activeItemDimensions,
activeItemKey,
containerHeight,
containerWidth,
controlledContainerDimensions,
controlledItemDimensions,
itemHeights,
itemWidths,
usesAbsoluteLayout
} = useCommonValuesContext();
const { activeItemDimensions: multiZoneActiveItemDimensions } =
useMultiZoneContext() ?? {};
const { getKeys } = useItemsContext();
const context = useMutableValue<null | StateContextType>(null);
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;
const itemsCount = getKeys().length;
runOnUI(() => {
context.value ??= {
measuredItemKeys: new Set(),
queuedMeasurements: new Map()
};
const ctx = context.value;
const isNewItem =
!ctx.measuredItemKeys.has(key) &&
(resolveDimension(itemWidths.value, key) === null ||
resolveDimension(itemHeights.value, key) === null);
if (isNewItem) {
ctx.measuredItemKeys.add(key);
}
ctx.queuedMeasurements.set(key, dimensions);
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 (ctx.measuredItemKeys.size !== itemsCount) {
return;
}
const updateDimensions = () => {
const updateDimension = (
dimension: Dimension,
sizes: SharedValue<ItemSizes>
) => {
const newSizes = { ...(sizes.value as Record<string, number>) };
for (const [k, dims] of ctx.queuedMeasurements.entries()) {
newSizes[k] = dims[dimension];
}
sizes.value = newSizes;
};
if (!isWidthControlled) {
updateDimension('width', itemWidths);
}
if (!isHeightControlled) {
updateDimension('height', itemHeights);
}
ctx.queuedMeasurements.clear();
debounce.cancel();
};
if (isNewItem || ctx.queuedMeasurements.size === itemsCount) {
// Update dimensions immediately to avoid unnecessary delays when:
// - measurements were triggered because of adding new items and all new items have been measured
// - all sortable container items' dimensions have changed (e.g. when someone creates collapsible
// items which change their height when the user starts dragging them)
updateDimensions();
} else {
// In all other cases, debounce the update to reduce the number of
// updates when dimensions change many times within a short period of time
debounce.schedule(updateDimensions, measureDebounceDelay);
}
})();
}
);
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];
}
context.value?.measuredItemKeys.delete(key);
})();
},
[controlledItemDimensions, itemHeights, itemWidths, context]
);
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
]
);
const resetMeasurements = useCallback(() => {
previousItemDimensionsRef.current = {};
runOnUI(() => {
context.value = null;
if (typeof itemWidths.value === 'object') {
itemWidths.value = null;
}
if (typeof itemHeights.value === 'object') {
itemHeights.value = null;
}
})();
}, [itemHeights, itemWidths, context]);
return {
value: {
applyControlledContainerDimensions,
handleContainerMeasurement,
handleItemMeasurement,
removeItemMeasurements,
resetMeasurements
}
};
});
export { MeasurementsProvider, useMeasurementsContext };