react-native-reorderable-list
Version:
Reorderable list for React Native applications, powered by Reanimated
719 lines (677 loc) • 33.5 kB
JavaScript
function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { FlatList, InteractionManager, Platform } from 'react-native';
import { Gesture, GestureDetector, State } from 'react-native-gesture-handler';
import Animated, { Easing, measure, runOnJS, runOnUI, scrollTo, useAnimatedReaction, useAnimatedRef, useAnimatedScrollHandler, useComposedEventHandler, useDerivedValue, useSharedValue, withDelay, withTiming } from 'react-native-reanimated';
import { ReorderableListContext } from '../contexts';
import { ReorderableListState } from '../types';
import { AUTOSCROLL_CONFIG, OPACITY_ANIMATION_CONFIG_DEFAULT, SCALE_ANIMATION_CONFIG_DEFAULT } from './constants';
import { ReorderableListCell } from './ReorderableListCell';
import { usePropAsSharedValue, useStableCallback } from '../hooks';
const AnimatedFlatList = Animated.createAnimatedComponent(FlatList);
const ReorderableListCore = ({
autoscrollThreshold = 0.1,
autoscrollThresholdOffset,
autoscrollSpeedScale = 1,
autoscrollDelay = AUTOSCROLL_CONFIG.delay,
autoscrollActivationDelta = 5,
animationDuration = 200,
onLayout,
onReorder,
onScroll,
onDragStart,
onDragEnd,
onIndexChange,
scrollViewContainerRef,
scrollViewPageXY,
scrollViewSize,
scrollViewScrollOffsetXY,
scrollViewScrollEnabledProp,
setScrollViewForceDisableScroll,
scrollable,
outerScrollGesture,
cellAnimations,
dragEnabled = true,
shouldUpdateActiveItem,
itemLayoutAnimation,
panGesture,
panEnabled = true,
panActivateAfterLongPress,
data,
keyExtractor,
...rest
}, ref) => {
const scrollEnabled = rest.scrollEnabled ?? true;
const flatListRef = useAnimatedRef();
const markedCellsRef = useRef();
const [activeIndex, setActiveIndex] = useState(-1);
const prevItemCount = useRef(data.length);
const [forceDisableScroll, setForceDisableScroll] = useState(false);
const scrollEnabledProp = usePropAsSharedValue(scrollEnabled);
const currentScrollEnabled = useSharedValue(scrollEnabled);
const gestureState = useSharedValue(State.UNDETERMINED);
const currentXY = useSharedValue(0);
const currentTranslationXY = useSharedValue(0);
const currentItemDragCenterXY = useSharedValue(null);
const startItemDragCenterXY = useSharedValue(0);
const flatListScrollOffsetXY = useSharedValue(0);
const flatListSize = useSharedValue(0);
const flatListPageXY = useSharedValue(0);
// The scroll x or y translation of the list since drag start
const dragScrollTranslationXY = useSharedValue(0);
// The initial scroll offset x or y of the list on drag start
const dragInitialScrollOffsetXY = useSharedValue(0);
// The scroll x or y translation of the ScrollViewContainer since drag start
const scrollViewDragScrollTranslationXY = useSharedValue(0);
// The initial scroll offset x or y of the ScrollViewContainer on drag start
const scrollViewDragInitialScrollOffsetXY = useSharedValue(0);
const draggedSize = useSharedValue(0);
const itemOffset = useSharedValue([]);
const itemSize = useSharedValue([]);
// We need to track data length since itemOffset and itemSize might contain more data than we need.
// e.g. items are removed from the list, in which case layout data for those items is set to 0.
const itemCount = useSharedValue(data.length);
const autoscrollTrigger = useSharedValue(-1);
const lastAutoscrollTrigger = useSharedValue(-1);
const dragXY = useSharedValue(0);
const currentIndex = useSharedValue(-1);
const draggedIndex = useSharedValue(-1);
const state = useSharedValue(ReorderableListState.IDLE);
const dragEndHandlers = useSharedValue([]);
const startXY = useSharedValue(0);
const scaleDefault = useSharedValue(1);
const opacityDefault = useSharedValue(1);
const dragDirection = useSharedValue(0);
const lastDragDirectionPivot = useSharedValue(null);
const itemLayoutAnimationPropRef = useRef(itemLayoutAnimation);
itemLayoutAnimationPropRef.current = itemLayoutAnimation;
const keyExtractorPropRef = useRef(keyExtractor);
keyExtractorPropRef.current = keyExtractor;
const animationDurationProp = usePropAsSharedValue(animationDuration);
const autoscrollActivationDeltaProp = usePropAsSharedValue(autoscrollActivationDelta);
const dragEnabledProp = usePropAsSharedValue(dragEnabled ?? true);
const horizontalProp = usePropAsSharedValue(!!rest.horizontal);
// Position of the list relative to the scroll container
const nestedFlatListPositionXY = useDerivedValue(() => flatListPageXY.value - ((scrollViewPageXY === null || scrollViewPageXY === void 0 ? void 0 : scrollViewPageXY.value) || 0));
useEffect(() => {
itemCount.value = data.length;
// This could be done unmount of the removed cell, however it leads to bugs.
// Surprisingly the unmount gets sometimes called after the onLayout event
// setting all layout data to 0 and breaking the list. So we solve it like this.
if (data.length < prevItemCount.current) {
for (let i = data.length; i < prevItemCount.current; i++) {
runOnUI(() => {
itemSize.value[i] = 0;
itemOffset.value[i] = 0;
})();
}
}
prevItemCount.current = data.length;
}, [data.length, itemSize, itemOffset, itemCount]);
useEffect(() => {
if (!markedCellsRef.current ||
// Clean keys once they surpass by 10% the size of the list itself.
markedCellsRef.current.size <= data.length + Math.ceil(data.length * 0.1)) {
return;
}
// Can be heavy to loop through all items, defer the task to run after interactions.
const task = InteractionManager.runAfterInteractions(() => {
if (!markedCellsRef.current) {
return;
}
const map = new Map();
for (let i = 0; i < data.length; i++) {
var _keyExtractorPropRef$;
const key = ((_keyExtractorPropRef$ = keyExtractorPropRef.current) === null || _keyExtractorPropRef$ === void 0 ? void 0 : _keyExtractorPropRef$.call(keyExtractorPropRef, data[i], i)) || i.toString();
if (markedCellsRef.current.has(key)) {
map.set(key, markedCellsRef.current.get(key));
}
}
markedCellsRef.current = map;
});
return () => {
task.cancel();
};
}, [data]);
const createCellKey = useCallback(cellKey => {
var _markedCellsRef$curre;
const mark = ((_markedCellsRef$curre = markedCellsRef.current) === null || _markedCellsRef$curre === void 0 ? void 0 : _markedCellsRef$curre.get(cellKey)) || 0;
return `${cellKey}#${mark}`;
}, []);
const listContextValue = useMemo(() => ({
draggedSize,
currentIndex,
draggedIndex,
dragEndHandlers,
activeIndex,
itemLayoutAnimation: itemLayoutAnimationPropRef,
horizontal: horizontalProp,
cellAnimations: {
...cellAnimations,
transform: cellAnimations && 'transform' in cellAnimations ? cellAnimations.transform : [{
scale: scaleDefault
}],
opacity: cellAnimations && 'opacity' in cellAnimations ? cellAnimations.opacity : opacityDefault
}
}), [draggedSize, currentIndex, draggedIndex, dragEndHandlers, activeIndex, cellAnimations, itemLayoutAnimationPropRef, scaleDefault, opacityDefault, horizontalProp]);
/**
* Decides the intended drag direction of the user.
* This is used to to determine if the user intends to autoscroll
* when within the threshold area.
*
* @param e - The payload of the pan gesture update event.
*/
const setDragDirection = useCallback(e => {
'worklet';
const absoluteXY = horizontalProp.value ? e.absoluteX : e.absoluteY;
const velocityXY = horizontalProp.value ? e.velocityX : e.velocityY;
const direction = velocityXY > 0 ? 1 : -1;
if (direction !== dragDirection.value) {
if (lastDragDirectionPivot.value === null) {
lastDragDirectionPivot.value = absoluteXY;
} else if (Math.abs(absoluteXY - lastDragDirectionPivot.value) >= autoscrollActivationDeltaProp.value) {
dragDirection.value = direction;
lastDragDirectionPivot.value = absoluteXY;
}
}
}, [dragDirection, lastDragDirectionPivot, autoscrollActivationDeltaProp, horizontalProp]);
const setCurrentItemDragCenterXY = useCallback(e => {
'worklet';
const translationXY = horizontalProp.value ? e.translationX : e.translationY;
if (currentItemDragCenterXY.value === null) {
if (currentIndex.value >= 0) {
const itemCenter = itemSize.value[currentIndex.value] * 0.5;
// the x or y coordinate of the item relative to the list
const itemXY = itemOffset.value[currentIndex.value] - (flatListScrollOffsetXY.value + scrollViewDragScrollTranslationXY.value);
const value = itemXY + itemCenter + translationXY;
startItemDragCenterXY.value = value;
currentItemDragCenterXY.value = value;
}
} else {
currentItemDragCenterXY.value = startItemDragCenterXY.value + translationXY;
}
}, [horizontalProp, currentItemDragCenterXY, currentIndex, startItemDragCenterXY, itemOffset, itemSize, flatListScrollOffsetXY, scrollViewDragScrollTranslationXY]);
const panGestureHandler = useMemo(() => (panGesture || Gesture.Pan()).onBegin(e => {
'worklet';
// prevent new dragging until item is completely released
if (state.value === ReorderableListState.IDLE) {
const xy = horizontalProp.value ? e.x : e.y;
const translationXY = horizontalProp.value ? e.translationX : e.translationY;
startXY.value = xy;
currentXY.value = xy;
currentTranslationXY.value = translationXY;
dragXY.value = translationXY;
gestureState.value = e.state;
}
}).onUpdate(e => {
'worklet';
if (state.value === ReorderableListState.DRAGGED) {
setDragDirection(e);
}
if (state.value !== ReorderableListState.RELEASED) {
setCurrentItemDragCenterXY(e);
const translationXY = horizontalProp.value ? e.translationX : e.translationY;
currentXY.value = startXY.value + translationXY;
currentTranslationXY.value = translationXY;
dragXY.value = translationXY + dragScrollTranslationXY.value + scrollViewDragScrollTranslationXY.value;
gestureState.value = e.state;
}
}).onEnd(e => {
'worklet';
gestureState.value = e.state;
}).onFinalize(e => {
'worklet';
gestureState.value = e.state;
}), [panGesture, state, startXY, currentXY, currentTranslationXY, dragXY, gestureState, dragScrollTranslationXY, scrollViewDragScrollTranslationXY, setDragDirection, setCurrentItemDragCenterXY, horizontalProp]);
const panGestureHandlerWithPropOptions = useMemo(() => {
if (typeof panActivateAfterLongPress === 'number') {
panGestureHandler.activateAfterLongPress(panActivateAfterLongPress);
}
if (!panEnabled) {
panGestureHandler.enabled(panEnabled);
}
return panGestureHandler;
}, [panActivateAfterLongPress, panEnabled, panGestureHandler]);
const gestureHandler = useMemo(() => Gesture.Simultaneous(Gesture.Native(), panGestureHandlerWithPropOptions), [panGestureHandlerWithPropOptions]);
const setScrollEnabled = useCallback(enabled => {
currentScrollEnabled.value = enabled;
// IMPORTANT:
// On web setNativeProps API is not available, so disabling scroll is controlled by a state.
// On Android/iOS we can keep using setNativeProps which performs better and doesn't require re-renders.
if (Platform.OS === 'web') {
setForceDisableScroll(!enabled);
if (setScrollViewForceDisableScroll) {
setScrollViewForceDisableScroll(!enabled);
}
} else {
if (!enabled || scrollEnabledProp.value) {
var _flatListRef$current;
// We disable the scroll or when re-enabling the scroll of the container we set it back to the current prop value.
(_flatListRef$current = flatListRef.current) === null || _flatListRef$current === void 0 || _flatListRef$current.setNativeProps({
scrollEnabled: enabled
});
}
if (!enabled || scrollViewScrollEnabledProp !== null && scrollViewScrollEnabledProp !== void 0 && scrollViewScrollEnabledProp.value) {
var _scrollViewContainerR;
// We disable the scroll or when re-enabling the scroll of the container we set it back to the current prop value.
scrollViewContainerRef === null || scrollViewContainerRef === void 0 || (_scrollViewContainerR = scrollViewContainerRef.current) === null || _scrollViewContainerR === void 0 || _scrollViewContainerR.setNativeProps({
scrollEnabled: enabled
});
}
}
}, [currentScrollEnabled, flatListRef, scrollEnabledProp, scrollViewContainerRef, scrollViewScrollEnabledProp, setScrollViewForceDisableScroll]);
const resetSharedValues = useCallback(() => {
'worklet';
state.value = ReorderableListState.IDLE;
draggedIndex.value = -1;
dragXY.value = 0;
dragScrollTranslationXY.value = 0;
scrollViewDragScrollTranslationXY.value = 0;
dragDirection.value = 0;
lastDragDirectionPivot.value = null;
currentItemDragCenterXY.value = null;
}, [state, draggedIndex, dragXY, dragScrollTranslationXY, scrollViewDragScrollTranslationXY, dragDirection, lastDragDirectionPivot, currentItemDragCenterXY]);
const resetSharedValuesAfterAnimations = useCallback(() => {
setTimeout(runOnUI(resetSharedValues), animationDurationProp.value);
}, [resetSharedValues, animationDurationProp]);
const markCells = (fromIndex, toIndex) => {
if (!markedCellsRef.current) {
markedCellsRef.current = new Map();
}
const start = Math.min(fromIndex, toIndex);
const end = Math.max(fromIndex, toIndex);
for (let i = start; i <= end; i++) {
var _keyExtractorPropRef$2;
const cellKey = ((_keyExtractorPropRef$2 = keyExtractorPropRef.current) === null || _keyExtractorPropRef$2 === void 0 ? void 0 : _keyExtractorPropRef$2.call(keyExtractorPropRef, data[i], i)) || i.toString();
if (!markedCellsRef.current.has(cellKey)) {
markedCellsRef.current.set(cellKey, 1);
} else {
markedCellsRef.current.delete(cellKey);
}
}
};
const reorder = (fromIndex, toIndex) => {
runOnUI(resetSharedValues)();
if (fromIndex !== toIndex) {
markCells(fromIndex, toIndex);
onReorder({
from: fromIndex,
to: toIndex
});
}
};
const recomputeLayout = useCallback((from, to) => {
'worklet';
const itemDirection = to > from;
const index1 = itemDirection ? from : to;
const index2 = itemDirection ? to : from;
const newOffset1 = itemOffset.value[index1];
const newSize1 = itemSize.value[index2];
const newOffset2 = itemOffset.value[index2] + itemSize.value[index2] - itemSize.value[index1];
const newSize2 = itemSize.value[index1];
itemOffset.value[index1] = newOffset1;
itemSize.value[index1] = newSize1;
itemOffset.value[index2] = newOffset2;
itemSize.value[index2] = newSize2;
}, [itemOffset, itemSize]);
/**
* Computes a potential new drop container for the current dragged item and evaluates
* whether the dragged item center is nearer to the center of the current container or the new one.
*
* @returns The new index if the center of the dragged item is closer to the center of
* the new drop container or the current index if closer to the current drop container.
*/
const computeCurrentIndex = useCallback(() => {
'worklet';
if (currentItemDragCenterXY.value === null) {
return currentIndex.value;
}
// Apply scroll offset and scroll container translation.
const relativeDragCenterXY = flatListScrollOffsetXY.value + scrollViewDragScrollTranslationXY.value + currentItemDragCenterXY.value;
const currentOffset = itemOffset.value[currentIndex.value];
const currentSize = itemSize.value[currentIndex.value];
const currentCenter = currentOffset + currentSize * 0.5;
const max = itemCount.value;
const possibleIndex = relativeDragCenterXY < currentCenter ? Math.max(0, currentIndex.value - 1) : Math.min(max - 1, currentIndex.value + 1);
if (currentIndex.value !== possibleIndex) {
let possibleOffset = itemOffset.value[possibleIndex];
if (possibleIndex > currentIndex.value) {
possibleOffset += itemSize.value[possibleIndex] - currentSize;
}
const possibleCenter = possibleOffset + currentSize * 0.5;
const distanceFromCurrent = Math.abs(relativeDragCenterXY - currentCenter);
const distanceFromPossible = Math.abs(relativeDragCenterXY - possibleCenter);
return distanceFromCurrent <= distanceFromPossible ? currentIndex.value : possibleIndex;
}
return currentIndex.value;
}, [currentIndex, currentItemDragCenterXY, itemCount, itemOffset, itemSize, flatListScrollOffsetXY, scrollViewDragScrollTranslationXY]);
const setCurrentIndex = useCallback(() => {
'worklet';
const newIndex = computeCurrentIndex();
if (currentIndex.value !== newIndex) {
recomputeLayout(currentIndex.value, newIndex);
currentIndex.value = newIndex;
onIndexChange === null || onIndexChange === void 0 || onIndexChange({
index: newIndex
});
}
}, [currentIndex, computeCurrentIndex, recomputeLayout, onIndexChange]);
const runDefaultDragAnimations = useCallback(type => {
'worklet';
// if no custom scale run default
if (!(cellAnimations && 'transform' in cellAnimations)) {
const scaleConfig = SCALE_ANIMATION_CONFIG_DEFAULT[type];
scaleDefault.value = withTiming(scaleConfig.toValue, scaleConfig);
}
// If no custom opacity run the default.
if (!(cellAnimations && 'opacity' in cellAnimations)) {
const opacityConfig = OPACITY_ANIMATION_CONFIG_DEFAULT[type];
opacityDefault.value = withTiming(opacityConfig.toValue, opacityConfig);
}
}, [cellAnimations, scaleDefault, opacityDefault]);
useAnimatedReaction(() => gestureState.value, () => {
if (gestureState.value !== State.ACTIVE && gestureState.value !== State.BEGAN && (state.value === ReorderableListState.DRAGGED || state.value === ReorderableListState.AUTOSCROLL)) {
state.value = ReorderableListState.RELEASED;
// enable back scroll on releasing
runOnJS(setScrollEnabled)(true);
if (shouldUpdateActiveItem) {
runOnJS(setActiveIndex)(-1);
}
// Trigger onDragEnd event.
let e = {
from: draggedIndex.value,
to: currentIndex.value
};
onDragEnd === null || onDragEnd === void 0 || onDragEnd(e);
const handlers = dragEndHandlers.value[draggedIndex.value];
if (Array.isArray(handlers)) {
handlers.forEach(fn => fn(e.from, e.to));
}
// they are actually swapped on drag translation
const currentItemOffset = itemOffset.value[draggedIndex.value];
const currentItemSize = itemSize.value[draggedIndex.value];
const draggedItemOffset = itemOffset.value[currentIndex.value];
const draggedItemSize = itemSize.value[currentIndex.value];
const newPositionXY = currentIndex.value > draggedIndex.value ? draggedItemOffset - currentItemOffset : draggedItemOffset - currentItemOffset + (draggedItemSize - currentItemSize);
runDefaultDragAnimations('end');
if (dragXY.value !== newPositionXY) {
// Animate dragged item to its new position on release.
dragXY.value = withTiming(newPositionXY, {
duration: animationDurationProp.value,
easing: Easing.out(Easing.ease)
}, () => {
runOnJS(reorder)(draggedIndex.value, currentIndex.value);
});
} else {
// User might drag and release the item without moving it so,
// since the animation end callback is not executed in that case
// we need to reset values as the reorder function would do.
runOnJS(resetSharedValuesAfterAnimations)();
}
}
});
const computeHiddenArea = useCallback(() => {
'worklet';
if (!scrollViewScrollOffsetXY || !scrollViewSize) {
return {
start: 0,
end: 0
};
}
// hidden area cannot be negative
const start = Math.max(0, scrollViewScrollOffsetXY.value - nestedFlatListPositionXY.value);
const end = Math.max(0, nestedFlatListPositionXY.value + flatListSize.value - (scrollViewScrollOffsetXY.value + scrollViewSize.value));
return {
start,
end
};
}, [scrollViewScrollOffsetXY, scrollViewSize, nestedFlatListPositionXY, flatListSize]);
const computeThresholdArea = useCallback(() => {
'worklet';
const hiddenArea = computeHiddenArea();
const offsetStart = Math.max(0, (autoscrollThresholdOffset === null || autoscrollThresholdOffset === void 0 ? void 0 : autoscrollThresholdOffset.start) || (autoscrollThresholdOffset === null || autoscrollThresholdOffset === void 0 ? void 0 : autoscrollThresholdOffset.top) || 0);
const offsetEnd = Math.max(0, (autoscrollThresholdOffset === null || autoscrollThresholdOffset === void 0 ? void 0 : autoscrollThresholdOffset.end) || (autoscrollThresholdOffset === null || autoscrollThresholdOffset === void 0 ? void 0 : autoscrollThresholdOffset.bottom) || 0);
const threshold = Math.max(0, Math.min(autoscrollThreshold, 0.4));
const visibleSize = flatListSize.value - (hiddenArea.start + hiddenArea.end) - (offsetStart + offsetEnd);
const area = visibleSize * threshold;
const start = area + offsetStart;
const end = flatListSize.value - area - offsetEnd;
return {
start,
end
};
}, [computeHiddenArea, autoscrollThreshold, autoscrollThresholdOffset, flatListSize]);
const computeContainerThresholdArea = useCallback(() => {
'worklet';
if (!scrollViewSize) {
return {
start: -Infinity,
end: Infinity
};
}
const offsetStart = Math.max(0, (autoscrollThresholdOffset === null || autoscrollThresholdOffset === void 0 ? void 0 : autoscrollThresholdOffset.start) || (autoscrollThresholdOffset === null || autoscrollThresholdOffset === void 0 ? void 0 : autoscrollThresholdOffset.top) || 0);
const offsetEnd = Math.max(0, (autoscrollThresholdOffset === null || autoscrollThresholdOffset === void 0 ? void 0 : autoscrollThresholdOffset.end) || (autoscrollThresholdOffset === null || autoscrollThresholdOffset === void 0 ? void 0 : autoscrollThresholdOffset.bottom) || 0);
const threshold = Math.max(0, Math.min(autoscrollThreshold, 0.4));
const visibleSize = scrollViewSize.value - (offsetStart + offsetEnd);
const area = visibleSize * threshold;
const start = area + offsetStart;
const end = visibleSize - area - offsetEnd;
return {
start,
end
};
}, [autoscrollThreshold, autoscrollThresholdOffset, scrollViewSize]);
const shouldScrollContainer = useCallback(y => {
'worklet';
const containerThresholdArea = computeContainerThresholdArea();
const nestedListHiddenArea = computeHiddenArea();
// We should scroll the container if there's a hidden part of the nested list.
// We might have floating errors like 0.0001 which we should ignore.
return nestedListHiddenArea.start > 0.01 && y <= containerThresholdArea.start || nestedListHiddenArea.end > 0.01 && y >= containerThresholdArea.end;
}, [computeHiddenArea, computeContainerThresholdArea]);
const getRelativeContainerXY = useCallback(() => {
'worklet';
return currentXY.value + nestedFlatListPositionXY.value - scrollViewDragInitialScrollOffsetXY.value;
}, [currentXY, nestedFlatListPositionXY, scrollViewDragInitialScrollOffsetXY]);
const getRelativeListXY = useCallback(() => {
'worklet';
return currentXY.value + scrollViewDragScrollTranslationXY.value;
}, [currentXY, scrollViewDragScrollTranslationXY]);
const scrollDirection = useCallback(() => {
'worklet';
const relativeContainerXY = getRelativeContainerXY();
if (shouldScrollContainer(relativeContainerXY)) {
const containerThresholdArea = computeContainerThresholdArea();
if (relativeContainerXY <= containerThresholdArea.start) {
return -1;
}
if (relativeContainerXY >= containerThresholdArea.end) {
return 1;
}
} else if (scrollable) {
const relativeListXY = getRelativeListXY();
const thresholdArea = computeThresholdArea();
if (relativeListXY <= thresholdArea.start) {
return -1;
}
if (relativeListXY >= thresholdArea.end) {
return 1;
}
}
return 0;
}, [shouldScrollContainer, computeThresholdArea, computeContainerThresholdArea, getRelativeContainerXY, getRelativeListXY, scrollable]);
useAnimatedReaction(() => currentXY.value, () => {
if (state.value === ReorderableListState.DRAGGED || state.value === ReorderableListState.AUTOSCROLL) {
setCurrentIndex();
// Trigger autoscroll when:
// 1. Within the threshold area (start or end of list)
// 2. Have dragged in the same direction as the scroll
// 3. Not already in autoscroll mode
if (dragDirection.value === scrollDirection()) {
// When the first two conditions are met and it's already in autoscroll mode,
// we let it continue (no-op).
if (state.value !== ReorderableListState.AUTOSCROLL) {
state.value = ReorderableListState.AUTOSCROLL;
lastAutoscrollTrigger.value = autoscrollTrigger.value;
autoscrollTrigger.value *= -1;
}
} else if (state.value === ReorderableListState.AUTOSCROLL) {
state.value = ReorderableListState.DRAGGED;
}
}
});
useAnimatedReaction(() => autoscrollTrigger.value, () => {
if (autoscrollTrigger.value !== lastAutoscrollTrigger.value && state.value === ReorderableListState.AUTOSCROLL) {
const autoscrollIncrement = dragDirection.value * AUTOSCROLL_CONFIG.increment * autoscrollSpeedScale;
if (autoscrollIncrement !== 0) {
let scrollOffset = flatListScrollOffsetXY.value;
let listRef = flatListRef;
// Checking on every autoscroll whether to scroll the container,
// this allows to smoothly pass the scroll from the container to the nested list
// without any gesture input.
if (scrollViewScrollOffsetXY && shouldScrollContainer(getRelativeContainerXY())) {
scrollOffset = scrollViewScrollOffsetXY.value;
listRef = scrollViewContainerRef;
}
const scrollToValue = scrollOffset + autoscrollIncrement;
scrollTo(listRef, horizontalProp.value ? scrollToValue : 0, horizontalProp.value ? 0 : scrollToValue, true);
}
// when autoscrolling user may not be moving his finger so we need
// to update the current position of the dragged item here
setCurrentIndex();
}
});
// flatlist scroll handler
const handleScroll = useAnimatedScrollHandler(e => {
flatListScrollOffsetXY.value = horizontalProp.value ? e.contentOffset.x : e.contentOffset.y;
// Checking if the list is not scrollable instead of the scrolling state.
// Fixes a bug on iOS where the item is shifted after autoscrolling and then
// moving away from the area.
if (!currentScrollEnabled.value) {
dragScrollTranslationXY.value = flatListScrollOffsetXY.value - dragInitialScrollOffsetXY.value;
}
if (state.value === ReorderableListState.AUTOSCROLL) {
dragXY.value = currentTranslationXY.value + dragScrollTranslationXY.value + scrollViewDragScrollTranslationXY.value;
lastAutoscrollTrigger.value = autoscrollTrigger.value;
autoscrollTrigger.value = withDelay(autoscrollDelay, withTiming(autoscrollTrigger.value * -1, {
duration: 0
}));
}
});
// container scroll handler
useAnimatedReaction(() => scrollViewScrollOffsetXY === null || scrollViewScrollOffsetXY === void 0 ? void 0 : scrollViewScrollOffsetXY.value, value => {
if (value) {
// Checking if the list is not scrollable instead of the scrolling state.
// Fixes a bug on iOS where the item is shifted, after autoscrolling and then
// moving away from the area.
if (!currentScrollEnabled.value) {
scrollViewDragScrollTranslationXY.value = value - scrollViewDragInitialScrollOffsetXY.value;
}
if (state.value === ReorderableListState.AUTOSCROLL) {
dragXY.value = currentTranslationXY.value + scrollViewDragScrollTranslationXY.value;
lastAutoscrollTrigger.value = autoscrollTrigger.value;
autoscrollTrigger.value = withDelay(autoscrollDelay, withTiming(autoscrollTrigger.value * -1, {
duration: 0
}));
}
}
});
const startDrag = useCallback(index => {
'worklet';
if (!dragEnabledProp.value) {
return;
}
// Allow new drag when item is completely released.
if (state.value === ReorderableListState.IDLE) {
// Resetting shared values again fixes a flickeing bug in nested lists where
// after scrolling the parent list it would offset the new dragged item in another nested list.
resetSharedValues();
if (shouldUpdateActiveItem) {
runOnJS(setActiveIndex)(index);
}
dragInitialScrollOffsetXY.value = flatListScrollOffsetXY.value;
scrollViewDragInitialScrollOffsetXY.value = (scrollViewScrollOffsetXY === null || scrollViewScrollOffsetXY === void 0 ? void 0 : scrollViewScrollOffsetXY.value) || 0;
draggedSize.value = itemSize.value[index];
draggedIndex.value = index;
currentIndex.value = index;
state.value = ReorderableListState.DRAGGED;
runOnJS(setScrollEnabled)(false);
// run animation before onDragStart to avoid potentially waiting for it
runDefaultDragAnimations('start');
onDragStart === null || onDragStart === void 0 || onDragStart({
index
});
}
}, [dragEnabledProp, resetSharedValues, shouldUpdateActiveItem, dragInitialScrollOffsetXY, scrollViewScrollOffsetXY, scrollViewDragInitialScrollOffsetXY, setScrollEnabled, currentIndex, draggedSize, draggedIndex, state, flatListScrollOffsetXY, itemSize, onDragStart, runDefaultDragAnimations]);
const handleFlatListLayout = useCallback(e => {
flatListSize.value = horizontalProp.value ? e.nativeEvent.layout.width : e.nativeEvent.layout.height;
// If nested in a scroll container.
if (scrollViewScrollOffsetXY) {
// Timeout fixes a bug where measure returns width or height 0.
setTimeout(() => {
runOnUI(() => {
const measurement = measure(flatListRef);
if (!measurement) {
return;
}
const pageXY = horizontalProp.value ? measurement.pageX : measurement.pageY;
// We need to use pageY because the list might be nested into other views,
// It's important that we take the measurement of the list without any scroll offset
// from the scroll container.
flatListPageXY.value = pageXY + ((scrollViewScrollOffsetXY === null || scrollViewScrollOffsetXY === void 0 ? void 0 : scrollViewScrollOffsetXY.value) || 0);
})();
}, 100);
}
onLayout === null || onLayout === void 0 || onLayout(e);
}, [flatListRef, flatListPageXY, flatListSize, horizontalProp, scrollViewScrollOffsetXY, onLayout]);
const handleRef = useCallback(value => {
flatListRef(value);
if (typeof ref === 'function') {
ref(value);
} else if (ref) {
ref.current = value;
}
}, [flatListRef, ref]);
const combinedGesture = useMemo(() => {
// Android is able to handle nested scroll view, but not the full size ones like iOS.
if (outerScrollGesture && !(Platform.OS === 'android' && scrollable)) {
return Gesture.Simultaneous(outerScrollGesture, gestureHandler);
}
return gestureHandler;
}, [scrollable, outerScrollGesture, gestureHandler]);
const composedScrollHandler = useComposedEventHandler([handleScroll, onScroll || null]);
const renderAnimatedCell = useStableCallback(({
cellKey,
...props
}) => /*#__PURE__*/React.createElement(ReorderableListCell, _extends({}, props, {
// forces remount with key change on reorder
key: createCellKey(cellKey),
itemOffset: itemOffset,
itemSize: itemSize,
dragXY: dragXY,
draggedIndex: draggedIndex,
animationDuration: animationDurationProp,
startDrag: startDrag
})));
return /*#__PURE__*/React.createElement(ReorderableListContext.Provider, {
value: listContextValue
}, /*#__PURE__*/React.createElement(GestureDetector, {
gesture: combinedGesture
}, /*#__PURE__*/React.createElement(AnimatedFlatList, _extends({}, rest, {
ref: handleRef,
data: data,
keyExtractor: keyExtractor,
CellRendererComponent: renderAnimatedCell,
onLayout: handleFlatListLayout,
onScroll: composedScrollHandler,
scrollEventThrottle: 1,
removeClippedSubviews: false,
numColumns: 1
// We force disable scroll or let the component prop control it.
,
scrollEnabled: forceDisableScroll ? false : scrollEnabled
}))));
};
const MemoizedReorderableListCore = /*#__PURE__*/React.memo(/*#__PURE__*/React.forwardRef(ReorderableListCore));
export { MemoizedReorderableListCore as ReorderableListCore };
//# sourceMappingURL=ReorderableListCore.js.map