diff --git a/packages/react-native-sortables/src/providers/shared/AutoScrollProvider.tsx b/packages/react-native-sortables/src/providers/shared/AutoScrollProvider.tsx index 8db80eec..3b125893 100644 --- a/packages/react-native-sortables/src/providers/shared/AutoScrollProvider.tsx +++ b/packages/react-native-sortables/src/providers/shared/AutoScrollProvider.tsx @@ -10,7 +10,6 @@ import { useSharedValue } from 'react-native-reanimated'; -import { OFFSET_EPS } from '../../constants'; import { useDebugContext } from '../../debug'; import { useAnimatableValue } from '../../hooks'; import type { AutoScrollContextType, AutoScrollSettings } from '../../types'; @@ -31,13 +30,8 @@ const { AutoScrollProvider, useAutoScrollContext } = createProvider( autoScrollSpeed, scrollableRef }) => { - const { - activeAnimationProgress, - activeItemKey, - containerRef, - itemDimensions, - touchPosition - } = useCommonValuesContext(); + const { activeItemKey, containerRef, itemDimensions, touchPosition } = + useCommonValuesContext(); const debugContext = useDebugContext(); const debugRects = debugContext?.useDebugRects(['top', 'bottom']); @@ -45,14 +39,12 @@ const { AutoScrollProvider, useAutoScrollContext } = createProvider( // eslint-disable-next-line @typescript-eslint/no-unsafe-argument const scrollOffset = useScrollViewOffset(scrollableRef); - const targetScrollOffset = useSharedValue(null); const dragStartScrollOffset = useAnimatableValue(null); - const startContainerPageY = useSharedValue(null); - const prevScrollToOffset = useSharedValue(null); + const dragScrollOffsetDiff = useSharedValue(0); const activeItemHeight = useDerivedValue(() => { const key = activeItemKey.value; - return (key ? itemDimensions.value[key]?.height : null) ?? null; + return key && itemDimensions.value[key]?.height; }); const offsetThreshold = useAnimatableValue( autoScrollActivationOffset, @@ -68,39 +60,86 @@ const { AutoScrollProvider, useAutoScrollContext } = createProvider( const isFrameCallbackActive = useSharedValue(false); + const hideDebugViews = useCallback(() => { + 'worklet'; + debugRects?.top?.hide(); + debugRects?.bottom?.hide(); + debugLine?.hide(); + }, [debugLine, debugRects]); + // SMOOTH SCROLL POSITION UPDATER // Updates the scroll position smoothly // (quickly at first, then slower if the remaining distance is small) const frameCallback = useFrameCallback(() => { - const targetOffset = targetScrollOffset.value; - if (!isFrameCallbackActive.value || targetOffset === null) { + if ( + activeItemHeight.value === null || + touchPosition.value === null || + dragStartScrollOffset.value === null + ) { + hideDebugViews(); return; } - const currentOffset = scrollOffset.value; - const diff = targetOffset - currentOffset; - if (Math.abs(diff) < OFFSET_EPS) { - targetScrollOffset.value = null; + const scrollableMeasurements = measure(scrollableRef); + const containerMeasurements = measure(containerRef); + + if (!scrollableMeasurements || !containerMeasurements) { + hideDebugViews(); return; } - const direction = diff > 0 ? 1 : -1; - const step = speed.value * direction * Math.sqrt(Math.abs(diff)); - const nextOffset = - targetOffset > currentOffset - ? Math.min(currentOffset + step, targetOffset) - : Math.max(currentOffset + step, targetOffset); + const scrollToOffset = + dragStartScrollOffset.value + dragScrollOffsetDiff.value; + if (Math.abs(scrollOffset.value - scrollToOffset) > 1) { + console.log('scrollToOffset', scrollToOffset, scrollOffset.value); + scrollTo(scrollableRef, 0, scrollToOffset, false); + } - if ( - Math.abs(nextOffset - currentOffset) < 0.1 * OFFSET_EPS || - prevScrollToOffset.value === nextOffset - ) { - targetScrollOffset.value = null; - return; + const threshold = offsetThreshold.value; + const touchOffset = touchPosition.value.y; + const { height: sH, pageY: sY } = scrollableMeasurements; + const { height: cH, pageY: cY } = containerMeasurements; + + if (debugRects) { + debugRects.top.set({ + ...DEBUG_COLORS, + height: threshold.top, + y: sY - cY + }); + debugRects.bottom.set({ + ...DEBUG_COLORS, + height: threshold.bottom, + positionOrigin: 'bottom', + y: sY - cY + sH + }); + } + if (debugLine) { + debugLine.set({ + color: DEBUG_COLORS.backgroundColor, + y: touchOffset + }); } - scrollTo(scrollableRef, 0, nextOffset, false); - prevScrollToOffset.value = nextOffset; + const topDistance = sY + threshold.top - cY; + const bottomDistance = cY + cH - (sY + sH - threshold.bottom); + + const topOverflow = sY + threshold.top - (cY + touchOffset); + const bottomOverflow = cY + touchOffset - (sY + sH - threshold.bottom); + + const scrollOffsetDiff = scrollOffset.value - dragStartScrollOffset.value; + // Scroll up + if (topDistance > 0 && topOverflow > 0) { + console.log('up dragScrollOffsetDiff.value', dragScrollOffsetDiff.value); + dragScrollOffsetDiff.value = scrollOffsetDiff - 0.05 * topOverflow; + } + // Scroll down + else if (bottomDistance > 0 && bottomOverflow > 0) { + console.log( + 'down dragScrollOffsetDiff.value', + dragScrollOffsetDiff.value + ); + dragScrollOffsetDiff.value = scrollOffsetDiff + 0.05 * bottomOverflow; + } }, false); const toggleFrameCallback = useCallback( @@ -111,127 +150,32 @@ const { AutoScrollProvider, useAutoScrollContext } = createProvider( // Enable/disable frame callback useAnimatedReaction( () => ({ - isEnabled: enabled.value, - itemKey: activeItemKey.value, - progress: activeAnimationProgress.value + activeKey: activeItemKey.value, + isEnabled: enabled.value }), - ({ isEnabled, itemKey, progress }) => { - const shouldBeEnabled = isEnabled && itemKey !== null; - if ( - isFrameCallbackActive.value === shouldBeEnabled || - (itemKey !== null && progress < 0.5) - ) { + ({ activeKey, isEnabled }) => { + const shouldBeEnabled = isEnabled && activeKey !== null; + if (isFrameCallbackActive.value === shouldBeEnabled) { return; } - targetScrollOffset.value = null; - startContainerPageY.value = null; - prevScrollToOffset.value = null; runOnJS(toggleFrameCallback)(shouldBeEnabled); isFrameCallbackActive.value = shouldBeEnabled; } ); - // AUTO SCROLL HANDLER - // Automatically scrolls the container when the active item is near the edge - useAnimatedReaction( - () => { - if ( - !enabled.value || - activeItemHeight.value === null || - !touchPosition.value - ) { - return null; - } - - return { - itemHeight: activeItemHeight.value, - threshold: offsetThreshold.value, - touchOffset: touchPosition.value.y - }; - }, - props => { - const hideDebugViews = () => { - debugRects?.top?.hide(); - debugRects?.bottom?.hide(); - debugLine?.hide(); - }; - - if (!props) { - hideDebugViews(); - return; - } - - const scrollableMeasurements = measure(scrollableRef); - const containerMeasurements = measure(containerRef); - - if ( - !scrollableMeasurements || - !containerMeasurements || - dragStartScrollOffset.value === null - ) { - hideDebugViews(); - return; - } - - const { threshold, touchOffset } = props; - const { height: sH, pageY: sY } = scrollableMeasurements; - const { height: cH, pageY: cY } = containerMeasurements; - - if (startContainerPageY.value === null) { - startContainerPageY.value = cY; - } - - const topDistance = sY + threshold.top - cY; - const bottomDistance = cY + cH - (sY + sH - threshold.bottom); - - const topOverflow = sY + threshold.top - (cY + touchOffset); - const bottomOverflow = cY + touchOffset - (sY + sH - threshold.bottom); - - if (debugRects) { - debugRects.top.set({ - ...DEBUG_COLORS, - height: threshold.top, - y: sY - cY - }); - debugRects.bottom.set({ - ...DEBUG_COLORS, - height: threshold.bottom, - positionOrigin: 'bottom', - y: sY - cY + sH - }); - } - if (debugLine) { - debugLine.set({ - color: DEBUG_COLORS.backgroundColor, - y: touchOffset - }); - } - - const deltaY = startContainerPageY.value - cY; - const offsetY = dragStartScrollOffset.value + deltaY; - // Scroll up - if (topDistance > 0 && topOverflow > 0) { - targetScrollOffset.value = offsetY - Math.min(topOverflow, topDistance); - } - // Scroll down - else if (bottomDistance > 0 && bottomOverflow > 0) { - targetScrollOffset.value = - offsetY + Math.min(bottomOverflow, bottomDistance); - } - } - ); - const updateStartScrollOffset = useCallback( (providedOffset?: null | number) => { 'worklet'; + dragScrollOffsetDiff.value = 0; dragStartScrollOffset.value = - providedOffset === undefined ? scrollOffset.value : providedOffset; + providedOffset !== undefined ? providedOffset : scrollOffset.value; }, - [dragStartScrollOffset, scrollOffset] + [dragScrollOffsetDiff, dragStartScrollOffset, scrollOffset] ); return { value: { + dragScrollOffsetDiff, dragStartScrollOffset, scrollOffset, updateStartScrollOffset diff --git a/packages/react-native-sortables/src/providers/shared/DragProvider.tsx b/packages/react-native-sortables/src/providers/shared/DragProvider.tsx index 0b94998e..eddd9e14 100644 --- a/packages/react-native-sortables/src/providers/shared/DragProvider.tsx +++ b/packages/react-native-sortables/src/providers/shared/DragProvider.tsx @@ -79,7 +79,7 @@ const { DragProvider, useDragContext } = createProvider('Drag')< const { maybeUpdateSnapDimensions, tryMeasureContainerHeight } = useMeasurementsContext(); const { updateLayer } = useLayerContext() ?? {}; - const { dragStartScrollOffset, scrollOffset, updateStartScrollOffset } = + const { dragScrollOffsetDiff, updateStartScrollOffset } = useAutoScrollContext() ?? {}; const debugContext = useDebugContext(); @@ -114,10 +114,7 @@ const { DragProvider, useDragContext } = createProvider('Drag')< offsetX: snapOffsetX.value, offsetY: snapOffsetY.value, progress: activeAnimationProgress.value, - scrollOffsetY: - dragStartScrollOffset?.value === -1 - ? 0 - : (scrollOffset?.value ?? 0) - (dragStartScrollOffset?.value ?? 0), + scrollOffsetDiff: dragScrollOffsetDiff?.value ?? 0, snapDimensions: snapItemDimensions.value, snapOffset: snapItemOffset.value, startTouchPosition: dragStartTouchPosition.value, @@ -133,7 +130,7 @@ const { DragProvider, useDragContext } = createProvider('Drag')< offsetX, offsetY, progress, - scrollOffsetY, + scrollOffsetDiff, snapDimensions, snapOffset, startTouchPosition, @@ -155,7 +152,7 @@ const { DragProvider, useDragContext } = createProvider('Drag')< touchPosition.value = { x: startTouchPosition.x + (translation?.x ?? 0), - y: startTouchPosition.y + (translation?.y ?? 0) + scrollOffsetY + y: startTouchPosition.y + (translation?.y ?? 0) + scrollOffsetDiff }; if (debugCross) { diff --git a/packages/react-native-sortables/src/types/providers/shared.ts b/packages/react-native-sortables/src/types/providers/shared.ts index f321c915..7358453d 100644 --- a/packages/react-native-sortables/src/types/providers/shared.ts +++ b/packages/react-native-sortables/src/types/providers/shared.ts @@ -75,7 +75,8 @@ export type MeasurementsContextType = { export type AutoScrollContextType = { scrollOffset: SharedValue; dragStartScrollOffset: SharedValue; - updateStartScrollOffset: (providedOffset?: null | number) => void; + dragScrollOffsetDiff: SharedValue; + updateStartScrollOffset: (providedOffset?: null | null | number) => void; }; // DRAG