Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 4365fb25 authored by Evan Rosky's avatar Evan Rosky
Browse files

Port WindowChangeAnimationSpec to shell transitions

This was the default animation for window size changes
in legacy. This change ports it to Shell so it can
be used by the default transition handler for the same
purpose.

Also added support for animating (surface)view in
addition to the default surface-only version.

Bug: 384976265
Test: Animation-change, so existing tests.
Flag: com.android.window.flags.port_window_size_animation
Change-Id: Ibee129aa26371d32b14cb1ded9973b55929634d7
parent 3a63169f
Loading
Loading
Loading
Loading
+288 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.wm.shell.animation;

import static com.android.wm.shell.transition.DefaultSurfaceAnimator.setupValueAnimator;

import android.animation.Animator;
import android.animation.ValueAnimator;
import android.annotation.Nullable;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.view.Choreographer;
import android.view.SurfaceControl;
import android.view.View;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.view.animation.AnimationSet;
import android.view.animation.ClipRectAnimation;
import android.view.animation.ScaleAnimation;
import android.view.animation.Transformation;
import android.view.animation.TranslateAnimation;

import java.util.function.Consumer;

/**
 * Animation implementation for size-changing window container animations. Ported from
 * {@link com.android.server.wm.WindowChangeAnimationSpec}.
 * <p>
 * This animation behaves slightly differently depending on whether the window is growing
 * or shrinking:
 * <ul>
 * <li>If growing, it will do a clip-reveal after quicker fade-out/scale of the smaller (old)
 * snapshot.
 * <li>If shrinking, it will do an opposite clip-reveal on the old snapshot followed by a quicker
 * fade-out of the bigger (old) snapshot while simultaneously shrinking the new window into
 * place.
 * </ul>
 */
public class SizeChangeAnimation {
    private final Rect mTmpRect = new Rect();
    final Transformation mTmpTransform = new Transformation();
    final Matrix mTmpMatrix = new Matrix();
    final float[] mTmpFloats = new float[9];
    final float[] mTmpVecs = new float[4];

    private final Animation mAnimation;
    private final Animation mSnapshotAnim;

    private final ValueAnimator mAnimator = ValueAnimator.ofFloat(0f, 1f);

    /**
     * The maximum of stretching applied to any surface during interpolation (since the animation
     * is a combination of stretching/cropping/fading).
     */
    private static final float SCALE_FACTOR = 0.7f;

    /**
     * Since this animation is made of several sub-animations, we want to pre-arrange the
     * sub-animations on a "virtual timeline" and then drive the overall progress in lock-step.
     *
     * To do this, we have a single value-animator which animates progress from 0-1 with an
     * arbitrary duration and interpolator. Then we convert the progress to a frame in our virtual
     * timeline to get the interpolated transforms.
     *
     * The APIs for arranging the sub-animations use integral frame numbers, so we need to pick
     * an integral "duration" for our virtual timeline. That's what this constant specifies. It
     * is effectively an animation "resolution" since it divides-up the 0-1 interpolation-space.
     */
    private static final int ANIMATION_RESOLUTION = 1000;

    public SizeChangeAnimation(Rect startBounds, Rect endBounds) {
        mAnimation = buildContainerAnimation(startBounds, endBounds);
        mSnapshotAnim = buildSnapshotAnimation(startBounds, endBounds);
    }

    /**
     * Initialize a size-change animation for a container leash.
     */
    public void initialize(SurfaceControl leash, SurfaceControl snapshot,
            SurfaceControl.Transaction startT) {
        startT.reparent(snapshot, leash);
        startT.setPosition(snapshot, 0, 0);
        startT.show(snapshot);
        startT.show(leash);
        apply(startT, leash, snapshot, 0.f);
    }

    /**
     * Initialize a size-change animation for a view containing the leash surface(s).
     *
     * Note that this **will** apply {@param startToApply}!
     */
    public void initialize(View view, SurfaceControl leash, SurfaceControl snapshot,
            SurfaceControl.Transaction startToApply) {
        startToApply.reparent(snapshot, leash);
        startToApply.setPosition(snapshot, 0, 0);
        startToApply.show(snapshot);
        startToApply.show(leash);
        apply(view, startToApply, leash, snapshot, 0.f);
    }

    private ValueAnimator buildAnimatorInner(ValueAnimator.AnimatorUpdateListener updater,
            SurfaceControl leash, SurfaceControl snapshot, Consumer<Animator> onFinish,
            SurfaceControl.Transaction transaction, @Nullable View view) {
        return setupValueAnimator(mAnimator, updater, (anim) -> {
            transaction.reparent(snapshot, null);
            if (view != null) {
                view.setClipBounds(null);
                view.setAnimationMatrix(null);
                transaction.setCrop(leash, null);
            }
            transaction.apply();
            transaction.close();
            onFinish.accept(anim);
        });
    }

