Merge ViewTransition layout/onLayout props into update/onUpdate (#32723)

We currently have the ability to have a separate animation for a
ViewTransition that relayouts but doesn't actually have any internal
mutations. This can be useful if you want to separate just a move from
for example flashing an update.

However, we're concerned that this might be more confusion than its
worth because subtle differences in mutations can cause it to trigger
the other case. The existence of the property name might also make you
start looking for it to solve something that it's not meant for.

We already fallback to using the "update" property if it exists but
layout doesn't. So if we ever decide to add this back it would backwards
compatible. We've also shown in implementation that it can work.
This commit is contained in:
Sebastian Markbåge 2025-03-24 14:04:27 -04:00 committed by GitHub
parent 04bf10e6a9
commit 42a57ea802
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 16 additions and 114 deletions

View File

@ -308,7 +308,7 @@ function applyNestedViewTransition(child: Fiber): void {
const name = getViewTransitionName(props, state);
const className: ?string = getViewTransitionClassName(
props.className,
props.layout,
props.update,
);
if (className !== 'none') {
const clones = state.clones;
@ -335,17 +335,13 @@ function applyUpdateViewTransition(current: Fiber, finishedWork: Fiber): void {
// we would use. However, since this animation is going in reverse we actually
// want the props from "current" since that's the class that would've won if
// it was the normal direction. To preserve the same effect in either direction.
let className: ?string = getViewTransitionClassName(
const className: ?string = getViewTransitionClassName(
newProps.className,
newProps.update,
);
if (className === 'none') {
className = getViewTransitionClassName(newProps.className, newProps.layout);
if (className === 'none') {
// If both update and layout are both "none" then we don't have to
// apply a name. Since we won't animate this boundary.
return;
}
// If update is "none" then we don't have to apply a name. Since we won't animate this boundary.
return;
}
const clones = state.clones;
// If there are no clones at this point, that should mean that there are no

View File

@ -469,17 +469,13 @@ export function commitBeforeUpdateViewTransition(
// For example, if update="foo" layout="none" and it turns out this was
// a layout only change, then the "foo" class will be applied even though
// it was not actually an update. Which is a bug.
let className: ?string = getViewTransitionClassName(
const className: ?string = getViewTransitionClassName(
newProps.className,
newProps.update,
);
if (className === 'none') {
className = getViewTransitionClassName(newProps.className, newProps.layout);
if (className === 'none') {
// If both update and layout are both "none" then we don't have to
// apply a name. Since we won't animate this boundary.
return;
}
// If update is "none" then we don't have to apply a name. Since we won't animate this boundary.
return;
}
applyViewTransitionToHostInstances(
current.child,
@ -500,7 +496,7 @@ export function commitNestedViewTransitions(changedParent: Fiber): void {
const name = getViewTransitionName(props, child.stateNode);
const className: ?string = getViewTransitionClassName(
props.className,
props.layout,
props.update,
);
if (className !== 'none') {
applyViewTransitionToHostInstances(
@ -590,61 +586,6 @@ export function restoreNestedViewTransitions(changedParent: Fiber): void {
}
}
export function cancelViewTransitionHostInstances(
child: null | Fiber,
oldName: string,
stopAtNestedViewTransitions: boolean,
): void {
viewTransitionHostInstanceIdx = 0;
cancelViewTransitionHostInstancesRecursive(
child,
oldName,
stopAtNestedViewTransitions,
);
}
function cancelViewTransitionHostInstancesRecursive(
child: null | Fiber,
oldName: string,
stopAtNestedViewTransitions: boolean,
): void {
if (!supportsMutation) {
return;
}
while (child !== null) {
if (child.tag === HostComponent) {
const instance: Instance = child.stateNode;
if (viewTransitionCancelableChildren === null) {
viewTransitionCancelableChildren = [];
}
viewTransitionCancelableChildren.push(
instance,
oldName,
child.memoizedProps,
);
viewTransitionHostInstanceIdx++;
} else if (
child.tag === OffscreenComponent &&
child.memoizedState !== null
) {
// Skip any hidden subtrees. They were or are effectively not there.
} else if (
child.tag === ViewTransitionComponent &&
stopAtNestedViewTransitions
) {
// Skip any nested view transitions for updates since in that case the
// inner most one is the one that handles the update.
} else {
cancelViewTransitionHostInstancesRecursive(
child.child,
oldName,
stopAtNestedViewTransitions,
);
}
child = child.sibling;
}
}
export function measureViewTransitionHostInstances(
parentViewTransition: Fiber,
child: null | Fiber,
@ -792,40 +733,14 @@ export function measureUpdateViewTransition(
const state: ViewTransitionState = newFiber.stateNode;
const newName = getViewTransitionName(props, state);
const oldName = getViewTransitionName(oldFiber.memoizedProps, state);
const updateClassName: ?string = getViewTransitionClassName(
// Whether it ends up having been updated or relayout we apply the update class name.
const className: ?string = getViewTransitionClassName(
props.className,
props.update,
);
const layoutClassName: ?string = getViewTransitionClassName(
props.className,
props.layout,
);
let className: ?string;
if (updateClassName === 'none') {
if (layoutClassName === 'none') {
// If both update and layout class name were none, then we didn't apply any
// names in the before update phase so we shouldn't now neither.
return false;
}
// We don't care if this is mutated or children layout changed, but we still
// measure each instance to see if it moved and therefore should apply layout.
finishedWork.flags &= ~Update;
className = layoutClassName;
} else if ((finishedWork.flags & Update) !== NoFlags) {
// It was updated and we have an appropriate class name to apply.
className = updateClassName;
} else {
if (layoutClassName === 'none') {
// If we did not update, then all changes are considered a layout. We'll
// attempt to cancel.
// This should use the Fiber that got names applied in the snapshot phase
// since those are the ones we're trying to cancel.
cancelViewTransitionHostInstances(oldFiber.child, oldName, true);
return false;
}
// We didn't update but we might still apply layout so we measure each
// instance to see if it moved or resized.
className = layoutClassName;
if (className === 'none') {
// If update is "none" then we don't have to apply a name. Since we won't animate this boundary.
return false;
}
// If nothing changed due to a mutation, or children changing size
// and the measurements end up unchanged, we should restore it to not animate.
@ -873,7 +788,7 @@ export function measureNestedViewTransitions(
const name = getViewTransitionName(props, state);
const className: ?string = getViewTransitionClassName(
props.className,
props.layout,
props.update,
);
let previousMeasurements: null | Array<InstanceMeasurement>;
if (gesture) {
@ -902,7 +817,7 @@ export function measureNestedViewTransitions(
if (gesture) {
// TODO: Schedule gesture events.
} else {
scheduleViewTransitionEvent(child, props.onLayout);
scheduleViewTransitionEvent(child, props.onUpdate);
}
}
} else if ((child.subtreeFlags & ViewTransitionStatic) !== NoFlags) {

View File

@ -2440,8 +2440,6 @@ function commitAfterMutationEffectsOnFiber(
break;
}
case ViewTransitionComponent: {
const wasMutated = (finishedWork.flags & Update) !== NoFlags;
const prevContextChanged = viewTransitionContextChanged;
const prevCancelableChildren = pushViewTransitionCancelableScope();
viewTransitionContextChanged = false;
@ -2477,12 +2475,7 @@ function commitAfterMutationEffectsOnFiber(
// then we should probably issue an event since this instance is part of it.
} else {
const props: ViewTransitionProps = finishedWork.memoizedProps;
scheduleViewTransitionEvent(
finishedWork,
wasMutated || viewTransitionContextChanged
? props.onUpdate
: props.onLayout,
);
scheduleViewTransitionEvent(finishedWork, props.onUpdate);
// If this boundary did update, we cannot cancel its children so those are dropped.
popViewTransitionCancelableScope(prevCancelableChildren);

View File

@ -32,12 +32,10 @@ export type ViewTransitionProps = {
className?: ViewTransitionClass,
enter?: ViewTransitionClass,
exit?: ViewTransitionClass,
layout?: ViewTransitionClass,
share?: ViewTransitionClass,
update?: ViewTransitionClass,
onEnter?: (instance: ViewTransitionInstance, types: Array<string>) => void,
onExit?: (instance: ViewTransitionInstance, types: Array<string>) => void,
onLayout?: (instance: ViewTransitionInstance, types: Array<string>) => void,
onShare?: (instance: ViewTransitionInstance, types: Array<string>) => void,
onUpdate?: (instance: ViewTransitionInstance, types: Array<string>) => void,
};