Skip to content

Commit

Permalink
fix: use a more stable drag end handling
Browse files Browse the repository at this point in the history
Instead of triggering the animated reaction through a shared value update
we save the handler in an array and call it when the item is released.
This way we remove hacks to avoid reanimated batching.
  • Loading branch information
omahili committed Dec 12, 2024
1 parent beda466 commit 1035e08
Show file tree
Hide file tree
Showing 6 changed files with 44 additions and 39 deletions.
12 changes: 1 addition & 11 deletions src/components/ReorderableList/ReorderableList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ const ReorderableList = <T,>(
itemHeight,
dragY,
draggedIndex,
releasedIndex,
duration,
} = useReorderableList({
ref,
Expand All @@ -70,20 +69,11 @@ const ReorderableList = <T,>(
itemHeight={itemHeight}
dragY={dragY}
draggedIndex={draggedIndex}
releasedIndex={releasedIndex}
animationDuration={duration}
startDrag={startDrag}
/>
),
[
itemOffset,
itemHeight,
dragY,
draggedIndex,
releasedIndex,
duration,
startDrag,
],
[itemOffset, itemHeight, dragY, draggedIndex, duration, startDrag],
);

return (
Expand Down
24 changes: 13 additions & 11 deletions src/components/ReorderableList/useReorderableList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,10 @@ export const useReorderableList = <T>({
const previousIndex = useSharedValue(-1);
const currentIndex = useSharedValue(-1);
const draggedIndex = useSharedValue(-1);
const releasedIndex = useSharedValue(-1);
const state = useSharedValue<ReorderableListState>(ReorderableListState.IDLE);
const dragEndHandlers = useSharedValue<
((from: number, to: number) => void)[][]
>([]);

// animation duration as a shared value allows to avoid re-rendering of all cells on value change
const duration = useSharedValue(animationDuration);
Expand All @@ -93,8 +95,9 @@ export const useReorderableList = <T>({
draggedHeight,
currentIndex,
draggedIndex,
dragEndHandlers,
}),
[draggedHeight, currentIndex, draggedIndex],
[draggedHeight, currentIndex, draggedIndex, dragEndHandlers],
);

const startY = useSharedValue(0);
Expand Down Expand Up @@ -157,18 +160,13 @@ export const useReorderableList = <T>({
const resetSharedValues = useCallback(() => {
'worklet';

// must be reset before the reorder function is called
// to avoid triggering on drag end event twice,
// update with a delay to avoid reanimated batching the
// change if updated immediately
releasedIndex.value = withDelay(1, withTiming(-1, {duration: 0}));
draggedIndex.value = -1;
// current index is reset on item render for the on end event
dragY.value = 0;
// released flag is reset after release is triggered in the item
state.value = ReorderableListState.IDLE;
dragScrollTranslationY.value = 0;
}, [releasedIndex, draggedIndex, dragY, state, dragScrollTranslationY]);
}, [draggedIndex, dragY, state, dragScrollTranslationY]);

const reorder = (fromIndex: number, toIndex: number) => {
runOnUI(resetSharedValues)();
Expand Down Expand Up @@ -283,12 +281,17 @@ export const useReorderableList = <T>({
state.value === ReorderableListState.AUTO_SCROLL)
) {
state.value = ReorderableListState.RELEASING;
releasedIndex.value = draggedIndex.value;

// enable back scroll on releasing
runOnJS(setScrollEnabled)(true);
// trigger onDragEnd event
onDragEnd?.({from: draggedIndex.value, to: currentIndex.value});
let e = {from: draggedIndex.value, to: currentIndex.value};
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];
Expand Down Expand Up @@ -488,7 +491,6 @@ export const useReorderableList = <T>({
itemOffset,
itemHeight,
draggedIndex,
releasedIndex,
dragY,
duration,
};
Expand Down
5 changes: 1 addition & 4 deletions src/components/ReorderableListCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ interface ReorderableListCellProps<T>
itemHeight: SharedValue<number[]>;
dragY: SharedValue<number>;
draggedIndex: SharedValue<number>;
releasedIndex: SharedValue<number>;
// animation duration as a shared value allows to avoid re-renders on value change
animationDuration: SharedValue<number>;
}
Expand All @@ -36,7 +35,6 @@ export const ReorderableListCell = memo(
itemHeight,
dragY,
draggedIndex,
releasedIndex,
animationDuration,
}: ReorderableListCellProps<T>) => {
const dragHandler = useCallback(() => {
Expand All @@ -50,9 +48,8 @@ export const ReorderableListCell = memo(
index,
dragHandler,
draggedIndex,
releasedIndex,
}),
[index, dragHandler, draggedIndex, releasedIndex],
[index, dragHandler, draggedIndex],
);
const {currentIndex, draggedHeight} = useContext(ReorderableListContext);

Expand Down
1 change: 0 additions & 1 deletion src/contexts/ReorderableCellContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ interface ReorderableCellContextData {
index: number;
dragHandler: () => void;
draggedIndex: SharedValue<number>;
releasedIndex: SharedValue<number>;
}

export const ReorderableCellContext = React.createContext<
Expand Down
1 change: 1 addition & 0 deletions src/contexts/ReorderableListContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {SharedValue} from 'react-native-reanimated';
interface ReorderableListContextData {
currentIndex: SharedValue<number>;
draggedHeight: SharedValue<number>;
dragEndHandlers: SharedValue<((from: number, to: number) => void)[][]>;
}

export const ReorderableListContext = React.createContext<
Expand Down
40 changes: 28 additions & 12 deletions src/hooks/useReorderableDragEnd.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,37 @@
import {useAnimatedReaction} from 'react-native-reanimated';
import {useEffect} from 'react';

import {useContext} from './useContext';
import {ReorderableCellContext, ReorderableListContext} from '../contexts';

export const useReorderableDragEnd = (
onEnd: (from: number, to: number) => void,
) => {
const {currentIndex} = useContext(ReorderableListContext);
const {releasedIndex, index} = useContext(ReorderableCellContext);

useAnimatedReaction(
() => releasedIndex.value === index,
newValue => {
if (newValue) {
onEnd(index, currentIndex.value);
const {dragEndHandlers} = useContext(ReorderableListContext);
const {index} = useContext(ReorderableCellContext);

useEffect(() => {
dragEndHandlers.modify(value => {
'worklet';

if (!Array.isArray(value[index])) {
value[index] = [];
}
},
[onEnd],
);

value[index].push(onEnd);

return value;
});

return () => {
dragEndHandlers.modify(value => {
'worklet';

if (Array.isArray(value[index])) {
value[index] = value[index].filter(x => x.name !== onEnd.name);
}

return value;
});
};
}, [index, dragEndHandlers, onEnd]);
};

0 comments on commit 1035e08

Please sign in to comment.