    /**
     * Build an animator which works on a pair of surface controls (where the snapshot is assumed
     * to be a child of the main leash).
     *
     * @param onFinish Called when animation finishes. This is called on the anim thread!
     */
    public ValueAnimator buildAnimator(SurfaceControl leash, SurfaceControl snapshot,
            Consumer<Animator> onFinish) {
        final SurfaceControl.Transaction transaction = new SurfaceControl.Transaction();
        Choreographer choreographer = Choreographer.getInstance();
        return buildAnimatorInner(animator -> {
            // The finish callback in buildSurfaceAnimation will ensure that the animation ends
            // with fraction 1.
            final float progress = Math.clamp(animator.getAnimatedFraction(), 0.f, 1.f);
            apply(transaction, leash, snapshot, progress);
            transaction.setFrameTimelineVsync(choreographer.getVsyncId());
            transaction.apply();
        }, leash, snapshot, onFinish, transaction, null /* view */);
    }

    /**
     * Build an animator which works on a view that contains a pair of surface controls (where
     * the snapshot is assumed to be a child of the main leash).
     *
     * @param onFinish Called when animation finishes. This is called on the anim thread!
     */
    public ValueAnimator buildViewAnimator(View view, SurfaceControl leash,
            SurfaceControl snapshot, Consumer<Animator> onFinish) {
        final SurfaceControl.Transaction transaction = new SurfaceControl.Transaction();
        return buildAnimatorInner(animator -> {
            // The finish callback in buildSurfaceAnimation will ensure that the animation ends
            // with fraction 1.
            final float progress = Math.clamp(animator.getAnimatedFraction(), 0.f, 1.f);
            apply(view, transaction, leash, snapshot, progress);
        }, leash, snapshot, onFinish, transaction, view);
    }

    /** Animation for the whole container (snapshot is inside this container). */
    private static AnimationSet buildContainerAnimation(Rect startBounds, Rect endBounds) {
        final long duration = ANIMATION_RESOLUTION;
        boolean growing = endBounds.width() - startBounds.width()
                + endBounds.height() - startBounds.height() >= 0;
        long scalePeriod = (long) (duration * SCALE_FACTOR);
        float startScaleX = SCALE_FACTOR * ((float) startBounds.width()) / endBounds.width()
                + (1.f - SCALE_FACTOR);
        float startScaleY = SCALE_FACTOR * ((float) startBounds.height()) / endBounds.height()
                + (1.f - SCALE_FACTOR);
        final AnimationSet animSet = new AnimationSet(true);

        final Animation scaleAnim = new ScaleAnimation(startScaleX, 1, startScaleY, 1);
        scaleAnim.setDuration(scalePeriod);
        if (!growing) {
            scaleAnim.setStartOffset(duration - scalePeriod);
        }
        animSet.addAnimation(scaleAnim);
        final Animation translateAnim = new TranslateAnimation(startBounds.left,
                endBounds.left, startBounds.top, endBounds.top);
        translateAnim.setDuration(duration);
        animSet.addAnimation(translateAnim);
        Rect startClip = new Rect(startBounds);
        Rect endClip = new Rect(endBounds);
        startClip.offsetTo(0, 0);
        endClip.offsetTo(0, 0);
        final Animation clipAnim = new ClipRectAnimation(startClip, endClip);
        clipAnim.setDuration(duration);
        animSet.addAnimation(clipAnim);
        animSet.initialize(startBounds.width(), startBounds.height(),
                endBounds.width(), endBounds.height());
        return animSet;
    }

    /** The snapshot surface is assumed to be a child of the container surface. */
    private static AnimationSet buildSnapshotAnimation(Rect startBounds, Rect endBounds) {
        final long duration = ANIMATION_RESOLUTION;
        boolean growing = endBounds.width() - startBounds.width()
                + endBounds.height() - startBounds.height() >= 0;
        long scalePeriod = (long) (duration * SCALE_FACTOR);
        float endScaleX = 1.f / (SCALE_FACTOR * ((float) startBounds.width()) / endBounds.width()
                + (1.f - SCALE_FACTOR));
        float endScaleY = 1.f / (SCALE_FACTOR * ((float) startBounds.height()) / endBounds.height()
                + (1.f - SCALE_FACTOR));

        AnimationSet snapAnimSet = new AnimationSet(true);
        // Animation for the "old-state" snapshot that is atop the task.
        final Animation snapAlphaAnim = new AlphaAnimation(1.f, 0.f);
        snapAlphaAnim.setDuration(scalePeriod);
        if (!growing) {
            snapAlphaAnim.setStartOffset(duration - scalePeriod);
        }
        snapAnimSet.addAnimation(snapAlphaAnim);
        final Animation snapScaleAnim =
                new ScaleAnimation(endScaleX, endScaleX, endScaleY, endScaleY);
        snapScaleAnim.setDuration(duration);
        snapAnimSet.addAnimation(snapScaleAnim);
        snapAnimSet.initialize(startBounds.width(), startBounds.height(),
                endBounds.width(), endBounds.height());
        return snapAnimSet;
    }

