Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 18 additions & 5 deletions packages/react-native-sortables/src/components/SortableGrid.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useMemo } from 'react';
import { useLayoutEffect, useMemo, useRef } from 'react';
import type { DimensionValue } from 'react-native';
import { StyleSheet } from 'react-native';
import type { SharedValue } from 'react-native-reanimated';
Expand All @@ -7,7 +7,11 @@ import { runOnUI, useAnimatedStyle } from 'react-native-reanimated';
import { DEFAULT_SORTABLE_GRID_PROPS, IS_WEB } from '../constants';
import { useDragEndHandler } from '../hooks';
import { useAnimatableValue } from '../integrations/reanimated';
import { GridProvider, useMeasurementsContext } from '../providers';
import {
GridProvider,
useGridLayoutContext,
useMeasurementsContext
} from '../providers';
import type {
DropIndicatorSettings,
SortableGridProps,
Expand Down Expand Up @@ -165,8 +169,16 @@ function SortableGridInner<I>({
}: SortableGridInnerProps<I>) {
const { handleContainerMeasurement, resetMeasurements } =
useMeasurementsContext();
const { mainGroupSize } = useGridLayoutContext();
const isFirstRenderRef = useRef(true);

useEffect(resetMeasurements, [groups, resetMeasurements]);
useLayoutEffect(() => {
if (isFirstRenderRef.current) {
isFirstRenderRef.current = false;
return;
}
resetMeasurements();
}, [groups, resetMeasurements]);

const animatedInnerStyle = useAnimatedStyle(() =>
isVertical
Expand All @@ -192,8 +204,9 @@ function SortableGridInner<I>({
`calc((100% - ${columnGap.value * (groups - 1)}px) / ${groups})` as DimensionValue
}
: {
flexBasis: `${100 / groups}%`,
paddingHorizontal: columnGap.value / 2
flexBasis: mainGroupSize.value ? undefined : `${100 / groups}%`,
paddingHorizontal: columnGap.value / 2,
width: mainGroupSize.value
}
: { height: rowHeight }
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useAnimatedReaction } from 'react-native-reanimated';

import { IS_WEB } from '../../../constants';
import { useDebugContext } from '../../../debug';
import { useMutableValue } from '../../../integrations/reanimated';
import type { GridLayoutContextType } from '../../../types';
import {
useAutoScrollContext,
Expand Down Expand Up @@ -60,6 +61,8 @@ const { GridLayoutProvider, useGridLayoutContext } = createProvider(
const mainGap = isVertical ? columnGap : rowGap;
const crossGap = isVertical ? rowGap : columnGap;

const mainGroupSize = useMutableValue<null | number>(null);

// MAIN GROUP SIZE UPDATER
useAnimatedReaction(
() => {
Expand All @@ -83,6 +86,7 @@ const { GridLayoutProvider, useGridLayoutContext } = createProvider(
} else {
itemHeights.value = value;
}
mainGroupSize.value = value;

// DEBUG ONLY
if (debugMainGapRects) {
Expand Down Expand Up @@ -168,6 +172,7 @@ const { GridLayoutProvider, useGridLayoutContext } = createProvider(
crossGap,
isVertical,
mainGap,
mainGroupSize,
numGroups
}
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ export const createGridStrategy =
itemHeights,
itemWidths
} = useCommonValuesContext();
const { crossGap, isVertical, mainGap, numGroups } = useGridLayoutContext();
const { crossGap, isVertical, mainGap, mainGroupSize, numGroups } =
useGridLayoutContext();
const { additionalCrossOffset } = useAdditionalCrossOffsetContext() ?? {};
const { fixedItemKeys } = useCustomHandleContext() ?? {};

Expand Down Expand Up @@ -75,14 +76,11 @@ export const createGridStrategy =

return ({ activeIndex, dimensions, position }) => {
'worklet';
const mainGroupSize = (isVertical ? itemWidths : itemHeights).value;

if (
!othersLayout.value ||
crossContainerSize.value === null ||
mainContainerSize.value === null ||
mainGroupSize === null ||
typeof mainGroupSize !== 'number'
mainGroupSize.value === null
) {
return;
}
Expand Down Expand Up @@ -170,7 +168,7 @@ export const createGridStrategy =
if (mainBeforeBound !== Infinity) {
mainIndex--;
}
mainBeforeOffset = mainIndex * (mainGroupSize + mainGap.value);
mainBeforeOffset = mainIndex * (mainGroupSize.value + mainGap.value);
mainBeforeBound = mainBeforeOffset - additionalOffset;
} while (
mainBeforeBound > 0 &&
Expand All @@ -186,7 +184,8 @@ export const createGridStrategy =
mainIndex++;
}
mainAfterOffset =
mainIndex * (mainGroupSize + mainGap.value) + mainGroupSize;
mainIndex * (mainGroupSize.value + mainGap.value) +
mainGroupSize.value;
mainAfterBound = mainAfterOffset + additionalOffset;
} while (
mainAfterBound < mainContainerSize.value &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ import { createProvider } from '../utils';
import { useCommonValuesContext } from './CommonValuesProvider';
import { useMultiZoneContext } from './MultiZoneProvider';

const DEBOUNCE_DURATION = 100;

type StateContext = {
measuredItemKeys: Set<string>;
queuedMeasurements: Map<string, Dimensions>;
};

type MeasurementsProviderProps = {
itemsCount: number;
};
Expand All @@ -40,10 +47,7 @@ const { MeasurementsProvider, useMeasurementsContext } = createProvider(
const { activeItemDimensions: multiZoneActiveItemDimensions } =
useMultiZoneContext() ?? {};

const measuredItemsCount = useMutableValue(0);
const queuedMeasurements = useMutableValue<Map<string, Dimensions> | null>(
null
);
const context = useMutableValue<null | StateContext>(null);
const previousItemDimensionsRef = useRef<Record<string, Dimensions>>({});
const debounce = useAnimatedDebounce();

Expand Down Expand Up @@ -79,19 +83,23 @@ const { MeasurementsProvider, useMeasurementsContext } = createProvider(
previousItemDimensionsRef.current[key] = dimensions;

runOnUI(() => {
queuedMeasurements.value ??= new Map();
const queued = queuedMeasurements.value;
context.value ??= {
measuredItemKeys: new Set(),
queuedMeasurements: new Map()
};

const ctx = context.value;

const isNewItem =
!queued.get(key) &&
!ctx.measuredItemKeys.has(key) &&
(resolveDimension(itemWidths.value, key) === null ||
resolveDimension(itemHeights.value, key) === null);

if (isNewItem) {
measuredItemsCount.value += 1;
ctx.measuredItemKeys.add(key);
}

queued.set(key, dimensions);
ctx.queuedMeasurements.set(key, dimensions);

if (activeItemKey.value === key) {
activeItemDimensions.value = dimensions;
Expand All @@ -102,7 +110,7 @@ const { MeasurementsProvider, useMeasurementsContext } = createProvider(

// 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) {
if (ctx.measuredItemKeys.size !== itemsCount) {
return;
}

Expand All @@ -111,7 +119,7 @@ const { MeasurementsProvider, useMeasurementsContext } = createProvider(
dimension: Dimension,
sizes: SharedValue<ItemSizes>
) => {
for (const [k, dims] of queued.entries()) {
for (const [k, dims] of ctx.queuedMeasurements.entries()) {
(sizes.value as Record<string, number>)[k] = dims[dimension];
}
sizes.modify();
Expand All @@ -124,11 +132,11 @@ const { MeasurementsProvider, useMeasurementsContext } = createProvider(
updateDimension('height', itemHeights);
}

queuedMeasurements.value = null;
ctx.queuedMeasurements.clear();
debounce.cancel();
};

if (isNewItem || queued.size === itemsCount) {
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
Expand All @@ -137,7 +145,7 @@ const { MeasurementsProvider, useMeasurementsContext } = createProvider(
} 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, 100);
debounce.schedule(updateDimensions, DEBOUNCE_DURATION);
}
})();
}
Expand All @@ -159,10 +167,10 @@ const { MeasurementsProvider, useMeasurementsContext } = createProvider(
if (itemHeights.value && typeof itemHeights.value === 'object') {
delete itemHeights.value[key];
}
measuredItemsCount.value -= 1;
context.value?.measuredItemKeys.delete(key);
})();
},
[controlledItemDimensions, itemHeights, itemWidths, measuredItemsCount]
[controlledItemDimensions, itemHeights, itemWidths, context]
);

const handleContainerMeasurement = useCallback(
Expand Down Expand Up @@ -196,7 +204,7 @@ const { MeasurementsProvider, useMeasurementsContext } = createProvider(

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)
// in a short period of time (this may cause issues on low-end devices)
setAnimatedTimeout(() => {
usesAbsoluteLayout.value = true;
}, 100);
Expand All @@ -213,16 +221,15 @@ const { MeasurementsProvider, useMeasurementsContext } = createProvider(
const resetMeasurements = useCallback(() => {
previousItemDimensionsRef.current = {};
runOnUI(() => {
measuredItemsCount.value = 0;
queuedMeasurements.value = null;
context.value = null;
if (typeof itemWidths.value === 'object') {
itemWidths.value = {};
}
if (typeof itemHeights.value === 'object') {
itemHeights.value = {};
}
})();
}, [itemHeights, itemWidths, measuredItemsCount, queuedMeasurements]);
}, [itemHeights, itemWidths, context]);

return {
value: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ export type GridLayoutContextType = {
numGroups: number;
mainGap: SharedValue<number>;
crossGap: SharedValue<number>;
mainGroupSize: SharedValue<null | number>;
isVertical: boolean;
};
Loading