Skip to content

Commit f31a7d2

Browse files
committed
feat(provider): switch to absolute coordinates to allow more flexible layouts
1 parent ea0274c commit f31a7d2

File tree

4 files changed

+141
-85
lines changed

4 files changed

+141
-85
lines changed

src/DndProvider.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ export const DndProvider = forwardRef<DndProviderHandle, PropsWithChildren<DndPr
216216

217217
const panGesture = Gesture.Pan()
218218
.onBegin((event) => {
219-
const { state, x, y } = event;
219+
const { state, absoluteX: x, absoluteY: y } = event;
220220
debug && console.log("begin", { state, x, y });
221221
// Gesture is globally disabled
222222
if (disabled) {

src/hooks/useDraggable.ts

Lines changed: 40 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,17 @@
22

33
import { useLayoutEffect } from "react";
44
import { LayoutRectangle, ViewProps } from "react-native";
5-
import { measure, runOnUI, useAnimatedRef, useSharedValue } from "react-native-reanimated";
5+
import {
6+
measure,
7+
runOnUI,
8+
useAnimatedReaction,
9+
useAnimatedRef,
10+
useSharedValue,
11+
} from "react-native-reanimated";
612
import { DraggableState, useDndContext } from "../DndContext";
713
import { useLatestSharedValue } from "../hooks";
814
import { Data, UniqueIdentifier } from "../types";
9-
import { getLayoutFromMeasurement, isReanimatedSharedValue } from "../utils";
15+
import { getLayoutFromMeasurement, isReanimatedSharedValue, updateLayoutValue, waitForLayout } from "../utils";
1016
import { useSharedPoint } from "./useSharedPoint";
1117

1218
export type DraggableConstraints = {
@@ -81,28 +87,24 @@ export const useDraggable = ({
8187
const runLayoutEffect = () => {
8288
"worklet";
8389
// Wait for the layout to be available by requesting two consecutive animation frames
84-
requestAnimationFrame(() => {
85-
requestAnimationFrame(() => {
86-
draggableLayouts.value[id] = layout;
87-
// Try to recover the layout from the ref if it's not available yet
88-
if (layout.value.width === 0 || layout.value.height === 0) {
89-
const measurement = measure(animatedRef);
90-
if (measurement !== null) {
91-
layout.value = getLayoutFromMeasurement(measurement);
92-
}
93-
}
94-
draggableOffsets.value[id] = offset;
95-
draggableRestingOffsets.value[id] = restingOffset;
96-
draggableOptions.value[id] = {
97-
id,
98-
data: sharedData,
99-
disabled,
100-
activationDelay,
101-
activationTolerance,
102-
};
103-
draggableStates.value[id] = state;
104-
draggableIds.value = [...draggableIds.value, id];
105-
});
90+
waitForLayout(() => {
91+
// Try to recover the layout from the ref if it's not available yet
92+
if (layout.value.width === 0 || layout.value.height === 0) {
93+
// console.log(`Recovering layout for ${id} from ref`);
94+
updateLayoutValue(layout, animatedRef);
95+
}
96+
draggableLayouts.value[id] = layout;
97+
draggableOffsets.value[id] = offset;
98+
draggableRestingOffsets.value[id] = restingOffset;
99+
draggableStates.value[id] = state;
100+
draggableIds.value = [...draggableIds.value, id];
101+
draggableOptions.value[id] = {
102+
id,
103+
data: sharedData,
104+
disabled,
105+
activationDelay,
106+
activationTolerance,
107+
};
106108
});
107109
};
108110

@@ -120,35 +122,27 @@ export const useDraggable = ({
120122
draggableIds.value = draggableIds.value.filter((draggableId) => draggableId !== id);
121123
});
122124
};
123-
// if(node && node.key === key)
124125
runOnUI(cleanupLayoutEffect)();
125126
};
126127
// eslint-disable-next-line react-hooks/exhaustive-deps
127128
}, [id]);
128129

129-
const onLayout: ViewProps["onLayout"] = () => {
130-
// console.log(`onLayout: ${id}`);
131-
runOnUI(() => {
132-
const measurement = measure(animatedRef);
133-
if (measurement === null) {
134-
return;
135-
}
136-
layout.value = getLayoutFromMeasurement(measurement);
137-
})();
130+
const onLayout: ViewProps["onLayout"] = (_event) => {
131+
// console.log(`onLayout: ${id}`, event.nativeEvent.layout);
132+
runOnUI(updateLayoutValue)(layout, animatedRef);
138133
};
139134

140-
// const setDisabled = useCallback(
141-
// (disabled: boolean) => {
142-
// const updateDisabled = () => {
143-
// "worklet";
144-
// console.log("disabled", disabled);
145-
// draggableOptions.value[id] = { ...draggableOptions.value[id], disabled };
146-
// };
147-
// runOnUI(updateDisabled)();
148-
// },
149-
// // eslint-disable-next-line react-hooks/exhaustive-deps
150-
// [id]
151-
// );
135+
// Track disabled prop changes
136+
useAnimatedReaction(
137+
() => disabled,
138+
(next, prev) => {
139+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
140+
if (next !== prev && draggableOptions.value[id]) {
141+
draggableOptions.value[id].disabled = disabled;
142+
}
143+
},
144+
[disabled],
145+
);
152146

153147
return {
154148
offset,

src/hooks/useDroppable.ts

Lines changed: 41 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22

33
import { useLayoutEffect } from "react";
44
import { type LayoutRectangle, type ViewProps } from "react-native";
5-
import { runOnUI, useAnimatedReaction, useSharedValue } from "react-native-reanimated";
5+
import { runOnUI, useAnimatedReaction, useAnimatedRef, useSharedValue } from "react-native-reanimated";
66
import { useDndContext } from "../DndContext";
7-
import { useLatestSharedValue, useNodeRef } from "../hooks";
8-
import type { Data, NativeElement, UniqueIdentifier } from "../types";
9-
import { assert, isReanimatedSharedValue } from "../utils";
7+
import { useLatestSharedValue } from "../hooks";
8+
import type { Data, UniqueIdentifier } from "../types";
9+
import { isReanimatedSharedValue, updateLayoutValue, waitForLayout } from "../utils";
1010

1111
export type UseDroppableOptions = { id: UniqueIdentifier; data?: Data; disabled?: boolean };
1212

@@ -29,11 +29,8 @@ export type UseDroppableOptions = { id: UniqueIdentifier; data?: Data; disabled?
2929
* @property {object} panGestureState - An object representing the current state of the draggable component within the context.
3030
*/
3131
export const useDroppable = ({ id, data = {}, disabled = false }: UseDroppableOptions) => {
32-
const { droppableLayouts, droppableOptions, droppableActiveId, containerRef, panGestureState } =
33-
useDndContext();
34-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
35-
const [node, setNodeRef] = useNodeRef<NativeElement, any>();
36-
// ^?
32+
const { droppableLayouts, droppableOptions, droppableActiveId, panGestureState } = useDndContext();
33+
const animatedRef = useAnimatedRef();
3734
// eslint-disable-next-line react-hooks/rules-of-hooks
3835
const sharedData = isReanimatedSharedValue(data) ? data : useLatestSharedValue(data);
3936

@@ -44,41 +41,52 @@ export const useDroppable = ({ id, data = {}, disabled = false }: UseDroppableOp
4441
height: 0,
4542
});
4643

47-
useAnimatedReaction(
48-
() => disabled,
49-
(next, prev) => {
50-
if (next !== prev) {
51-
droppableOptions.value[id].disabled = disabled;
52-
}
53-
},
54-
[disabled],
55-
);
56-
5744
useLayoutEffect(() => {
5845
const runLayoutEffect = () => {
5946
"worklet";
60-
droppableLayouts.value[id] = layout;
61-
droppableOptions.value[id] = { id, data: sharedData, disabled };
47+
waitForLayout(() => {
48+
// Try to recover the layout from the ref if it's not available yet
49+
if (layout.value.width === 0 || layout.value.height === 0) {
50+
console.log(`Recovering layout for ${id} from ref`);
51+
updateLayoutValue(layout, animatedRef);
52+
}
53+
droppableLayouts.value[id] = layout;
54+
// Options
55+
droppableOptions.value[id] = { id, data: sharedData, disabled };
56+
});
6257
};
58+
6359
runOnUI(runLayoutEffect)();
60+
6461
return () => {
65-
const runLayoutEffect = () => {
62+
const cleanupLayoutEffect = () => {
6663
"worklet";
67-
delete droppableLayouts.value[id];
68-
delete droppableOptions.value[id];
64+
requestAnimationFrame(() => {
65+
delete droppableLayouts.value[id];
66+
delete droppableOptions.value[id];
67+
});
6968
};
70-
// if(node && node.key === key)
71-
runOnUI(runLayoutEffect)();
69+
runOnUI(cleanupLayoutEffect)();
7270
};
7371
// eslint-disable-next-line react-hooks/exhaustive-deps
74-
}, []);
72+
}, [id]);
7573

76-
const onLayout: ViewProps["onLayout"] = () => {
77-
assert(containerRef.current);
78-
node.current?.measureLayout(containerRef.current, (x, y, width, height) => {
79-
layout.value = { x, y, width, height };
80-
});
74+
const onLayout: ViewProps["onLayout"] = (_event) => {
75+
// console.log(`onLayout: ${id}`, event.nativeEvent.layout);
76+
updateLayoutValue(layout, animatedRef);
8177
};
8278

83-
return { setNodeRef, setNodeLayout: onLayout, activeId: droppableActiveId, panGestureState };
79+
// Track disabled prop changes
80+
useAnimatedReaction(
81+
() => disabled,
82+
(next, prev) => {
83+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
84+
if (next !== prev && droppableOptions.value[id]) {
85+
droppableOptions.value[id].disabled = disabled;
86+
}
87+
},
88+
[disabled],
89+
);
90+
91+
return { animatedRef, setNodeLayout: onLayout, activeId: droppableActiveId, panGestureState };
8492
};

src/utils/reanimated.ts

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1-
import { LayoutRectangle } from "react-native";
1+
import { type Component } from "react";
2+
import { type LayoutRectangle } from "react-native";
23
import {
3-
MeasuredDimensions,
4-
SharedValue,
4+
measure,
5+
runOnUI,
56
withSpring,
67
type AnimatableValue,
8+
type AnimatedRef,
79
type AnimationCallback,
10+
type MeasuredDimensions,
11+
type SharedValue,
812
type WithSpringConfig,
913
} from "react-native-reanimated";
1014
import type { SharedPoint } from "../hooks";
@@ -148,9 +152,59 @@ export const isReanimatedSharedValue = (value: unknown): value is SharedValue<An
148152
export const getLayoutFromMeasurement = (measurement: MeasuredDimensions): LayoutRectangle => {
149153
"worklet";
150154
return {
151-
x: measurement.x,
152-
y: measurement.y,
155+
x: measurement.pageX,
156+
y: measurement.pageY,
153157
width: measurement.width,
154158
height: measurement.height,
155159
};
156160
};
161+
162+
export const updateLayoutValue = (
163+
layout: SharedValue<LayoutRectangle>,
164+
animatedRef: AnimatedRef<Component>,
165+
) => {
166+
"worklet";
167+
const measurement = measure(animatedRef);
168+
if (measurement === null) {
169+
return;
170+
}
171+
layout.value = getLayoutFromMeasurement(measurement);
172+
};
173+
174+
export const waitForLayout = (fn: (lastTime: number, time: number) => void) => {
175+
"worklet";
176+
let lastTime = 0;
177+
178+
function loop() {
179+
requestAnimationFrame((time) => {
180+
if (lastTime > 0) {
181+
fn(lastTime, time);
182+
return;
183+
}
184+
lastTime = time;
185+
requestAnimationFrame(loop);
186+
});
187+
}
188+
189+
loop();
190+
};
191+
192+
/*
193+
194+
function loopAnimationFrame(fn: (lastTime: number, time: number) => void) {
195+
let lastTime = 0;
196+
197+
function loop() {
198+
requestAnimationFrame((time) => {
199+
if (lastTime > 0) {
200+
fn(lastTime, time);
201+
}
202+
lastTime = time;
203+
requestAnimationFrame(loop);
204+
});
205+
}
206+
207+
loop();
208+
}
209+
210+
*/

0 commit comments

Comments
 (0)