    private void calcCurrentClipBounds(Rect outClip, Transformation fromTransform) {
        // The following applies an inverse scale to the clip-rect so that it crops "after" the
        // scale instead of before.
        mTmpVecs[1] = mTmpVecs[2] = 0;
        mTmpVecs[0] = mTmpVecs[3] = 1;
        fromTransform.getMatrix().mapVectors(mTmpVecs);

        mTmpVecs[0] = 1.f / mTmpVecs[0];
        mTmpVecs[3] = 1.f / mTmpVecs[3];
        final Rect clipRect = fromTransform.getClipRect();
        outClip.left = (int) (clipRect.left * mTmpVecs[0] + 0.5f);
        outClip.right = (int) (clipRect.right * mTmpVecs[0] + 0.5f);
        outClip.top = (int) (clipRect.top * mTmpVecs[3] + 0.5f);
        outClip.bottom = (int) (clipRect.bottom * mTmpVecs[3] + 0.5f);
    }

    private void apply(SurfaceControl.Transaction t, SurfaceControl leash, SurfaceControl snapshot,
            float progress) {
        long currentPlayTime = (long) (((float) ANIMATION_RESOLUTION) * progress);
        // update thumbnail surface
        mSnapshotAnim.getTransformation(currentPlayTime, mTmpTransform);
        t.setMatrix(snapshot, mTmpTransform.getMatrix(), mTmpFloats);
        t.setAlpha(snapshot, mTmpTransform.getAlpha());

        // update container surface
        mAnimation.getTransformation(currentPlayTime, mTmpTransform);
        final Matrix matrix = mTmpTransform.getMatrix();
        t.setMatrix(leash, matrix, mTmpFloats);

        calcCurrentClipBounds(mTmpRect, mTmpTransform);
        t.setCrop(leash, mTmpRect);
    }

    private void apply(View view, SurfaceControl.Transaction tmpT, SurfaceControl leash,
            SurfaceControl snapshot, float progress) {
        long currentPlayTime = (long) (((float) ANIMATION_RESOLUTION) * progress);
        // update thumbnail surface
        mSnapshotAnim.getTransformation(currentPlayTime, mTmpTransform);
        tmpT.setMatrix(snapshot, mTmpTransform.getMatrix(), mTmpFloats);
        tmpT.setAlpha(snapshot, mTmpTransform.getAlpha());

        // update container surface
        mAnimation.getTransformation(currentPlayTime, mTmpTransform);
        final Matrix matrix = mTmpTransform.getMatrix();
        mTmpMatrix.set(matrix);
        // animationMatrix is applied after getTranslation, so "move" the translate to the end.
        mTmpMatrix.preTranslate(-view.getTranslationX(), -view.getTranslationY());
        mTmpMatrix.postTranslate(view.getTranslationX(), view.getTranslationY());
        view.setAnimationMatrix(mTmpMatrix);

        calcCurrentClipBounds(mTmpRect, mTmpTransform);
        tmpT.setCrop(leash, mTmpRect);
        view.setClipBounds(mTmpRect);

        // this takes stuff out of mTmpT so mTmpT can be re-used immediately
        view.getViewRootImpl().applyTransactionOnDraw(tmpT);
    }
}
+53 −36
Original line number Diff line number Diff line
@@ -33,6 +33,7 @@ import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.shared.TransactionPool;

import java.util.ArrayList;
import java.util.function.Consumer;

public class DefaultSurfaceAnimator {

@@ -58,42 +59,12 @@ public class DefaultSurfaceAnimator {
        // Animation length is already expected to be scaled.
        va.overrideDurationScale(1.0f);
        va.setDuration(anim.computeDurationHint());
        va.addUpdateListener(updateListener);
        va.addListener(new AnimatorListenerAdapter() {
            // It is possible for the end/cancel to be called more than once, which may cause
            // issues if the animating surface has already been released. Track the finished
            // state here to skip duplicate callbacks. See b/252872225.
            private boolean mFinished;

            @Override
            public void onAnimationEnd(Animator animation) {
                onFinish();
            }

            @Override
            public void onAnimationCancel(Animator animation) {
                onFinish();
            }

            private void onFinish() {
                if (mFinished) return;
                mFinished = true;
                // Apply transformation of end state in case the animation is canceled.
                if (va.getAnimatedFraction() < 1f) {
                    va.setCurrentFraction(1f);
                }

        setupValueAnimator(va, updateListener, (vanim) -> {
            pool.release(transaction);
            mainExecutor.execute(() -> {
                    animations.remove(va);
                animations.remove(vanim);
                finishCallback.run();
            });
                // The update listener can continue to be called after the animation has ended if
                // end() is called manually again before the finisher removes the animation.
                // Remove it manually here to prevent animating a released surface.
                // See b/252872225.
                va.removeUpdateListener(updateListener);
            }
        });
        animations.add(va);
    }
@@ -188,4 +159,50 @@ public class DefaultSurfaceAnimator {
            }
        }
    }

