Skip to content

Commit 4028aaa

Browse files
authored
Commit the Gesture lane if a gesture ends closer to the target state (#35486)
Stacked on #35485. Before this PR, the `startGestureTransition` API would itself never commit its state. After the gesture releases it stops the animation in the next commit which just leaves the DOM tree in the original state. If there's an actual state change from the Action then that's committed as the new DOM tree. To avoid animating from the original state to the new state again, this is DOM without an animation. However, this means that you can't have the actual action committing be in a slightly different state and animate between the final gesture state and into the new action. Instead, we now actually keep the render tree around and commit it in the end. Basically we assume that if the Timeline was closer to the end then visually you're already there and we can commit into that state. Most of the time this will be at the actual end state when you release but if you have something else cancelling the gesture (e.g. `touchcancel`) it can still commit this state even though your gesture recognizer might not consider this an Action. I think this is ok and keeps it simple. When the gesture lane commits, it'll leave a Transition behind as work from the revert lanes on the Optimistic updates. This means that if you don't do anything in the Action this will cause another commit right after which reverts. This revert can animate the snap back. There's a few fixes needed in follow up PRs: - Fixed in #35487. ~To support unentangled Transitions we need to explicitly entangle the revert lane with the Action to avoid committing a revert followed by a forward instead of committing the forward entangled with the revert. This just works now since everything is entangled but won't work with #35392.~ - Fixed in #35510. ~This currently rerenders the gesture lane once before committing if it was already completed but blocked. We should be able to commit the already completed tree as is.~
1 parent f0fbb0d commit 4028aaa

6 files changed

Lines changed: 271 additions & 156 deletions

File tree

fixtures/view-transition/src/components/Page.js

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -171,17 +171,20 @@ export default function Page({url, navigate}) {
171171
}}>
172172
<h1>{!show ? 'A' + counter : 'B'}</h1>
173173
</ViewTransition>
174-
{show ? (
175-
<div>
176-
{a}
177-
{b}
178-
</div>
179-
) : (
180-
<div>
181-
{b}
182-
{a}
183-
</div>
184-
)}
174+
{
175+
// Using url instead of renderedUrl here lets us only update this on commit.
176+
url === '/?b' ? (
177+
<div>
178+
{a}
179+
{b}
180+
</div>
181+
) : (
182+
<div>
183+
{b}
184+
{a}
185+
</div>
186+
)
187+
}
185188
<ViewTransition>
186189
{show ? (
187190
<div>hello{exclamation}</div>

packages/react-reconciler/src/ReactFiberGestureScheduler.js

Lines changed: 82 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,10 @@ import type {GestureOptions} from 'shared/ReactTypes';
1212
import type {GestureTimeline, RunningViewTransition} from './ReactFiberConfig';
1313
import type {TransitionTypes} from 'react/src/ReactTransitionType';
1414

15-
import {
16-
GestureLane,
17-
includesBlockingLane,
18-
includesTransitionLane,
19-
} from './ReactFiberLane';
15+
import {GestureLane, markRootFinished, NoLane, NoLanes} from './ReactFiberLane';
2016
import {ensureRootIsScheduled} from './ReactFiberRootScheduler';
2117
import {getCurrentGestureOffset, stopViewTransition} from './ReactFiberConfig';
18+
import {pingGestureRoot, restartGestureRoot} from './ReactFiberWorkLoop';
2219

2320
// This type keeps track of any scheduled or active gestures.
2421
export type ScheduledGesture = {
@@ -28,6 +25,7 @@ export type ScheduledGesture = {
2825
rangeEnd: number, // The percentage along the timeline where the "destination" state is reached.
2926
types: null | TransitionTypes, // Any addTransitionType call made during startGestureTransition.
3027
running: null | RunningViewTransition, // Used to cancel the running transition after we're done.
28+
committing: boolean, // If the gesture was released in a committed state and should actually commit.
3129
prev: null | ScheduledGesture, // The previous scheduled gesture in the queue for this root.
3230
next: null | ScheduledGesture, // The next scheduled gesture in the queue for this root.
3331
};
@@ -55,6 +53,7 @@ export function scheduleGesture(
5553
rangeEnd: 100, // Uninitialized
5654
types: null,
5755
running: null,
56+
committing: false,
5857
prev: prev,
5958
next: null,
6059
};
@@ -122,77 +121,98 @@ export function cancelScheduledGesture(
122121
): void {
123122
gesture.count--;
124123
if (gesture.count === 0) {
125-
// Delete the scheduled gesture from the pending queue.
126-
deleteScheduledGesture(root, gesture);
124+
// If the end state is closer to the end than the beginning then we commit into the
125+
// end state before reverting back (or applying a new Transition).
126+
// Otherwise we just revert back and don't commit.
127+
let shouldCommit: boolean;
128+
const finalOffset = getCurrentGestureOffset(gesture.provider);
129+
const rangeStart = gesture.rangeStart;
130+
const rangeEnd = gesture.rangeEnd;
131+
if (rangeStart < rangeEnd) {
132+
shouldCommit = finalOffset > rangeStart + (rangeEnd - rangeStart) / 2;
133+
} else {
134+
shouldCommit = finalOffset < rangeEnd + (rangeStart - rangeEnd) / 2;
135+
}
127136
// TODO: If we're currently rendering this gesture, we need to restart the render
128137
// on a different gesture or cancel the render..
129138
// TODO: We might want to pause the View Transition at this point since you should
130139
// no longer be able to update the position of anything but it might be better to
131140
// just commit the gesture state.
132141
const runningTransition = gesture.running;
133-
if (runningTransition !== null) {
134-
const pendingLanesExcludingGestureLane = root.pendingLanes & ~GestureLane;
135-
if (
136-
includesBlockingLane(pendingLanesExcludingGestureLane) ||
137-
includesTransitionLane(pendingLanesExcludingGestureLane)
138-
) {
139-
// If we have pending work we schedule the gesture to be stopped at the next commit.
140-
// This ensures that we don't snap back to the previous state until we have
141-
// had a chance to commit any resulting updates.
142-
const existing = root.stoppingGestures;
143-
if (existing !== null) {
144-
gesture.next = existing;
145-
existing.prev = gesture;
142+
if (runningTransition !== null && shouldCommit) {
143+
// If we are going to commit this gesture in its to state, we need to wait to
144+
// stop it until it commits. We should now schedule a render at the gesture
145+
// lane to actually commit it.
146+
gesture.committing = true;
147+
if (root.pendingGestures === gesture) {
148+
// Ping the root given the new state. This is similar to pingSuspendedRoot.
149+
// This will either schedule the gesture lane to be committed possibly from its current state.
150+
pingGestureRoot(root);
151+
}
152+
} else {
153+
// If we're not going to commit this gesture we can stop the View Transition
154+
// right away and delete the scheduled gesture from the pending queue.
155+
if (gesture.prev === null) {
156+
if (root.pendingGestures === gesture) {
157+
// This was the currently rendering gesture.
158+
root.pendingGestures = gesture.next;
159+
let remainingLanes = root.pendingLanes;
160+
if (root.pendingGestures === null) {
161+
// Gestures don't clear their lanes while the gesture is still active but it
162+
// might not be scheduled to do any more renders and so we shouldn't schedule
163+
// any more gesture lane work until a new gesture is scheduled.
164+
remainingLanes &= ~GestureLane;
165+
}
166+
markRootFinished(
167+
root,
168+
GestureLane,
169+
remainingLanes,
170+
NoLane,
171+
NoLane,
172+
NoLanes,
173+
);
174+
// If we had a currently rendering gesture we need to now reset the gesture lane to
175+
// now render the next gesture or cancel if there's no more gestures in the queue.
176+
restartGestureRoot(root);
146177
}
147-
root.stoppingGestures = gesture;
148-
} else {
149178
gesture.running = null;
150-
// If there's no work scheduled so we can stop the View Transition right away.
151-
stopViewTransition(runningTransition);
179+
if (runningTransition !== null) {
180+
stopViewTransition(runningTransition);
181+
}
182+
} else {
183+
// This was not the current gesture so it doesn't affect the current render.
184+
gesture.prev.next = gesture.next;
185+
if (gesture.next !== null) {
186+
gesture.next.prev = gesture.prev;
187+
}
188+
gesture.prev = null;
189+
gesture.next = null;
152190
}
153191
}
154192
}
155193
}
156194

157-
export function deleteScheduledGesture(
158-
root: FiberRoot,
159-
gesture: ScheduledGesture,
160-
): void {
161-
if (gesture.prev === null) {
162-
if (root.pendingGestures === gesture) {
163-
root.pendingGestures = gesture.next;
164-
if (root.pendingGestures === null) {
165-
// Gestures don't clear their lanes while the gesture is still active but it
166-
// might not be scheduled to do any more renders and so we shouldn't schedule
167-
// any more gesture lane work until a new gesture is scheduled.
168-
root.pendingLanes &= ~GestureLane;
169-
}
170-
}
171-
if (root.stoppingGestures === gesture) {
172-
// This should not really happen the way we use it now but just in case we start.
173-
root.stoppingGestures = gesture.next;
174-
}
175-
} else {
176-
gesture.prev.next = gesture.next;
177-
if (gesture.next !== null) {
178-
gesture.next.prev = gesture.prev;
195+
export function stopCommittedGesture(root: FiberRoot) {
196+
// The top was just committed. We can delete it from the queue
197+
// and stop its View Transition now.
198+
const committedGesture = root.pendingGestures;
199+
if (committedGesture !== null) {
200+
// Mark it as no longer committing and should no longer be included in rerenders.
201+
committedGesture.committing = false;
202+
const nextGesture = committedGesture.next;
203+
if (nextGesture === null) {
204+
// Gestures don't clear their lanes while the gesture is still active but it
205+
// might not be scheduled to do any more renders and so we shouldn't schedule
206+
// any more gesture lane work until a new gesture is scheduled.
207+
root.pendingLanes &= ~GestureLane;
208+
} else {
209+
nextGesture.prev = null;
179210
}
180-
gesture.prev = null;
181-
gesture.next = null;
182-
}
183-
}
184-
185-
export function stopCompletedGestures(root: FiberRoot) {
186-
let gesture = root.stoppingGestures;
187-
root.stoppingGestures = null;
188-
while (gesture !== null) {
189-
if (gesture.running !== null) {
190-
stopViewTransition(gesture.running);
191-
gesture.running = null;
211+
root.pendingGestures = nextGesture;
212+
const runningTransition = committedGesture.running;
213+
if (runningTransition !== null) {
214+
committedGesture.running = null;
215+
stopViewTransition(runningTransition);
192216
}
193-
const nextGesture = gesture.next;
194-
gesture.next = null;
195-
gesture.prev = null;
196-
gesture = nextGesture;
197217
}
198218
}

packages/react-reconciler/src/ReactFiberHooks.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1384,7 +1384,7 @@ function updateReducerImpl<S, A>(
13841384
// ScheduledGesture.
13851385
const scheduledGesture = update.gesture;
13861386
if (scheduledGesture !== null) {
1387-
if (scheduledGesture.count === 0) {
1387+
if (scheduledGesture.count === 0 && !scheduledGesture.committing) {
13881388
// This gesture has already been cancelled. We can clean up this update.
13891389
update = update.next;
13901390
continue;

packages/react-reconciler/src/ReactFiberRoot.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,6 @@ function FiberRootNode(
115115

116116
if (enableGestureTransition) {
117117
this.pendingGestures = null;
118-
this.stoppingGestures = null;
119118
this.gestureClone = null;
120119
}
121120

0 commit comments

Comments
 (0)