react-native-livechart 3.11.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/BadgeOverlay.d.ts +9 -1
- package/dist/components/BadgeOverlay.d.ts.map +1 -1
- package/dist/components/ChartOverlayLayer.d.ts +19 -0
- package/dist/components/ChartOverlayLayer.d.ts.map +1 -0
- package/dist/components/CustomMarkerOverlay.d.ts +18 -6
- package/dist/components/CustomMarkerOverlay.d.ts.map +1 -1
- package/dist/components/CustomReferenceLineOverlay.d.ts +36 -0
- package/dist/components/CustomReferenceLineOverlay.d.ts.map +1 -0
- package/dist/components/CustomTooltipOverlay.d.ts +5 -2
- package/dist/components/CustomTooltipOverlay.d.ts.map +1 -1
- package/dist/components/DotOverlay.d.ts +7 -1
- package/dist/components/DotOverlay.d.ts.map +1 -1
- package/dist/components/LiveChart.d.ts.map +1 -1
- package/dist/components/LiveChartSeries.d.ts.map +1 -1
- package/dist/components/MarkerOverlay.d.ts +8 -3
- package/dist/components/MarkerOverlay.d.ts.map +1 -1
- package/dist/components/MultiSeriesDots.d.ts +4 -1
- package/dist/components/MultiSeriesDots.d.ts.map +1 -1
- package/dist/components/ReferenceLineGroupOverlay.d.ts +27 -0
- package/dist/components/ReferenceLineGroupOverlay.d.ts.map +1 -0
- package/dist/components/ReferenceLineOverlay.d.ts +25 -2
- package/dist/components/ReferenceLineOverlay.d.ts.map +1 -1
- package/dist/components/XAxisOverlay.d.ts +7 -1
- package/dist/components/XAxisOverlay.d.ts.map +1 -1
- package/dist/components/YAxisOverlay.d.ts +12 -1
- package/dist/components/YAxisOverlay.d.ts.map +1 -1
- package/dist/constants.d.ts +7 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/core/liveChartEngineTick.d.ts +29 -0
- package/dist/core/liveChartEngineTick.d.ts.map +1 -1
- package/dist/core/liveChartSeriesEngineTick.d.ts +18 -0
- package/dist/core/liveChartSeriesEngineTick.d.ts.map +1 -1
- package/dist/core/resolveConfig.d.ts +56 -2
- package/dist/core/resolveConfig.d.ts.map +1 -1
- package/dist/core/useLiveChartEngine.d.ts +58 -7
- package/dist/core/useLiveChartEngine.d.ts.map +1 -1
- package/dist/core/useLiveChartSeriesEngine.d.ts +8 -2
- package/dist/core/useLiveChartSeriesEngine.d.ts.map +1 -1
- package/dist/draw/grid.d.ts.map +1 -1
- package/dist/draw/markerAtlas.d.ts +20 -2
- package/dist/draw/markerAtlas.d.ts.map +1 -1
- package/dist/draw/volume.d.ts +25 -0
- package/dist/draw/volume.d.ts.map +1 -0
- package/dist/hooks/crosshairShared.d.ts +12 -6
- package/dist/hooks/crosshairShared.d.ts.map +1 -1
- package/dist/hooks/overlayScale.d.ts +22 -0
- package/dist/hooks/overlayScale.d.ts.map +1 -0
- package/dist/hooks/resolveChartLayout.d.ts +9 -0
- package/dist/hooks/resolveChartLayout.d.ts.map +1 -1
- package/dist/hooks/useBadge.d.ts +22 -1
- package/dist/hooks/useBadge.d.ts.map +1 -1
- package/dist/hooks/useCandlePaths.d.ts +6 -0
- package/dist/hooks/useCandlePaths.d.ts.map +1 -1
- package/dist/hooks/useChartOverlayContext.d.ts +45 -0
- package/dist/hooks/useChartOverlayContext.d.ts.map +1 -0
- package/dist/hooks/useChartPaths.d.ts +8 -1
- package/dist/hooks/useChartPaths.d.ts.map +1 -1
- package/dist/hooks/useCrosshair.d.ts +9 -2
- package/dist/hooks/useCrosshair.d.ts.map +1 -1
- package/dist/hooks/useLiveDot.d.ts +6 -1
- package/dist/hooks/useLiveDot.d.ts.map +1 -1
- package/dist/hooks/useMarkers.d.ts +12 -4
- package/dist/hooks/useMarkers.d.ts.map +1 -1
- package/dist/hooks/useMultiSeriesLinePaths.d.ts.map +1 -1
- package/dist/hooks/usePanScroll.d.ts +82 -0
- package/dist/hooks/usePanScroll.d.ts.map +1 -0
- package/dist/hooks/usePinchZoom.d.ts +74 -0
- package/dist/hooks/usePinchZoom.d.ts.map +1 -0
- package/dist/hooks/useReferenceDrag.d.ts +24 -0
- package/dist/hooks/useReferenceDrag.d.ts.map +1 -0
- package/dist/hooks/useReferenceLine.d.ts +23 -4
- package/dist/hooks/useReferenceLine.d.ts.map +1 -1
- package/dist/hooks/useReferenceLinePress.d.ts +5 -1
- package/dist/hooks/useReferenceLinePress.d.ts.map +1 -1
- package/dist/hooks/useVisibleRange.d.ts +39 -0
- package/dist/hooks/useVisibleRange.d.ts.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/math/markerCluster.d.ts +47 -0
- package/dist/math/markerCluster.d.ts.map +1 -0
- package/dist/math/markers.d.ts +15 -0
- package/dist/math/markers.d.ts.map +1 -1
- package/dist/math/referenceDrag.d.ts +24 -0
- package/dist/math/referenceDrag.d.ts.map +1 -0
- package/dist/math/referenceGroup.d.ts +31 -0
- package/dist/math/referenceGroup.d.ts.map +1 -0
- package/dist/math/referenceLines.d.ts +33 -7
- package/dist/math/referenceLines.d.ts.map +1 -1
- package/dist/types.d.ts +512 -21
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/BadgeOverlay.tsx +26 -1
- package/src/components/ChartOverlayLayer.tsx +32 -0
- package/src/components/CustomMarkerOverlay.tsx +153 -53
- package/src/components/CustomReferenceLineOverlay.tsx +250 -0
- package/src/components/CustomTooltipOverlay.tsx +27 -7
- package/src/components/DotOverlay.tsx +9 -0
- package/src/components/LiveChart.tsx +561 -34
- package/src/components/LiveChartSeries.tsx +110 -5
- package/src/components/MarkerOverlay.tsx +100 -6
- package/src/components/MultiSeriesDots.tsx +12 -3
- package/src/components/ReferenceLineGroupOverlay.tsx +207 -0
- package/src/components/ReferenceLineOverlay.tsx +105 -32
- package/src/components/XAxisOverlay.tsx +9 -2
- package/src/components/YAxisOverlay.tsx +48 -26
- package/src/constants.ts +10 -0
- package/src/core/liveChartEngineTick.ts +81 -8
- package/src/core/liveChartSeriesEngineTick.ts +50 -9
- package/src/core/resolveConfig.ts +122 -1
- package/src/core/useLiveChartEngine.ts +103 -10
- package/src/core/useLiveChartSeriesEngine.ts +26 -3
- package/src/draw/grid.ts +13 -1
- package/src/draw/markerAtlas.ts +117 -3
- package/src/draw/volume.ts +190 -0
- package/src/hooks/crosshairShared.ts +37 -9
- package/src/hooks/overlayScale.ts +59 -0
- package/src/hooks/resolveChartLayout.ts +33 -0
- package/src/hooks/useBadge.ts +89 -21
- package/src/hooks/useCandlePaths.ts +104 -7
- package/src/hooks/useChartOverlayContext.ts +93 -0
- package/src/hooks/useChartPaths.ts +14 -1
- package/src/hooks/useCrosshair.ts +29 -2
- package/src/hooks/useLiveDot.ts +16 -5
- package/src/hooks/useMarkers.ts +43 -11
- package/src/hooks/useMultiSeriesLinePaths.ts +2 -1
- package/src/hooks/usePanScroll.ts +253 -0
- package/src/hooks/usePinchZoom.ts +189 -0
- package/src/hooks/useReferenceDrag.ts +254 -0
- package/src/hooks/useReferenceLine.ts +56 -9
- package/src/hooks/useReferenceLinePress.ts +16 -2
- package/src/hooks/useVisibleRange.ts +119 -0
- package/src/index.ts +17 -1
- package/src/math/markerCluster.ts +214 -0
- package/src/math/markers.ts +54 -9
- package/src/math/referenceDrag.ts +66 -0
- package/src/math/referenceGroup.ts +65 -0
- package/src/math/referenceLines.ts +76 -19
- package/src/types.ts +532 -21
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { Gesture } from "react-native-gesture-handler";
|
|
2
|
+
import {
|
|
3
|
+
cancelAnimation,
|
|
4
|
+
useSharedValue,
|
|
5
|
+
withDecay,
|
|
6
|
+
type SharedValue,
|
|
7
|
+
} from "react-native-reanimated";
|
|
8
|
+
|
|
9
|
+
import type { ChartPadding } from "../draw/line";
|
|
10
|
+
|
|
11
|
+
/** Minimum height (px) of the bottom "grab the time ruler" band in axis-drag mode. */
|
|
12
|
+
export const AXIS_GRAB_MIN_PX = 44;
|
|
13
|
+
/** Horizontal travel (px) before a one-finger drag commits to scrolling vs. falling through. */
|
|
14
|
+
const AXIS_ACTIVATE_PX = 6;
|
|
15
|
+
/** Vertical travel (px) that fails the one-finger scroll so a parent vertical scroll wins. */
|
|
16
|
+
const HOLD_SCRUB_FAIL_Y_PX = 12;
|
|
17
|
+
|
|
18
|
+
/** Which gesture activates a time-scroll. */
|
|
19
|
+
export type PanScrollGestureMode = "holdToScrub" | "axisDrag";
|
|
20
|
+
|
|
21
|
+
/** Engine SharedValues the pan-scroll gesture reads/writes (subset of the engine state). */
|
|
22
|
+
export interface PanScrollEngineRefs {
|
|
23
|
+
/** Absolute right-edge time to freeze at, or `null` to follow the live edge. */
|
|
24
|
+
viewEnd: SharedValue<number | null>;
|
|
25
|
+
/** Right-edge time the engine would use if following live (advances each frame). */
|
|
26
|
+
liveEdge: SharedValue<number>;
|
|
27
|
+
/** Animating visible window width in seconds. */
|
|
28
|
+
displayWindow: SharedValue<number>;
|
|
29
|
+
/** Canvas width in px. */
|
|
30
|
+
canvasWidth: SharedValue<number>;
|
|
31
|
+
/** Canvas height in px (for the axis-drag band hit-test). */
|
|
32
|
+
canvasHeight: SharedValue<number>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface UsePanScrollOptions {
|
|
36
|
+
engine: PanScrollEngineRefs;
|
|
37
|
+
padding: ChartPadding;
|
|
38
|
+
/**
|
|
39
|
+
* Earliest selectable time (unix seconds) — typically the first data point /
|
|
40
|
+
* candle. Clamps how far back the window can pan. When there's no scrollable
|
|
41
|
+
* history this should equal (or exceed) the live edge so panning is a no-op.
|
|
42
|
+
*/
|
|
43
|
+
minTime: SharedValue<number>;
|
|
44
|
+
/** Master switch. When false the gesture is disabled and the chart follows live. */
|
|
45
|
+
enabled: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Activation model:
|
|
48
|
+
* - `"holdToScrub"` (default): a one-finger drag anywhere scrolls; scrub moves
|
|
49
|
+
* to press-and-hold (Rainbow-style). The caller must give the scrub gesture
|
|
50
|
+
* a long-press delay so a quick drag falls through to this one.
|
|
51
|
+
* - `"axisDrag"`: a one-finger drag starting in the bottom X-axis band ("grab
|
|
52
|
+
* the time ruler"); one-finger plot scrub untouched.
|
|
53
|
+
*/
|
|
54
|
+
mode?: PanScrollGestureMode;
|
|
55
|
+
/**
|
|
56
|
+
* Worklet fired when a scroll drag activates — e.g. to clear the crosshair so
|
|
57
|
+
* a stray scrub doesn't linger behind the pan.
|
|
58
|
+
*/
|
|
59
|
+
onScrollStart?: () => void;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Smallest valid right-edge time: keeps the window's left edge
|
|
64
|
+
* (`rightEdge - window`) from passing `minTime`, and never exceeds the live edge
|
|
65
|
+
* so the `[lo, liveEdge]` clamp range stays valid when history is short.
|
|
66
|
+
*/
|
|
67
|
+
export function panLowerBound(
|
|
68
|
+
minTime: number,
|
|
69
|
+
windowSecs: number,
|
|
70
|
+
liveEdge: number,
|
|
71
|
+
): number {
|
|
72
|
+
"worklet";
|
|
73
|
+
return Math.min(minTime + windowSecs, liveEdge);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Next right-edge time after dragging `changeX` px (drag right ⇒ reveal earlier
|
|
78
|
+
* time ⇒ smaller right edge), clamped to `[lo, liveEdge]`. Returns `null` once
|
|
79
|
+
* the drag reaches the live edge — the signal to resume following.
|
|
80
|
+
*/
|
|
81
|
+
export function nextViewEnd(
|
|
82
|
+
cur: number,
|
|
83
|
+
changeX: number,
|
|
84
|
+
chartW: number,
|
|
85
|
+
windowSecs: number,
|
|
86
|
+
liveEdge: number,
|
|
87
|
+
lo: number,
|
|
88
|
+
): number | null {
|
|
89
|
+
"worklet";
|
|
90
|
+
let next = cur - (changeX / chartW) * windowSecs;
|
|
91
|
+
if (next < lo) next = lo;
|
|
92
|
+
if (next > liveEdge) next = liveEdge;
|
|
93
|
+
return next >= liveEdge ? null : next;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Pan velocity (px/s) → time-seconds/s for `withDecay` (drag right ⇒ earlier). */
|
|
97
|
+
export function flingVelocity(
|
|
98
|
+
velocityX: number,
|
|
99
|
+
chartW: number,
|
|
100
|
+
windowSecs: number,
|
|
101
|
+
): number {
|
|
102
|
+
"worklet";
|
|
103
|
+
return -(velocityX / chartW) * windowSecs;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Top y (px) of the axis-drag grab band — a touch at or below this row starts a
|
|
108
|
+
* scroll. The band is the bottom padding (where the time labels sit), widened to
|
|
109
|
+
* a comfortable touch target.
|
|
110
|
+
*/
|
|
111
|
+
export function axisBandTop(canvasHeight: number, padBottom: number): number {
|
|
112
|
+
"worklet";
|
|
113
|
+
return canvasHeight - Math.max(padBottom, AXIS_GRAB_MIN_PX);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Horizontal pan that scrolls the chart back through time. Pick the activation
|
|
118
|
+
* with `mode`:
|
|
119
|
+
* - `"holdToScrub"` — a one-finger drag anywhere (activates on horizontal
|
|
120
|
+
* travel; vertical falls through). Scrub must be press-and-hold so a quick
|
|
121
|
+
* drag races past it (compose with `Gesture.Race`).
|
|
122
|
+
* - `"axisDrag"` — a one-finger drag starting in the bottom X-axis band, gated
|
|
123
|
+
* via `manualActivation` (compose with `Gesture.Exclusive`, this gesture
|
|
124
|
+
* first: outside the band it fails instantly so scrub runs).
|
|
125
|
+
*
|
|
126
|
+
* Writes `engine.viewEnd`: a number freezes the window at that absolute right
|
|
127
|
+
* edge; `null` means "follow live". Dragging (or flinging) back to the live edge
|
|
128
|
+
* resumes following.
|
|
129
|
+
*/
|
|
130
|
+
export function usePanScroll({
|
|
131
|
+
engine,
|
|
132
|
+
padding,
|
|
133
|
+
minTime,
|
|
134
|
+
enabled,
|
|
135
|
+
mode = "holdToScrub",
|
|
136
|
+
onScrollStart,
|
|
137
|
+
}: UsePanScrollOptions): ReturnType<typeof Gesture.Pan> {
|
|
138
|
+
const { viewEnd, liveEdge, displayWindow, canvasWidth, canvasHeight } = engine;
|
|
139
|
+
const padLeft = padding.left;
|
|
140
|
+
const padRight = padding.right;
|
|
141
|
+
const padBottom = padding.bottom;
|
|
142
|
+
|
|
143
|
+
// Axis-drag activation tracking (manualActivation). Created unconditionally so
|
|
144
|
+
// the hook order is stable; unused in hold-to-scrub mode.
|
|
145
|
+
const startX = useSharedValue(0);
|
|
146
|
+
const startY = useSharedValue(0);
|
|
147
|
+
const armed = useSharedValue(false);
|
|
148
|
+
|
|
149
|
+
const onStart =
|
|
150
|
+
/* istanbul ignore next -- gesture worklet runs on the UI thread, not in Jest */
|
|
151
|
+
() => {
|
|
152
|
+
"worklet";
|
|
153
|
+
cancelAnimation(viewEnd);
|
|
154
|
+
// Anchor at the current right edge so the first delta is relative to where
|
|
155
|
+
// the window sits (the live edge when we were following).
|
|
156
|
+
if (viewEnd.get() == null) viewEnd.set(liveEdge.get());
|
|
157
|
+
onScrollStart?.();
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const onChange =
|
|
161
|
+
/* istanbul ignore next -- gesture worklet runs on the UI thread, not in Jest */
|
|
162
|
+
(e: { changeX: number }) => {
|
|
163
|
+
"worklet";
|
|
164
|
+
const win = displayWindow.get();
|
|
165
|
+
const chartW = canvasWidth.get() - padLeft - padRight;
|
|
166
|
+
if (chartW <= 0) return;
|
|
167
|
+
const edge = liveEdge.get();
|
|
168
|
+
const cur = viewEnd.get() ?? edge;
|
|
169
|
+
const lo = panLowerBound(minTime.get(), win, edge);
|
|
170
|
+
viewEnd.set(nextViewEnd(cur, e.changeX, chartW, win, edge, lo));
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const onEnd =
|
|
174
|
+
/* istanbul ignore next -- gesture worklet runs on the UI thread, not in Jest */
|
|
175
|
+
(e: { velocityX: number }) => {
|
|
176
|
+
"worklet";
|
|
177
|
+
if (viewEnd.get() == null) return;
|
|
178
|
+
const win = displayWindow.get();
|
|
179
|
+
const chartW = canvasWidth.get() - padLeft - padRight;
|
|
180
|
+
if (chartW <= 0) return;
|
|
181
|
+
const edge = liveEdge.get();
|
|
182
|
+
const lo = panLowerBound(minTime.get(), win, edge);
|
|
183
|
+
const velocity = flingVelocity(e.velocityX, chartW, win);
|
|
184
|
+
cancelAnimation(viewEnd);
|
|
185
|
+
viewEnd.set(
|
|
186
|
+
withDecay({ velocity, clamp: [lo, edge] }, (finished) => {
|
|
187
|
+
"worklet";
|
|
188
|
+
// Landed on the live-edge clamp → resume following; stopped short →
|
|
189
|
+
// stay frozen where inertia died.
|
|
190
|
+
if (finished && (viewEnd.get() ?? edge) >= edge - 1e-3) {
|
|
191
|
+
viewEnd.set(null);
|
|
192
|
+
}
|
|
193
|
+
}),
|
|
194
|
+
);
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
if (mode === "axisDrag") {
|
|
198
|
+
return Gesture.Pan()
|
|
199
|
+
.enabled(enabled)
|
|
200
|
+
.maxPointers(1)
|
|
201
|
+
// Position-gate a one-finger pan: only a drag that starts in the bottom
|
|
202
|
+
// axis band scrolls; everything else fails fast so scrub/parent gestures run.
|
|
203
|
+
.manualActivation(true)
|
|
204
|
+
.onTouchesDown(
|
|
205
|
+
/* istanbul ignore next -- gesture worklet runs on the UI thread, not in Jest */
|
|
206
|
+
(e, manager) => {
|
|
207
|
+
"worklet";
|
|
208
|
+
const t = e.changedTouches[0];
|
|
209
|
+
if (!t) return;
|
|
210
|
+
if (t.y < axisBandTop(canvasHeight.get(), padBottom)) {
|
|
211
|
+
armed.set(false);
|
|
212
|
+
manager.fail();
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
armed.set(true);
|
|
216
|
+
startX.set(t.x);
|
|
217
|
+
startY.set(t.y);
|
|
218
|
+
},
|
|
219
|
+
)
|
|
220
|
+
.onTouchesMove(
|
|
221
|
+
/* istanbul ignore next -- gesture worklet runs on the UI thread, not in Jest */
|
|
222
|
+
(e, manager) => {
|
|
223
|
+
"worklet";
|
|
224
|
+
if (!armed.get()) return;
|
|
225
|
+
const t = e.allTouches[0];
|
|
226
|
+
if (!t) return;
|
|
227
|
+
const dx = Math.abs(t.x - startX.get());
|
|
228
|
+
const dy = Math.abs(t.y - startY.get());
|
|
229
|
+
if (dx > AXIS_ACTIVATE_PX && dx >= dy) {
|
|
230
|
+
manager.activate(); // horizontal intent → take the gesture
|
|
231
|
+
} else if (dy > AXIS_ACTIVATE_PX) {
|
|
232
|
+
manager.fail(); // vertical intent → release to parent/scroll
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
)
|
|
236
|
+
.onStart(onStart)
|
|
237
|
+
.onChange(onChange)
|
|
238
|
+
.onEnd(onEnd);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// holdToScrub (default): a one-finger drag anywhere scrolls. Activate on
|
|
242
|
+
// horizontal travel so a quick one-finger drag scrolls; a still press-hold
|
|
243
|
+
// crosses no offset and falls through to the scrub gesture (which owns the
|
|
244
|
+
// long-press). Vertical travel fails it so a parent vertical scroll wins.
|
|
245
|
+
return Gesture.Pan()
|
|
246
|
+
.enabled(enabled)
|
|
247
|
+
.maxPointers(1)
|
|
248
|
+
.activeOffsetX([-AXIS_ACTIVATE_PX, AXIS_ACTIVATE_PX])
|
|
249
|
+
.failOffsetY([-HOLD_SCRUB_FAIL_Y_PX, HOLD_SCRUB_FAIL_Y_PX])
|
|
250
|
+
.onStart(onStart)
|
|
251
|
+
.onChange(onChange)
|
|
252
|
+
.onEnd(onEnd);
|
|
253
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { Gesture } from "react-native-gesture-handler";
|
|
2
|
+
import {
|
|
3
|
+
cancelAnimation,
|
|
4
|
+
useSharedValue,
|
|
5
|
+
type SharedValue,
|
|
6
|
+
} from "react-native-reanimated";
|
|
7
|
+
|
|
8
|
+
import type { ChartPadding } from "../draw/line";
|
|
9
|
+
import { panLowerBound } from "./usePanScroll";
|
|
10
|
+
|
|
11
|
+
/** Engine SharedValues the pinch-zoom gesture reads/writes. */
|
|
12
|
+
export interface PinchZoomEngineRefs {
|
|
13
|
+
/** Visible-window width override in seconds, or `null` to follow `timeWindow`. */
|
|
14
|
+
viewWindow: SharedValue<number | null>;
|
|
15
|
+
/** Absolute right-edge time, or `null` to follow the live edge (shared with pan). */
|
|
16
|
+
viewEnd: SharedValue<number | null>;
|
|
17
|
+
/** Right-edge time the engine would use if following live (advances each frame). */
|
|
18
|
+
liveEdge: SharedValue<number>;
|
|
19
|
+
/**
|
|
20
|
+
* Animating visible window width in seconds. Written directly during a pinch so
|
|
21
|
+
* the displayed window tracks the fingers 1:1 (the frame-loop lerp toward
|
|
22
|
+
* `timeWindow` would otherwise lag a fast pinch and the focal anchor would drift).
|
|
23
|
+
*/
|
|
24
|
+
displayWindow: SharedValue<number>;
|
|
25
|
+
/** Canvas width in px. */
|
|
26
|
+
canvasWidth: SharedValue<number>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface UsePinchZoomOptions {
|
|
30
|
+
engine: PinchZoomEngineRefs;
|
|
31
|
+
padding: ChartPadding;
|
|
32
|
+
/**
|
|
33
|
+
* Earliest selectable time (unix seconds) — typically the first data point /
|
|
34
|
+
* candle. Caps how far the (now wider) window can pan back when zooming out.
|
|
35
|
+
*/
|
|
36
|
+
minTime: SharedValue<number>;
|
|
37
|
+
/** The configured `timeWindow` prop (seconds) — the anchor for the zoom bounds. */
|
|
38
|
+
timeWindow: number;
|
|
39
|
+
/** Master switch. When false the gesture is disabled. */
|
|
40
|
+
enabled: boolean;
|
|
41
|
+
/** Tightest window (max zoom-in), seconds. Default `timeWindow / 8`. */
|
|
42
|
+
minTimeWindow?: number;
|
|
43
|
+
/** Widest window (max zoom-out), seconds. Default: the full data span, ≥ `timeWindow`. */
|
|
44
|
+
maxTimeWindow?: number;
|
|
45
|
+
/** Worklet fired when a pinch activates — e.g. to clear a lingering crosshair. */
|
|
46
|
+
onZoomStart?: () => void;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Clamp a window width to `[minWin, maxWin]`. */
|
|
50
|
+
export function clampWindow(
|
|
51
|
+
win: number,
|
|
52
|
+
minWin: number,
|
|
53
|
+
maxWin: number,
|
|
54
|
+
): number {
|
|
55
|
+
"worklet";
|
|
56
|
+
if (win < minWin) return minWin;
|
|
57
|
+
if (win > maxWin) return maxWin;
|
|
58
|
+
return win;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Resolve the zoom bounds for a gesture. `maxWin` defaults to the data span but
|
|
63
|
+
* never goes below the configured window (so you can always zoom back out to the
|
|
64
|
+
* starting view); `minWin` defaults to an 8× zoom-in of the configured window.
|
|
65
|
+
* Guards against an inverted range (min ≤ max).
|
|
66
|
+
*/
|
|
67
|
+
export function zoomWindowBounds(
|
|
68
|
+
configWindow: number,
|
|
69
|
+
span: number,
|
|
70
|
+
minCfg: number | undefined,
|
|
71
|
+
maxCfg: number | undefined,
|
|
72
|
+
): [number, number] {
|
|
73
|
+
"worklet";
|
|
74
|
+
const maxWin = Math.max(maxCfg ?? span, configWindow);
|
|
75
|
+
let minWin = minCfg ?? configWindow / 8;
|
|
76
|
+
if (minWin > maxWin) minWin = maxWin;
|
|
77
|
+
return [minWin, maxWin];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Absolute time under a pixel x, given the visible window. Inverse of the chart's
|
|
82
|
+
* time→x projection: `x = chartLeft + (t - winStart)/window * chartW`.
|
|
83
|
+
*/
|
|
84
|
+
export function focalTime(
|
|
85
|
+
focalX: number,
|
|
86
|
+
chartLeft: number,
|
|
87
|
+
chartW: number,
|
|
88
|
+
winStart: number,
|
|
89
|
+
windowSecs: number,
|
|
90
|
+
): number {
|
|
91
|
+
"worklet";
|
|
92
|
+
return winStart + ((focalX - chartLeft) / chartW) * windowSecs;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The new right-edge time that keeps `focalT` pinned under `focalX` after the
|
|
97
|
+
* window width changes to `newWindow` — i.e. zoom around the focal point. Derived
|
|
98
|
+
* from the projection above by solving for the right edge (`winStart + window`):
|
|
99
|
+
* the right edge sits `(fraction of the window to the right of the focal)` past
|
|
100
|
+
* `focalT`.
|
|
101
|
+
*/
|
|
102
|
+
export function zoomViewEnd(
|
|
103
|
+
focalT: number,
|
|
104
|
+
focalX: number,
|
|
105
|
+
chartLeft: number,
|
|
106
|
+
chartW: number,
|
|
107
|
+
newWindow: number,
|
|
108
|
+
): number {
|
|
109
|
+
"worklet";
|
|
110
|
+
const rightFraction = (chartLeft + chartW - focalX) / chartW;
|
|
111
|
+
return focalT + rightFraction * newWindow;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Pinch-to-zoom the visible time window, anchored at the focal point between the
|
|
116
|
+
* two fingers (the time under your fingers stays put). Writes `engine.viewWindow`
|
|
117
|
+
* (the window width) and `engine.viewEnd` (the right edge, so the focal anchor
|
|
118
|
+
* holds) — the symmetric counterpart of {@link usePanScroll}, which only moves the
|
|
119
|
+
* right edge. Pinch is two-finger, so it composes with the one-finger pan/scrub
|
|
120
|
+
* via `Gesture.Simultaneous`.
|
|
121
|
+
*
|
|
122
|
+
* Reaching the live edge resumes following (`viewEnd → null`); the zoom level
|
|
123
|
+
* (`viewWindow`) is independent of scroll position and persists.
|
|
124
|
+
*/
|
|
125
|
+
export function usePinchZoom({
|
|
126
|
+
engine,
|
|
127
|
+
padding,
|
|
128
|
+
minTime,
|
|
129
|
+
timeWindow,
|
|
130
|
+
enabled,
|
|
131
|
+
minTimeWindow,
|
|
132
|
+
maxTimeWindow,
|
|
133
|
+
onZoomStart,
|
|
134
|
+
}: UsePinchZoomOptions): ReturnType<typeof Gesture.Pinch> {
|
|
135
|
+
const { viewWindow, viewEnd, liveEdge, displayWindow, canvasWidth } = engine;
|
|
136
|
+
const padLeft = padding.left;
|
|
137
|
+
const padRight = padding.right;
|
|
138
|
+
|
|
139
|
+
// Snapshot at gesture start so the cumulative `scale`/`focalX` map from a fixed
|
|
140
|
+
// origin (no compounding drift while pinching).
|
|
141
|
+
const startWindow = useSharedValue(timeWindow);
|
|
142
|
+
const startViewEnd = useSharedValue(0);
|
|
143
|
+
|
|
144
|
+
const onStart =
|
|
145
|
+
/* istanbul ignore next -- gesture worklet runs on the UI thread, not in Jest */
|
|
146
|
+
() => {
|
|
147
|
+
"worklet";
|
|
148
|
+
cancelAnimation(viewEnd);
|
|
149
|
+
cancelAnimation(viewWindow);
|
|
150
|
+
const edge = liveEdge.get();
|
|
151
|
+
startWindow.set(viewWindow.get() ?? timeWindow);
|
|
152
|
+
startViewEnd.set(viewEnd.get() ?? edge);
|
|
153
|
+
onZoomStart?.();
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const onChange =
|
|
157
|
+
/* istanbul ignore next -- gesture worklet runs on the UI thread, not in Jest */
|
|
158
|
+
(e: { scale: number; focalX: number }) => {
|
|
159
|
+
"worklet";
|
|
160
|
+
const chartW = canvasWidth.get() - padLeft - padRight;
|
|
161
|
+
if (chartW <= 0 || e.scale <= 0) return;
|
|
162
|
+
const edge = liveEdge.get();
|
|
163
|
+
const sWin = startWindow.get();
|
|
164
|
+
const sEnd = startViewEnd.get();
|
|
165
|
+
const span = edge - minTime.get();
|
|
166
|
+
const [minWin, maxWin] = zoomWindowBounds(
|
|
167
|
+
timeWindow,
|
|
168
|
+
span,
|
|
169
|
+
minTimeWindow,
|
|
170
|
+
maxTimeWindow,
|
|
171
|
+
);
|
|
172
|
+
// Pinch out (scale > 1) ⇒ narrower window ⇒ zoom in.
|
|
173
|
+
const newWin = clampWindow(sWin / e.scale, minWin, maxWin);
|
|
174
|
+
// Time under the fingers, mapped through the START window so the anchor
|
|
175
|
+
// doesn't compound; the right edge then keeps it under the (live) focal.
|
|
176
|
+
const focalT = focalTime(e.focalX, padLeft, chartW, sEnd - sWin, sWin);
|
|
177
|
+
let newEnd = zoomViewEnd(focalT, e.focalX, padLeft, chartW, newWin);
|
|
178
|
+
const lo = panLowerBound(minTime.get(), newWin, edge);
|
|
179
|
+
if (newEnd < lo) newEnd = lo;
|
|
180
|
+
if (newEnd > edge) newEnd = edge;
|
|
181
|
+
viewWindow.set(newWin);
|
|
182
|
+
// Track the displayed window 1:1 (bypass the frame-loop lerp lag) so the
|
|
183
|
+
// focal anchor is pixel-accurate during the gesture.
|
|
184
|
+
displayWindow.set(newWin);
|
|
185
|
+
viewEnd.set(newEnd >= edge ? null : newEnd);
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
return Gesture.Pinch().enabled(enabled).onStart(onStart).onChange(onChange);
|
|
189
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { Gesture } from "react-native-gesture-handler";
|
|
2
|
+
import {
|
|
3
|
+
useAnimatedReaction,
|
|
4
|
+
useDerivedValue,
|
|
5
|
+
useSharedValue,
|
|
6
|
+
type SharedValue,
|
|
7
|
+
} from "react-native-reanimated";
|
|
8
|
+
import { scheduleOnRN } from "react-native-worklets";
|
|
9
|
+
|
|
10
|
+
import type { ChartEngineLayout } from "../core/useLiveChartEngine";
|
|
11
|
+
import type { ChartPadding } from "../draw/line";
|
|
12
|
+
import {
|
|
13
|
+
clampToBounds,
|
|
14
|
+
nearestDraggableIndex,
|
|
15
|
+
referenceValueOut,
|
|
16
|
+
} from "../math/referenceDrag";
|
|
17
|
+
import { referenceLineForm } from "../math/referenceLines";
|
|
18
|
+
import type { ReferenceLine } from "../types";
|
|
19
|
+
import {
|
|
20
|
+
computeScrubDotY,
|
|
21
|
+
computeValueAtY,
|
|
22
|
+
snapPrice,
|
|
23
|
+
} from "./crosshairShared";
|
|
24
|
+
|
|
25
|
+
/** Vertical reach (px) around a line within which a touch grabs it. */
|
|
26
|
+
const GRAB_SLOP = 14;
|
|
27
|
+
/** Travel (px) that resolves drag (vertical) vs. fall-through (horizontal) intent. */
|
|
28
|
+
const DRAG_ACTIVATE_PX = 4;
|
|
29
|
+
|
|
30
|
+
/** Stable empty array so the handle / out-state worklets stay referentially stable
|
|
31
|
+
* (and the onDragIn/Out reaction never fires) when the feature is unused. */
|
|
32
|
+
const EMPTY: never[] = [];
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Builds the per-line **drag** gesture for draggable Form-A reference lines: grab a
|
|
36
|
+
* line near its value-Y and drag vertically to set a new value, with optional
|
|
37
|
+
* `snap` + `bounds` clamp. Mirrors the order-ticket reticle in {@link useCrosshair}
|
|
38
|
+
* (value↔Y via `computeValueAtY` / `computeScrubDotY`, frozen value re-projected
|
|
39
|
+
* each frame) but per line, writing into the shared `dragValues` array the layout
|
|
40
|
+
* and overlays read.
|
|
41
|
+
*
|
|
42
|
+
* The pan uses `manualActivation`: it grabs only when a touch starts within
|
|
43
|
+
* {@link GRAB_SLOP} of a draggable line **and** the motion is vertical — a
|
|
44
|
+
* horizontal drag (scrub) or a touch off every line fails fast so the chart's other
|
|
45
|
+
* gestures run (compose this ahead of them via `Gesture.Exclusive`).
|
|
46
|
+
*
|
|
47
|
+
* Also fires the per-line drag callbacks: `onChange` (de-duped during drag),
|
|
48
|
+
* `onCommit` (on release), and `onDragIn` / `onDragOut` (value crossing the visible
|
|
49
|
+
* range or a `bounds`, from a drag or the axis rescaling — edge-detected each frame).
|
|
50
|
+
*/
|
|
51
|
+
export function useReferenceDrag(
|
|
52
|
+
engine: ChartEngineLayout,
|
|
53
|
+
padding: ChartPadding,
|
|
54
|
+
lines: ReferenceLine[],
|
|
55
|
+
dragValues: SharedValue<number[]>,
|
|
56
|
+
dragActive: SharedValue<boolean[]>,
|
|
57
|
+
enabled: boolean,
|
|
58
|
+
): ReturnType<typeof Gesture.Pan> {
|
|
59
|
+
const anyDraggable =
|
|
60
|
+
enabled &&
|
|
61
|
+
lines.some((l) => l.draggable && referenceLineForm(l) === "line");
|
|
62
|
+
const anyDragInOut = lines.some(
|
|
63
|
+
(l) =>
|
|
64
|
+
referenceLineForm(l) === "line" &&
|
|
65
|
+
(l.onDragIn != null || l.onDragOut != null),
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
// Per-line handle Y (canvas px), index-aligned with `lines`; -1 when the line
|
|
69
|
+
// isn't draggable or the canvas isn't laid out. Off-screen lines pin to the
|
|
70
|
+
// nearest plot edge so they stay grabbable. Recomputed each frame (UI thread).
|
|
71
|
+
/* istanbul ignore next -- worklet runs on the UI thread, not in Jest */
|
|
72
|
+
const handleYs = useDerivedValue<number[]>(() => {
|
|
73
|
+
if (!anyDraggable) return EMPTY;
|
|
74
|
+
const ch = engine.canvasHeight.get();
|
|
75
|
+
const dMin = engine.displayMin.get();
|
|
76
|
+
const dMax = engine.displayMax.get();
|
|
77
|
+
const top = padding.top;
|
|
78
|
+
const bottom = ch - padding.bottom;
|
|
79
|
+
const out: number[] = [];
|
|
80
|
+
for (let i = 0; i < lines.length; i++) {
|
|
81
|
+
const l = lines[i];
|
|
82
|
+
if (
|
|
83
|
+
!l.draggable ||
|
|
84
|
+
referenceLineForm(l) !== "line" ||
|
|
85
|
+
l.value === undefined
|
|
86
|
+
) {
|
|
87
|
+
out.push(-1);
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const v = dragValues.get()[i] ?? l.value;
|
|
91
|
+
const y = computeScrubDotY(v, dMin, dMax, ch, top, padding.bottom);
|
|
92
|
+
out.push(y < 0 ? -1 : Math.min(bottom, Math.max(top, y)));
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const dragIndex = useSharedValue(-1);
|
|
98
|
+
const startX = useSharedValue(0);
|
|
99
|
+
const startY = useSharedValue(0);
|
|
100
|
+
const activated = useSharedValue(false);
|
|
101
|
+
const lastChange = useSharedValue(0);
|
|
102
|
+
|
|
103
|
+
// JS-thread callback dispatch — closes over the latest `lines` each render.
|
|
104
|
+
/* istanbul ignore next -- runs via scheduleOnRN from the UI-thread gesture */
|
|
105
|
+
function emitChange(i: number, v: number) {
|
|
106
|
+
lines[i]?.onChange?.(v);
|
|
107
|
+
}
|
|
108
|
+
/* istanbul ignore next -- runs via scheduleOnRN from the UI-thread gesture */
|
|
109
|
+
function emitCommit(i: number, v: number) {
|
|
110
|
+
lines[i]?.onCommit?.(v);
|
|
111
|
+
}
|
|
112
|
+
/* istanbul ignore next -- runs via scheduleOnRN from the UI-thread reaction */
|
|
113
|
+
function emitDragOut(i: number, v: number) {
|
|
114
|
+
lines[i]?.onDragOut?.(v);
|
|
115
|
+
}
|
|
116
|
+
/* istanbul ignore next -- runs via scheduleOnRN from the UI-thread reaction */
|
|
117
|
+
function emitDragIn(i: number, v: number) {
|
|
118
|
+
lines[i]?.onDragIn?.(v);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// onDragIn / onDragOut — edge-detect each line's "out of the watched interval"
|
|
122
|
+
// state (from a drag or the axis rescaling under a fixed value).
|
|
123
|
+
useAnimatedReaction(
|
|
124
|
+
/* istanbul ignore next -- worklet runs on the UI thread, not in Jest */
|
|
125
|
+
() => {
|
|
126
|
+
if (!anyDragInOut) return EMPTY as boolean[];
|
|
127
|
+
const dMin = engine.displayMin.get();
|
|
128
|
+
const dMax = engine.displayMax.get();
|
|
129
|
+
const out: boolean[] = [];
|
|
130
|
+
for (let i = 0; i < lines.length; i++) {
|
|
131
|
+
const l = lines[i];
|
|
132
|
+
if (
|
|
133
|
+
referenceLineForm(l) !== "line" ||
|
|
134
|
+
l.value === undefined ||
|
|
135
|
+
(l.onDragIn == null && l.onDragOut == null)
|
|
136
|
+
) {
|
|
137
|
+
out.push(false);
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
const v = dragValues.get()[i] ?? l.value;
|
|
141
|
+
out.push(referenceValueOut(v, dMin, dMax, l.bounds));
|
|
142
|
+
}
|
|
143
|
+
return out;
|
|
144
|
+
},
|
|
145
|
+
/* istanbul ignore next -- worklet runs on the UI thread, not in Jest */
|
|
146
|
+
(curr, prev) => {
|
|
147
|
+
if (!prev || curr === prev) return;
|
|
148
|
+
for (let i = 0; i < curr.length; i++) {
|
|
149
|
+
if (prev[i] === undefined || curr[i] === prev[i]) continue;
|
|
150
|
+
const l = lines[i];
|
|
151
|
+
const v = dragValues.get()[i] ?? l.value ?? 0;
|
|
152
|
+
if (curr[i]) scheduleOnRN(emitDragOut, i, v);
|
|
153
|
+
else scheduleOnRN(emitDragIn, i, v);
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
[lines, anyDragInOut],
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
// ── Gesture callbacks (UI-thread worklets — excluded from Jest coverage) ──────
|
|
160
|
+
/* istanbul ignore next -- gesture worklet runs on the UI thread, not in Jest */
|
|
161
|
+
const onTouchesDown = (
|
|
162
|
+
e: { changedTouches: { x: number; y: number }[] },
|
|
163
|
+
manager: { fail: () => void },
|
|
164
|
+
) => {
|
|
165
|
+
"worklet";
|
|
166
|
+
const t = e.changedTouches[0];
|
|
167
|
+
if (!t) return;
|
|
168
|
+
const i = nearestDraggableIndex(handleYs.get(), t.y, GRAB_SLOP);
|
|
169
|
+
if (i < 0) {
|
|
170
|
+
manager.fail();
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
dragIndex.set(i);
|
|
174
|
+
startX.set(t.x);
|
|
175
|
+
startY.set(t.y);
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
/* istanbul ignore next -- gesture worklet runs on the UI thread, not in Jest */
|
|
179
|
+
const onTouchesMove = (
|
|
180
|
+
e: { allTouches: { x: number; y: number }[] },
|
|
181
|
+
manager: { activate: () => void; fail: () => void },
|
|
182
|
+
) => {
|
|
183
|
+
"worklet";
|
|
184
|
+
if (dragIndex.get() < 0) return;
|
|
185
|
+
const t = e.allTouches[0];
|
|
186
|
+
if (!t) return;
|
|
187
|
+
const dx = Math.abs(t.x - startX.get());
|
|
188
|
+
const dy = Math.abs(t.y - startY.get());
|
|
189
|
+
if (dy > DRAG_ACTIVATE_PX && dy >= dx) manager.activate();
|
|
190
|
+
else if (dx > DRAG_ACTIVATE_PX) manager.fail();
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
/* istanbul ignore next -- gesture worklet runs on the UI thread, not in Jest */
|
|
194
|
+
const onStart = () => {
|
|
195
|
+
"worklet";
|
|
196
|
+
const i = dragIndex.get();
|
|
197
|
+
if (i < 0) return;
|
|
198
|
+
activated.set(true);
|
|
199
|
+
const arr = dragActive.get().slice();
|
|
200
|
+
arr[i] = true;
|
|
201
|
+
dragActive.set(arr);
|
|
202
|
+
lastChange.set(dragValues.get()[i] ?? lines[i].value ?? 0);
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
/* istanbul ignore next -- gesture worklet runs on the UI thread, not in Jest */
|
|
206
|
+
const onUpdate = (e: { y: number }) => {
|
|
207
|
+
"worklet";
|
|
208
|
+
const i = dragIndex.get();
|
|
209
|
+
if (i < 0) return;
|
|
210
|
+
const l = lines[i];
|
|
211
|
+
const raw = computeValueAtY(
|
|
212
|
+
e.y,
|
|
213
|
+
engine.displayMin.get(),
|
|
214
|
+
engine.displayMax.get(),
|
|
215
|
+
engine.canvasHeight.get(),
|
|
216
|
+
padding.top,
|
|
217
|
+
padding.bottom,
|
|
218
|
+
);
|
|
219
|
+
if (raw === null) return;
|
|
220
|
+
const v = clampToBounds(snapPrice(raw, l.snap), l.bounds);
|
|
221
|
+
const arr = dragValues.get().slice();
|
|
222
|
+
arr[i] = v;
|
|
223
|
+
dragValues.set(arr);
|
|
224
|
+
if (v !== lastChange.get()) {
|
|
225
|
+
lastChange.set(v);
|
|
226
|
+
if (l.onChange) scheduleOnRN(emitChange, i, v);
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
/* istanbul ignore next -- gesture worklet runs on the UI thread, not in Jest */
|
|
231
|
+
const onFinalize = () => {
|
|
232
|
+
"worklet";
|
|
233
|
+
const i = dragIndex.get();
|
|
234
|
+
if (i >= 0 && activated.get()) {
|
|
235
|
+
const v = dragValues.get()[i] ?? lines[i].value ?? 0;
|
|
236
|
+
const arr = dragActive.get().slice();
|
|
237
|
+
arr[i] = false;
|
|
238
|
+
dragActive.set(arr);
|
|
239
|
+
if (lines[i].onCommit) scheduleOnRN(emitCommit, i, v);
|
|
240
|
+
}
|
|
241
|
+
dragIndex.set(-1);
|
|
242
|
+
activated.set(false);
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
return Gesture.Pan()
|
|
246
|
+
.enabled(anyDraggable)
|
|
247
|
+
.maxPointers(1)
|
|
248
|
+
.manualActivation(true)
|
|
249
|
+
.onTouchesDown(onTouchesDown)
|
|
250
|
+
.onTouchesMove(onTouchesMove)
|
|
251
|
+
.onStart(onStart)
|
|
252
|
+
.onUpdate(onUpdate)
|
|
253
|
+
.onFinalize(onFinalize);
|
|
254
|
+
}
|