    /**
     * Setup some callback logic on a value-animator. This helper ensures that a value animator
     * finishes at its final fraction (1f) and that relevant callbacks are only called once.
     */
    public static ValueAnimator setupValueAnimator(ValueAnimator animator,
            ValueAnimator.AnimatorUpdateListener updateListener,
            Consumer<ValueAnimator> afterFinish) {
        animator.addUpdateListener(updateListener);
        animator.addListener(new AnimatorListenerAdapter() {
            // It is possible for the end/cancel to be called more than once, which may cause
            // issues if the animating surface has already been released. Track the finished
            // state here to skip duplicate callbacks. See b/252872225.
            private boolean mFinished;

            @Override
            public void onAnimationStart(Animator animation) {
            }

            @Override
            public void onAnimationEnd(Animator animation) {
                onFinish();
            }

            @Override
            public void onAnimationCancel(Animator animation) {
                onFinish();
            }

            private void onFinish() {
                if (mFinished) return;
                mFinished = true;
                // Apply transformation of end state in case the animation is canceled.
                if (animator.getAnimatedFraction() < 1f) {
                    animator.setCurrentFraction(1f);
                }
                afterFinish.accept(animator);
                // The update listener can continue to be called after the animation has ended if
                // end() is called manually again before the finisher removes the animation.
                // Remove it manually here to prevent animating a released surface.
                // See b/252872225.
                animator.removeUpdateListener(updateListener);
            }
        });
        return animator;
    }
}
+26 −0
Original line number Diff line number Diff line
@@ -55,6 +55,7 @@ import static android.window.TransitionInfo.FLAG_SHOW_WALLPAPER;
import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT;
import static android.window.TransitionInfo.FLAG_TRANSLUCENT;

import static com.android.internal.policy.TransitionAnimation.DEFAULT_APP_TRANSITION_DURATION;
import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_CHANGE;
import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_CLOSE;
import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_INTRA_CLOSE;
@@ -69,6 +70,7 @@ import static com.android.wm.shell.transition.TransitionAnimationHelper.isCovere
import static com.android.wm.shell.transition.TransitionAnimationHelper.loadAttributeAnimation;

import android.animation.Animator;
import android.animation.ValueAnimator;
import android.annotation.ColorInt;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -104,6 +106,7 @@ import com.android.internal.policy.TransitionAnimation;
import com.android.internal.protolog.ProtoLog;
import com.android.window.flags.Flags;
import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
import com.android.wm.shell.animation.SizeChangeAnimation;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.common.ShellExecutor;
@@ -422,6 +425,14 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
                            ROTATION_ANIMATION_ROTATE, 0 /* flags */, animations, onAnimFinish);
                    continue;
                }

                if (Flags.portWindowSizeAnimation() && isTask
                        && TransitionInfo.isIndependent(change, info)
                        && change.getSnapshot() != null) {
                    startBoundsChangeAnimation(startTransaction, animations, change, onAnimFinish,
                            mMainExecutor);
                    continue;
                }
            }

            // Hide the invisible surface directly without animating it if there is a display
@@ -734,6 +745,21 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler {
        }
    }

    private void startBoundsChangeAnimation(@NonNull SurfaceControl.Transaction startT,
            @NonNull ArrayList<Animator> animations, @NonNull TransitionInfo.Change change,
            @NonNull Runnable finishCb, @NonNull ShellExecutor mainExecutor) {
        final SizeChangeAnimation sca =
                new SizeChangeAnimation(change.getStartAbsBounds(), change.getEndAbsBounds());
        sca.initialize(change.getLeash(), change.getSnapshot(), startT);
        final ValueAnimator va = sca.buildAnimator(change.getLeash(), change.getSnapshot(),
                (animator) -> mainExecutor.execute(() -> {
                    animations.remove(animator);
                    finishCb.run();
                }));
        va.setDuration(DEFAULT_APP_TRANSITION_DURATION);
        animations.add(va);
    }

    @Nullable
    @Override
    public WindowContainerTransaction handleRequest(@NonNull IBinder transition,