react-native-sortables
Version:
Powerful Sortable Components for Flexible Content Reordering in React Native
236 lines (226 loc) • 8.27 kB
JavaScript
;
import React from "react";
import { useCallback } from 'react';
import { interpolate, measure, runOnJS, scrollTo, useAnimatedReaction, useDerivedValue, useFrameCallback, useScrollViewOffset } from 'react-native-reanimated';
import { EMPTY_OBJECT } from '../../../constants';
import { useMutableValue } from '../../../integrations/reanimated';
import { toPair } from '../../../utils';
import { createProvider } from '../../utils';
import { useCommonValuesContext } from '../CommonValuesProvider';
import useDebugHelpers from './useDebugHelpers';
import { calculateRawProgress } from './utils';
// Maximum elapsed time multiplier to prevent excessive scrolling distances when app lags
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
const MAX_ELAPSED_TIME_MULTIPLIER = 2;
const MIN_ELAPSED_TIME_CAP = 100;
const {
AutoScrollProvider,
useAutoScrollContext
} = createProvider('AutoScroll', {
guarded: false
})(({
autoScrollDirection,
autoScrollEnabled: enabled,
children,
scrollableRef,
...rest
}) => {
const {
containerRef
} = useCommonValuesContext();
const scrollOffset = useScrollViewOffset(scrollableRef);
const context = useMutableValue(null);
const contentBounds = useMutableValue(null);
const scrollOffsetDiff = useDerivedValue(() => {
const startOffset = context.value?.startScrollOffset;
if (startOffset === undefined) {
return 0;
}
return scrollOffset.value - startOffset;
});
const isVertical = autoScrollDirection === 'vertical';
const scrollToOffset = useCallback((offset, animated) => {
'worklet';
if (Math.abs(offset - scrollOffset.value) < 1) {
return;
}
scrollTo(scrollableRef, isVertical ? 0 : offset, isVertical ? offset : 0, animated);
}, [isVertical, scrollOffset, scrollableRef]);
const scrollBy = useCallback((distance, animated) => {
'worklet';
if (Math.abs(distance) < 1) {
return;
}
scrollToOffset(scrollOffset.value + distance, animated);
}, [scrollToOffset, scrollOffset]);
const getSortableOffset = useCallback(() => {
'worklet';
const containerMeasurements = measure(containerRef);
const scrollableMeasurements = measure(scrollableRef);
if (!containerMeasurements || !scrollableMeasurements) {
return null;
}
const prop = isVertical ? 'pageY' : 'pageX';
const scrollContainerPosition = scrollableMeasurements[prop] - scrollOffset.value;
const sortableContainerPosition = containerMeasurements[prop];
return sortableContainerPosition - scrollContainerPosition;
}, [containerRef, scrollableRef, scrollOffset, isVertical]);
return {
children: /*#__PURE__*/_jsxs(_Fragment, {
children: [children, enabled && /*#__PURE__*/_jsx(AutoScrollUpdater, {
...rest,
contentBounds: contentBounds,
context: context,
getSortableOffset: getSortableOffset,
isVertical: isVertical,
scrollableRef: scrollableRef,
scrollOffset: scrollOffset,
scrollToOffset: scrollToOffset
})]
}),
enabled,
value: {
contentBounds,
isVerticalScroll: isVertical,
scrollableRef,
scrollBy,
scrollOffsetDiff
}
};
});
function AutoScrollUpdater({
animateScrollTo,
autoScrollActivationOffset,
autoScrollExtrapolation,
autoScrollInterval,
autoScrollMaxOverscroll,
autoScrollMaxVelocity,
contentBounds,
context,
getSortableOffset,
isVertical,
scrollableRef,
scrollOffset,
scrollToOffset
}) {
const {
activeAnimationProgress,
touchPosition
} = useCommonValuesContext();
const scrollAxis = isVertical ? 'y' : 'x';
const activationOffset = toPair(autoScrollActivationOffset);
const [maxStartVelocity, maxEndVelocity] = toPair(autoScrollMaxVelocity);
const maxOverscroll = toPair(autoScrollMaxOverscroll);
const isActive = useDerivedValue(() => activeAnimationProgress.value === 1);
const contentAxisBounds = useDerivedValue(() => {
if (!contentBounds.value) {
return null;
}
const [start, end] = contentBounds.value;
return [start[scrollAxis], end[scrollAxis]];
});
let debug = EMPTY_OBJECT;
if (__DEV__) {
// eslint-disable-next-line react-hooks/rules-of-hooks
debug = useDebugHelpers(isVertical, activationOffset, contentAxisBounds, maxOverscroll);
}
const scrollBy = useCallback((distance, animated) => {
'worklet';
const ctx = context.value;
const bounds = contentAxisBounds.value;
const scrollableMeasurements = measure(scrollableRef);
if (!ctx || !bounds || !scrollableMeasurements) {
return;
}
const scrollableSize = scrollableMeasurements[isVertical ? 'height' : 'width'];
let newScrollOffset = 0;
if (distance > 0) {
// scroll down
newScrollOffset = Math.min(ctx.targetScrollOffset + distance, ctx.sortableOffset - scrollableSize + bounds[1] + maxOverscroll[1]);
} else if (distance < 0) {
// scroll up
newScrollOffset = Math.max(ctx.targetScrollOffset + distance, ctx.sortableOffset + bounds[0] - maxOverscroll[0]);
} else return;
if (Math.abs(newScrollOffset - ctx.targetScrollOffset) < 1) {
return;
}
ctx.targetScrollOffset = newScrollOffset;
scrollToOffset(newScrollOffset, animated);
}, [context, scrollToOffset, contentAxisBounds, maxOverscroll, isVertical, scrollableRef]);
const frameCallbackFunction = useCallback(({
timestamp
}) => {
'worklet';
const ctx = context.value;
if (!ctx?.progress) {
return;
}
ctx.lastUpdateTimestamp ??= timestamp;
const elapsedTime = timestamp - ctx.lastUpdateTimestamp;
if (elapsedTime < autoScrollInterval) {
return;
}
// Cap the elapsed time to prevent excessive scrolling distances when app lags
const maxElapsedTime = Math.max(autoScrollInterval * MAX_ELAPSED_TIME_MULTIPLIER, MIN_ELAPSED_TIME_CAP);
const cappedElapsedTime = Math.min(elapsedTime, maxElapsedTime);
ctx.lastUpdateTimestamp = timestamp;
const velocity = interpolate(ctx.progress, [-1, 0, 1], [-maxStartVelocity, 0, maxEndVelocity]);
const distance = velocity * (cappedElapsedTime / 1000);
scrollBy(distance, animateScrollTo);
}, [context, scrollBy, maxStartVelocity, maxEndVelocity, autoScrollInterval, animateScrollTo]);
const frameCallback = useFrameCallback(frameCallbackFunction);
const toggleFrameCallback = useCallback(enabled => {
frameCallback.setActive(enabled);
}, [frameCallback]);
const enableAutoScroll = useCallback(() => {
'worklet';
if (context.value) {
return;
}
context.value = {
progress: 0,
sortableOffset: getSortableOffset() ?? 0,
startScrollOffset: scrollOffset.value,
targetScrollOffset: scrollOffset.value
};
runOnJS(toggleFrameCallback)(true);
}, [context, scrollOffset, toggleFrameCallback, getSortableOffset]);
const disableAutoScroll = useCallback(() => {
'worklet';
if (!context.value) {
return;
}
context.value = null;
debug?.hideDebugViews?.();
runOnJS(toggleFrameCallback)(false);
}, [toggleFrameCallback, context, debug]);
useAnimatedReaction(() => isActive.value, active => active ? enableAutoScroll() : disableAutoScroll(), [enableAutoScroll, disableAutoScroll]);
useAnimatedReaction(() => ({
bounds: contentAxisBounds.value,
ctx: context.value,
position: touchPosition.value?.[scrollAxis]
}), ({
bounds,
ctx,
position
}) => {
if (position === undefined || !bounds || !ctx) {
disableAutoScroll();
return;
}
const scrollableMeasurements = measure(scrollableRef);
if (!scrollableMeasurements) {
disableAutoScroll();
return;
}
const scrollableSize = scrollableMeasurements[isVertical ? 'height' : 'width'];
const containerPos = ctx.sortableOffset - scrollOffset.value;
ctx.progress = calculateRawProgress(position, containerPos, scrollableSize, activationOffset, bounds, maxOverscroll, autoScrollExtrapolation);
if (debug) {
debug?.updateDebugRects?.(containerPos, scrollableSize);
}
}, [debug]);
return null;
}
export { AutoScrollProvider, useAutoScrollContext };
//# sourceMappingURL=AutoScrollProvider.js.map