Skip to content

Commit 662957c

Browse files
authored
Allow passing range option to useSwipeTransition (facebook#32412)
Stacked on facebook#32379 Track the range offsets along the timeline where previous/current/next is. This can also be specified as an option. This lets you model more than three states along a timeline by clamping them and then updating the "current" as you go. It also allows specifying the "current" offset as something different than what it was when the gesture started such as if it has to start after scroll has already happened (such as what happens if you listen to the "scroll" event).
1 parent 88479c6 commit 662957c

File tree

10 files changed

+122
-36
lines changed

10 files changed

+122
-36
lines changed

packages/react-art/src/ReactFiberConfigART.js

+5
Original file line numberDiff line numberDiff line change
@@ -510,8 +510,13 @@ export function createViewTransitionInstance(
510510

511511
export type GestureTimeline = null;
512512

513+
export function getCurrentGestureOffset(provider: GestureTimeline): number {
514+
throw new Error('useSwipeTransition is not yet supported in react-art.');
515+
}
516+
513517
export function subscribeToGestureDirection(
514518
provider: GestureTimeline,
519+
currentOffset: number,
515520
directionCallback: (direction: boolean) => void,
516521
): () => void {
517522
throw new Error('useSwipeTransition is not yet supported in react-art.');

packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js

+17-15
Original file line numberDiff line numberDiff line change
@@ -1480,17 +1480,21 @@ export function createViewTransitionInstance(
14801480

14811481
export type GestureTimeline = AnimationTimeline; // TODO: More provider types.
14821482

1483-
export function subscribeToGestureDirection(
1484-
provider: GestureTimeline,
1485-
directionCallback: (direction: boolean) => void,
1486-
): () => void {
1483+
export function getCurrentGestureOffset(provider: GestureTimeline): number {
14871484
const time = provider.currentTime;
14881485
if (time === null) {
14891486
throw new Error(
14901487
'Cannot start a gesture with a disconnected AnimationTimeline.',
14911488
);
14921489
}
1493-
const startTime = typeof time === 'number' ? time : time.value;
1490+
return typeof time === 'number' ? time : time.value;
1491+
}
1492+
1493+
export function subscribeToGestureDirection(
1494+
provider: GestureTimeline,
1495+
currentOffset: number,
1496+
directionCallback: (direction: boolean) => void,
1497+
): () => void {
14941498
if (
14951499
typeof ScrollTimeline === 'function' &&
14961500
provider instanceof ScrollTimeline
@@ -1500,11 +1504,10 @@ export function subscribeToGestureDirection(
15001504
const scrollCallback = () => {
15011505
const newTime = provider.currentTime;
15021506
if (newTime !== null) {
1503-
directionCallback(
1504-
typeof newTime === 'number'
1505-
? newTime > startTime
1506-
: newTime.value > startTime,
1507-
);
1507+
const newValue = typeof newTime === 'number' ? newTime : newTime.value;
1508+
if (newValue !== currentOffset) {
1509+
directionCallback(newValue > currentOffset);
1510+
}
15081511
}
15091512
};
15101513
element.addEventListener('scroll', scrollCallback, false);
@@ -1517,11 +1520,10 @@ export function subscribeToGestureDirection(
15171520
const rafCallback = () => {
15181521
const newTime = provider.currentTime;
15191522
if (newTime !== null) {
1520-
directionCallback(
1521-
typeof newTime === 'number'
1522-
? newTime > startTime
1523-
: newTime.value > startTime,
1524-
);
1523+
const newValue = typeof newTime === 'number' ? newTime : newTime.value;
1524+
if (newValue !== currentOffset) {
1525+
directionCallback(newValue > currentOffset);
1526+
}
15251527
}
15261528
callbackID = requestAnimationFrame(rafCallback);
15271529
};

packages/react-native-renderer/src/ReactFiberConfigNative.js

+5
Original file line numberDiff line numberDiff line change
@@ -607,8 +607,13 @@ export function createViewTransitionInstance(
607607

608608
export type GestureTimeline = null;
609609

610+
export function getCurrentGestureOffset(provider: GestureTimeline): number {
611+
throw new Error('useSwipeTransition is not yet supported in React Native.');
612+
}
613+
610614
export function subscribeToGestureDirection(
611615
provider: GestureTimeline,
616+
currentOffset: number,
612617
directionCallback: (direction: boolean) => void,
613618
): () => void {
614619
throw new Error('useSwipeTransition is not yet supported in React Native.');

packages/react-noop-renderer/src/createReactNoop.js

+5
Original file line numberDiff line numberDiff line change
@@ -796,8 +796,13 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
796796
return null;
797797
},
798798

799+
getCurrentGestureOffset(provider: GestureTimeline): number {
800+
return 0;
801+
},
802+
799803
subscribeToGestureDirection(
800804
provider: GestureTimeline,
805+
currentOffset: number,
801806
directionCallback: (direction: boolean) => void,
802807
): () => void {
803808
return () => {};

packages/react-reconciler/src/ReactFiberConfigWithNoMutation.js

+1
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,5 @@ export const startViewTransition = shim;
4949
export type ViewTransitionInstance = null | {name: string, ...};
5050
export const createViewTransitionInstance = shim;
5151
export type GestureTimeline = any;
52+
export const getCurrentGestureOffset = shim;
5253
export const subscribeToGestureDirection = shim;

packages/react-reconciler/src/ReactFiberGestureScheduler.js

+36-19
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ export type ScheduledGesture = {
1919
provider: GestureTimeline,
2020
count: number, // The number of times this same provider has been started.
2121
direction: boolean, // false = previous, true = next
22+
rangePrevious: number, // The end along the timeline where the previous state is reached.
23+
rangeCurrent: number, // The starting offset along the timeline.
24+
rangeNext: number, // The end along the timeline where the next state is reached.
2225
cancel: () => void, // Cancel the subscription to direction change.
2326
prev: null | ScheduledGesture, // The previous scheduled gesture in the queue for this root.
2427
next: null | ScheduledGesture, // The next scheduled gesture in the queue for this root.
@@ -28,6 +31,9 @@ export function scheduleGesture(
2831
root: FiberRoot,
2932
provider: GestureTimeline,
3033
initialDirection: boolean,
34+
rangePrevious: number,
35+
rangeCurrent: number,
36+
rangeNext: number,
3137
): ScheduledGesture {
3238
let prev = root.gestures;
3339
while (prev !== null) {
@@ -42,32 +48,43 @@ export function scheduleGesture(
4248
}
4349
prev = next;
4450
}
51+
const isFlippedDirection = rangePrevious > rangeNext;
4552
// Add new instance to the end of the queue.
46-
const cancel = subscribeToGestureDirection(provider, (direction: boolean) => {
47-
if (gesture.direction !== direction) {
48-
gesture.direction = direction;
49-
if (gesture.prev === null && root.gestures !== gesture) {
50-
// This gesture is not in the schedule, meaning it was already rendered.
51-
// We need to rerender in the new direction. Insert it into the first slot
52-
// in case other gestures are queued after the on-going one.
53-
const existing = root.gestures;
54-
gesture.next = existing;
55-
if (existing !== null) {
56-
existing.prev = gesture;
53+
const cancel = subscribeToGestureDirection(
54+
provider,
55+
rangeCurrent,
56+
(direction: boolean) => {
57+
if (isFlippedDirection) {
58+
direction = !direction;
59+
}
60+
if (gesture.direction !== direction) {
61+
gesture.direction = direction;
62+
if (gesture.prev === null && root.gestures !== gesture) {
63+
// This gesture is not in the schedule, meaning it was already rendered.
64+
// We need to rerender in the new direction. Insert it into the first slot
65+
// in case other gestures are queued after the on-going one.
66+
const existing = root.gestures;
67+
gesture.next = existing;
68+
if (existing !== null) {
69+
existing.prev = gesture;
70+
}
71+
root.gestures = gesture;
72+
// Schedule the lane on the root. The Fibers will already be marked as
73+
// long as the gesture is active on that Hook.
74+
root.pendingLanes |= GestureLane;
75+
ensureRootIsScheduled(root);
5776
}
58-
root.gestures = gesture;
59-
// Schedule the lane on the root. The Fibers will already be marked as
60-
// long as the gesture is active on that Hook.
61-
root.pendingLanes |= GestureLane;
62-
ensureRootIsScheduled(root);
77+
// TODO: If we're currently rendering this gesture, we need to restart it.
6378
}
64-
// TODO: If we're currently rendering this gesture, we need to restart it.
65-
}
66-
});
79+
},
80+
);
6781
const gesture: ScheduledGesture = {
6882
provider: provider,
6983
count: 1,
7084
direction: initialDirection,
85+
rangePrevious: rangePrevious,
86+
rangeCurrent: rangeCurrent,
87+
rangeNext: rangeNext,
7188
cancel: cancel,
7289
prev: prev,
7390
next: null,

packages/react-reconciler/src/ReactFiberHooks.js

+38-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import type {
1616
Awaited,
1717
StartGesture,
1818
GestureProvider,
19+
GestureOptions,
1920
} from 'shared/ReactTypes';
2021
import type {
2122
Fiber,
@@ -35,6 +36,7 @@ import {
3536
NotPendingTransition as NoPendingHostTransition,
3637
setCurrentUpdatePriority,
3738
getCurrentUpdatePriority,
39+
getCurrentGestureOffset,
3840
} from './ReactFiberConfig';
3941
import ReactSharedInternals from 'shared/ReactSharedInternals';
4042
import {
@@ -3988,6 +3990,7 @@ function startGesture(
39883990
fiber: Fiber,
39893991
queue: SwipeTransitionUpdateQueue,
39903992
gestureProvider: GestureProvider,
3993+
gestureOptions?: GestureOptions,
39913994
): () => void {
39923995
const root = enqueueGestureRender(fiber);
39933996
if (root === null) {
@@ -3998,10 +4001,44 @@ function startGesture(
39984001
};
39994002
}
40004003
const gestureTimeline: GestureTimeline = gestureProvider;
4004+
const currentOffset = getCurrentGestureOffset(gestureTimeline);
4005+
const range = gestureOptions && gestureOptions.range;
4006+
const rangePrevious: number = range ? range[0] : 0; // If no range is provider we assume it's the starting point of the range.
4007+
const rangeCurrent: number = range ? range[1] : currentOffset;
4008+
const rangeNext: number = range ? range[2] : 100; // If no range is provider we assume it's the starting point of the range.
4009+
if (__DEV__) {
4010+
if (
4011+
(rangePrevious > rangeCurrent && rangeNext > rangeCurrent) ||
4012+
(rangePrevious < rangeCurrent && rangeNext < rangeCurrent)
4013+
) {
4014+
console.error(
4015+
'The range of a gesture needs "previous" and "next" to be on either side of ' +
4016+
'the "current" offset. Both cannot be above current and both cannot be below current.',
4017+
);
4018+
}
4019+
}
4020+
const isFlippedDirection = rangePrevious > rangeNext;
4021+
const initialDirection =
4022+
// If a range is specified we can imply initial direction if it's not the current
4023+
// value such as if the gesture starts after it has already moved.
4024+
currentOffset < rangeCurrent
4025+
? isFlippedDirection
4026+
: currentOffset > rangeCurrent
4027+
? !isFlippedDirection
4028+
: // Otherwise, look for an explicit option.
4029+
gestureOptions && gestureOptions.direction === 'next'
4030+
? true
4031+
: gestureOptions && gestureOptions.direction === 'previous'
4032+
? false
4033+
: // If no option is specified, imply from the values specified.
4034+
queue.initialDirection;
40014035
const scheduledGesture = scheduleGesture(
40024036
root,
40034037
gestureTimeline,
4004-
queue.initialDirection,
4038+
initialDirection,
4039+
rangePrevious,
4040+
rangeCurrent,
4041+
rangeNext,
40054042
);
40064043
// Add this particular instance to the queue.
40074044
// We add multiple of the same timeline even if they get batched so

packages/react-reconciler/src/forks/ReactFiberConfig.custom.js

+1
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ export const wasInstanceInViewport = $$$config.wasInstanceInViewport;
145145
export const hasInstanceChanged = $$$config.hasInstanceChanged;
146146
export const hasInstanceAffectedParent = $$$config.hasInstanceAffectedParent;
147147
export const startViewTransition = $$$config.startViewTransition;
148+
export const getCurrentGestureOffset = $$$config.getCurrentGestureOffset;
148149
export const subscribeToGestureDirection =
149150
$$$config.subscribeToGestureDirection;
150151
export const createViewTransitionInstance =

packages/react-test-renderer/src/ReactFiberConfigTestHost.js

+5
Original file line numberDiff line numberDiff line change
@@ -393,8 +393,13 @@ export function getInstanceFromNode(mockNode: Object): Object | null {
393393

394394
export type GestureTimeline = null;
395395

396+
export function getCurrentGestureOffset(provider: GestureTimeline): number {
397+
return 0;
398+
}
399+
396400
export function subscribeToGestureDirection(
397401
provider: GestureTimeline,
402+
currentOffset: number,
398403
directionCallback: (direction: boolean) => void,
399404
): () => void {
400405
return () => {};

packages/shared/ReactTypes.js

+9-1
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,15 @@ export type ReactFormState<S, ReferenceId> = [
172172
// renderer supports it.
173173
export type GestureProvider = any;
174174

175-
export type StartGesture = (gestureProvider: GestureProvider) => () => void;
175+
export type StartGesture = (
176+
gestureProvider: GestureProvider,
177+
gestureOptions: GestureOptions,
178+
) => () => void;
179+
180+
export type GestureOptions = {
181+
direction?: 'previous' | 'next',
182+
range?: [/*previous*/ number, /*current*/ number, /*next*/ number],
183+
};
176184

177185
export type Awaited<T> = T extends null | void
178186
? T // special case for `null | undefined` when not in `--strictNullChecks` mode

0 commit comments

Comments
 (0)