Skip to content

Commit 3ef4881

Browse files
committed
Fix autoscroll exceeding bounds
1 parent 2d24b14 commit 3ef4881

File tree

2 files changed

+59
-52
lines changed

2 files changed

+59
-52
lines changed

packages/react-native-sortables/src/providers/shared/AutoScrollProvider/AutoScrollProvider.tsx

Lines changed: 58 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,7 @@ import useDebugHelpers from './useDebugHelpers';
2525
import {
2626
calculateRawProgressHorizontal,
2727
calculateRawProgressVertical,
28-
clampDistanceHorizontal,
29-
clampDistanceVertical
28+
clampDistance
3029
} from './utils';
3130

3231
// Maximum elapsed time multiplier to prevent excessive scrolling distances when app lags
@@ -102,6 +101,18 @@ const { AutoScrollProvider, useAutoScrollContext } = createProvider(
102101
};
103102
});
104103

104+
type StateContextType = {
105+
targetScrollOffset: null | number;
106+
prevContainerOffset: null | number;
107+
lastUpdateTimestamp: null | number;
108+
};
109+
110+
const INITIAL_STATE: StateContextType = {
111+
lastUpdateTimestamp: null,
112+
prevContainerOffset: null,
113+
targetScrollOffset: null
114+
};
115+
105116
type AutoScrollUpdaterProps = Omit<
106117
AutoScrollSettings,
107118
'autoScrollDirection' | 'autoScrollEnabled'
@@ -129,8 +140,7 @@ function AutoScrollUpdater({
129140
useCommonValuesContext();
130141

131142
const progress = useMutableValue(0);
132-
const lastUpdateTimestamp = useMutableValue<null | number>(null);
133-
const targetScrollOffset = useMutableValue<null | number>(null);
143+
const context = useMutableValue<StateContextType>(INITIAL_STATE);
134144

135145
const scrollAxis = isVertical ? 'y' : 'x';
136146
const activationOffset = toPair(autoScrollActivationOffset);
@@ -158,29 +168,26 @@ function AutoScrollUpdater({
158168
);
159169
}
160170

161-
let calculateRawProgress, clampDistance;
162-
if (isVertical) {
163-
calculateRawProgress = calculateRawProgressVertical;
164-
clampDistance = clampDistanceVertical;
165-
} else {
166-
calculateRawProgress = calculateRawProgressHorizontal;
167-
clampDistance = clampDistanceHorizontal;
168-
}
171+
const calculateRawProgress = isVertical
172+
? calculateRawProgressVertical
173+
: calculateRawProgressHorizontal;
169174

170175
useAnimatedReaction(
171176
() => {
177+
const ctx = context.value;
172178
let position = touchPosition.value?.[scrollAxis] ?? null;
173-
if (position !== null && targetScrollOffset.value !== null) {
179+
if (position !== null && ctx.targetScrollOffset !== null) {
174180
// Sometimes the scroll distance is so small that the scrollTo takes
175181
// no effect. To handle this case, we have to update the position
176182
// of the view used to determine the progress, even if the actual
177183
// position of the view is not changed (because of too small scroll distance).
178-
position += targetScrollOffset.value - currentScrollOffset.value;
184+
position += ctx.targetScrollOffset - currentScrollOffset.value;
179185
}
180186
return position;
181187
},
182188
position => {
183189
if (position === null) {
190+
context.value.targetScrollOffset = null;
184191
debug?.hideDebugViews?.();
185192
return;
186193
}
@@ -202,7 +209,7 @@ function AutoScrollUpdater({
202209
);
203210

204211
if (progress.value === 0) {
205-
targetScrollOffset.value = null;
212+
context.value.targetScrollOffset = null;
206213
}
207214

208215
debug?.updateDebugRects?.(
@@ -223,26 +230,46 @@ function AutoScrollUpdater({
223230
return;
224231
}
225232

233+
const ctx = context.value;
226234
const pendingDistance =
227-
targetScrollOffset.value !== null
228-
? targetScrollOffset.value - currentScrollOffset.value
235+
ctx.targetScrollOffset !== null
236+
? ctx.targetScrollOffset - currentScrollOffset.value
229237
: 0;
230238

239+
const containerOffset = isVertical
240+
? scrollableMeasurements.pageY - containerMeasurements.pageY
241+
: scrollableMeasurements.pageX - containerMeasurements.pageX;
242+
const scrollableCrossSize = isVertical
243+
? scrollableMeasurements.height
244+
: scrollableMeasurements.width;
245+
246+
if (
247+
pendingDistance !== 0 &&
248+
containerOffset === ctx.prevContainerOffset
249+
) {
250+
// Return if measurements haven't been updated yet (we scroll based on the
251+
// relative position of the container in the ScrollView so we have to ensure
252+
// that the last update is already applied)
253+
return;
254+
}
255+
231256
const clampedDistance = clampDistance(
232257
distance + pendingDistance,
233-
containerMeasurements,
234-
scrollableMeasurements,
258+
containerOffset,
259+
scrollableCrossSize,
235260
bounds,
236261
maxOverscroll
237262
);
238263

239264
const targetOffset = currentScrollOffset.value + clampedDistance;
240-
targetScrollOffset.value = targetOffset;
265+
266+
ctx.targetScrollOffset = targetOffset;
241267

242268
if (Math.abs(clampedDistance) < 1) {
243269
return;
244270
}
245271

272+
ctx.prevContainerOffset = containerOffset;
246273
scrollTo(
247274
scrollableRef,
248275
isVertical ? 0 : targetOffset,
@@ -251,13 +278,12 @@ function AutoScrollUpdater({
251278
);
252279
},
253280
[
281+
context,
254282
currentScrollOffset,
255-
targetScrollOffset,
256283
isVertical,
257284
scrollableRef,
258285
containerRef,
259286
contentAxisBounds,
260-
clampDistance,
261287
maxOverscroll
262288
]
263289
);
@@ -269,8 +295,9 @@ function AutoScrollUpdater({
269295
return;
270296
}
271297

272-
lastUpdateTimestamp.value ??= timestamp;
273-
const elapsedTime = timestamp - lastUpdateTimestamp.value;
298+
const ctx = context.value;
299+
ctx.lastUpdateTimestamp ??= timestamp;
300+
const elapsedTime = timestamp - ctx.lastUpdateTimestamp;
274301
if (elapsedTime < autoScrollInterval) {
275302
return;
276303
}
@@ -281,7 +308,7 @@ function AutoScrollUpdater({
281308
MIN_ELAPSED_TIME_CAP
282309
);
283310
const cappedElapsedTime = Math.min(elapsedTime, maxElapsedTime);
284-
lastUpdateTimestamp.value = timestamp;
311+
ctx.lastUpdateTimestamp = timestamp;
285312

286313
const velocity = interpolate(
287314
progress.value,
@@ -294,13 +321,13 @@ function AutoScrollUpdater({
294321
scrollBy(distance, animateScrollTo);
295322
},
296323
[
324+
context,
297325
scrollBy,
298326
maxStartVelocity,
299327
maxEndVelocity,
300328
progress,
301329
autoScrollInterval,
302-
animateScrollTo,
303-
lastUpdateTimestamp
330+
animateScrollTo
304331
]
305332
);
306333

@@ -318,8 +345,10 @@ function AutoScrollUpdater({
318345
active => {
319346
if (active) {
320347
dragStartScrollOffset.value = currentScrollOffset.value;
321-
lastUpdateTimestamp.value = null;
322-
targetScrollOffset.value = null;
348+
const ctx = context.value;
349+
ctx.lastUpdateTimestamp = null;
350+
ctx.targetScrollOffset = null;
351+
ctx.prevContainerOffset = null;
323352
runOnJS(toggleFrameCallback)(true);
324353
} else {
325354
dragStartScrollOffset.value = null;

packages/react-native-sortables/src/providers/shared/AutoScrollProvider/utils.ts

Lines changed: 1 addition & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -65,15 +65,7 @@ export const calculateRawProgressHorizontal: CalculateRawProgressFunction = (
6565
...rest
6666
) => calculateRawProgress(position, cX, cW, sX, sW, ...rest);
6767

68-
type ClampDistanceFunction = (
69-
distance: number,
70-
contentContainerMeasurements: MeasuredDimensions,
71-
scrollContainerMeasurements: MeasuredDimensions,
72-
contentBounds: [number, number],
73-
maxOverscroll: [number, number]
74-
) => number;
75-
76-
const clampDistance = (
68+
export const clampDistance = (
7769
distance: number,
7870
containerOffset: number,
7971
scrollableSize: number,
@@ -102,17 +94,3 @@ const clampDistance = (
10294

10395
return 0;
10496
};
105-
106-
export const clampDistanceVertical: ClampDistanceFunction = (
107-
distance,
108-
{ pageY: cY },
109-
{ height: sH, pageY: sY },
110-
...rest
111-
) => clampDistance(distance, sY - cY, sH, ...rest);
112-
113-
export const clampDistanceHorizontal: ClampDistanceFunction = (
114-
distance,
115-
{ pageX: cX },
116-
{ pageX: sX, width: sW },
117-
...rest
118-
) => clampDistance(distance, sX - cX, sW, ...rest);

0 commit comments

Comments
 (0)