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

Commit c73dda36 authored by Chris Li's avatar Chris Li Committed by Automerger Merge Worker
Browse files

Merge "Animate ActivityEmbedding transition with Shell Transition" into...

Merge "Animate ActivityEmbedding transition with Shell Transition" into tm-qpr-dev am: 27ac3885 am: db8e3b19

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/19691237



Change-Id: Ibe0c526301acbe1d3fe8effe7d861dcc09466b4c
Signed-off-by: default avatarAutomerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
parents ba58e63d db8e3b19
Loading
Loading
Loading
Loading
+234 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 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.activityembedding;

import static android.graphics.Matrix.MSCALE_X;
import static android.graphics.Matrix.MTRANS_X;
import static android.graphics.Matrix.MTRANS_Y;

import android.annotation.CallSuper;
import android.graphics.Point;
import android.graphics.Rect;
import android.view.Choreographer;
import android.view.SurfaceControl;
import android.view.animation.Animation;
import android.view.animation.Transformation;
import android.window.TransitionInfo;

import androidx.annotation.NonNull;

/**
 * Wrapper to handle the ActivityEmbedding animation update in one
 * {@link SurfaceControl.Transaction}.
 */
class ActivityEmbeddingAnimationAdapter {

    /**
     * If {@link #mOverrideLayer} is set to this value, we don't want to override the surface layer.
     */
    private static final int LAYER_NO_OVERRIDE = -1;

    final Animation mAnimation;
    final TransitionInfo.Change mChange;
    final SurfaceControl mLeash;

    final Transformation mTransformation = new Transformation();
    final float[] mMatrix = new float[9];
    final float[] mVecs = new float[4];
    final Rect mRect = new Rect();
    private boolean mIsFirstFrame = true;
    private int mOverrideLayer = LAYER_NO_OVERRIDE;

    ActivityEmbeddingAnimationAdapter(@NonNull Animation animation,
            @NonNull TransitionInfo.Change change) {
        this(animation, change, change.getLeash());
    }

    /**
     * @param leash the surface to animate, which is not necessary the same as
     * {@link TransitionInfo.Change#getLeash()}, it can be a screenshot for example.
     */
    ActivityEmbeddingAnimationAdapter(@NonNull Animation animation,
            @NonNull TransitionInfo.Change change, @NonNull SurfaceControl leash) {
        mAnimation = animation;
        mChange = change;
        mLeash = leash;
    }

    /**
     * Surface layer to be set at the first frame of the animation. We will not set the layer if it
     * is set to {@link #LAYER_NO_OVERRIDE}.
     */
    final void overrideLayer(int layer) {
        mOverrideLayer = layer;
    }

    /** Called on frame update. */
    final void onAnimationUpdate(@NonNull SurfaceControl.Transaction t, long currentPlayTime) {
        if (mIsFirstFrame) {
            t.show(mLeash);
            if (mOverrideLayer != LAYER_NO_OVERRIDE) {
                t.setLayer(mLeash, mOverrideLayer);
            }
            mIsFirstFrame = false;
        }

        // Extract the transformation to the current time.
        mAnimation.getTransformation(Math.min(currentPlayTime, mAnimation.getDuration()),
                mTransformation);
        t.setFrameTimelineVsync(Choreographer.getInstance().getVsyncId());
        onAnimationUpdateInner(t);
    }

    /** To be overridden by subclasses to adjust the animation surface change. */
    void onAnimationUpdateInner(@NonNull SurfaceControl.Transaction t) {
        final Point offset = mChange.getEndRelOffset();
        mTransformation.getMatrix().postTranslate(offset.x, offset.y);
        t.setMatrix(mLeash, mTransformation.getMatrix(), mMatrix);
        t.setAlpha(mLeash, mTransformation.getAlpha());
        // Get current animation position.
        final int positionX = Math.round(mMatrix[MTRANS_X]);
        final int positionY = Math.round(mMatrix[MTRANS_Y]);
        // The exiting surface starts at position: Change#getEndRelOffset() and moves with
        // positionX varying. Offset our crop region by the amount we have slided so crop
        // regions stays exactly on the original container in split.
        final int cropOffsetX = offset.x - positionX;
        final int cropOffsetY = offset.y - positionY;
        final Rect cropRect = new Rect();
        cropRect.set(mChange.getEndAbsBounds());
        // Because window crop uses absolute position.
        cropRect.offsetTo(0, 0);
        cropRect.offset(cropOffsetX, cropOffsetY);
        t.setCrop(mLeash, cropRect);
    }

    /** Called after animation finished. */
    @CallSuper
    void onAnimationEnd(@NonNull SurfaceControl.Transaction t) {
        onAnimationUpdate(t, mAnimation.getDuration());
    }

    final long getDurationHint() {
        return mAnimation.computeDurationHint();
    }

    /**
     * Should be used when the {@link TransitionInfo.Change} is in split with others, and wants to
     * animate together as one. This adapter will offset the animation leash to make the animate of
     * two windows look like a single window.
     */
    static class SplitAdapter extends ActivityEmbeddingAnimationAdapter {
        private final boolean mIsLeftHalf;
        private final int mWholeAnimationWidth;

        /**
         * @param isLeftHalf whether this is the left half of the animation.
         * @param wholeAnimationWidth the whole animation windows width.
         */
        SplitAdapter(@NonNull Animation animation, @NonNull TransitionInfo.Change change,
                boolean isLeftHalf, int wholeAnimationWidth) {
            super(animation, change);
            mIsLeftHalf = isLeftHalf;
            mWholeAnimationWidth = wholeAnimationWidth;
            if (wholeAnimationWidth == 0) {
                throw new IllegalArgumentException("SplitAdapter must provide wholeAnimationWidth");
            }
        }

        @Override
        void onAnimationUpdateInner(@NonNull SurfaceControl.Transaction t) {
            final Point offset = mChange.getEndRelOffset();
            float posX = offset.x;
            final float posY = offset.y;
            // This window is half of the whole animation window. Offset left/right to make it
            // look as one with the other half.
            mTransformation.getMatrix().getValues(mMatrix);
            final int changeWidth = mChange.getEndAbsBounds().width();
            final float scaleX = mMatrix[MSCALE_X];
            final float totalOffset = mWholeAnimationWidth * (1 - scaleX) / 2;
            final float curOffset = changeWidth * (1 - scaleX) / 2;
            final float offsetDiff = totalOffset - curOffset;
            if (mIsLeftHalf) {
                posX += offsetDiff;
            } else {
                posX -= offsetDiff;
            }
            mTransformation.getMatrix().postTranslate(posX, posY);
            t.setMatrix(mLeash, mTransformation.getMatrix(), mMatrix);
            t.setAlpha(mLeash, mTransformation.getAlpha());
        }
    }

    /**
     * Should be used for the animation of the snapshot of a {@link TransitionInfo.Change} that has
     * size change.
     */
    static class SnapshotAdapter extends ActivityEmbeddingAnimationAdapter {

        SnapshotAdapter(@NonNull Animation animation, @NonNull TransitionInfo.Change change,
                @NonNull SurfaceControl snapshotLeash) {
            super(animation, change, snapshotLeash);
        }

        @Override
        void onAnimationUpdateInner(@NonNull SurfaceControl.Transaction t) {
            // Snapshot should always be placed at the top left of the animation leash.
            mTransformation.getMatrix().postTranslate(0, 0);
            t.setMatrix(mLeash, mTransformation.getMatrix(), mMatrix);
            t.setAlpha(mLeash, mTransformation.getAlpha());
        }

        @Override
        void onAnimationEnd(@NonNull SurfaceControl.Transaction t) {
            super.onAnimationEnd(t);
            // Remove the screenshot leash after animation is finished.
            t.remove(mLeash);
        }
    }

    /**
     * Should be used for the animation of the {@link TransitionInfo.Change} that has size change.
     */
    static class BoundsChangeAdapter extends ActivityEmbeddingAnimationAdapter {

        BoundsChangeAdapter(@NonNull Animation animation, @NonNull TransitionInfo.Change change) {
            super(animation, change);
        }

        @Override
        void onAnimationUpdateInner(@NonNull SurfaceControl.Transaction t) {
            final Point offset = mChange.getEndRelOffset();
            mTransformation.getMatrix().postTranslate(offset.x, offset.y);
            t.setMatrix(mLeash, mTransformation.getMatrix(), mMatrix);
            t.setAlpha(mLeash, mTransformation.getAlpha());

            // The following applies an inverse scale to the clip-rect so that it crops "after" the
            // scale instead of before.
            mVecs[1] = mVecs[2] = 0;
            mVecs[0] = mVecs[3] = 1;
            mTransformation.getMatrix().mapVectors(mVecs);
            mVecs[0] = 1.f / mVecs[0];
            mVecs[3] = 1.f / mVecs[3];
            final Rect clipRect = mTransformation.getClipRect();
            mRect.left = (int) (clipRect.left * mVecs[0] + 0.5f);
            mRect.right = (int) (clipRect.right * mVecs[0] + 0.5f);
            mRect.top = (int) (clipRect.top * mVecs[3] + 0.5f);
            mRect.bottom = (int) (clipRect.bottom * mVecs[3] + 0.5f);
            t.setCrop(mLeash, mRect);
        }
    }
}
+290 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 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.activityembedding;

import static android.view.WindowManager.TRANSIT_CHANGE;
import static android.view.WindowManagerPolicyConstants.TYPE_LAYER_OFFSET;

import android.animation.Animator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Point;
import android.graphics.Rect;
import android.os.IBinder;
import android.util.Log;
import android.view.SurfaceControl;
import android.view.animation.Animation;
import android.window.TransitionInfo;
import android.window.WindowContainerToken;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.android.internal.annotations.VisibleForTesting;
import com.android.wm.shell.common.ScreenshotUtils;
import com.android.wm.shell.transition.Transitions;

import java.util.ArrayList;
import java.util.List;
import java.util.function.BiFunction;

/** To run the ActivityEmbedding animations. */
class ActivityEmbeddingAnimationRunner {

    private static final String TAG = "ActivityEmbeddingAnimR";

    private final ActivityEmbeddingController mController;
    @VisibleForTesting
    final ActivityEmbeddingAnimationSpec mAnimationSpec;

    ActivityEmbeddingAnimationRunner(@NonNull Context context,
            @NonNull ActivityEmbeddingController controller) {
        mController = controller;
        mAnimationSpec = new ActivityEmbeddingAnimationSpec(context);
    }

    /** Creates and starts animation for ActivityEmbedding transition. */
    void startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
            @NonNull SurfaceControl.Transaction startTransaction,
            @NonNull SurfaceControl.Transaction finishTransaction) {
        final Animator animator = createAnimator(info, startTransaction, finishTransaction,
                () -> mController.onAnimationFinished(transition));
        startTransaction.apply();
        animator.start();
    }

    /**
     * Sets transition animation scale settings value.
     * @param scale The setting value of transition animation scale.
     */
    void setAnimScaleSetting(float scale) {
        mAnimationSpec.setAnimScaleSetting(scale);
    }

    /** Creates the animator for the given {@link TransitionInfo}. */
    @VisibleForTesting
    @NonNull
    Animator createAnimator(@NonNull TransitionInfo info,
            @NonNull SurfaceControl.Transaction startTransaction,
            @NonNull SurfaceControl.Transaction finishTransaction,
            @NonNull Runnable animationFinishCallback) {
        final List<ActivityEmbeddingAnimationAdapter> adapters =
                createAnimationAdapters(info, startTransaction);
        long duration = 0;
        for (ActivityEmbeddingAnimationAdapter adapter : adapters) {
            duration = Math.max(duration, adapter.getDurationHint());
        }
        final ValueAnimator animator = ValueAnimator.ofFloat(0, 1);
        animator.setDuration(duration);
        animator.addUpdateListener((anim) -> {
            // Update all adapters in the same transaction.
            final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
            for (ActivityEmbeddingAnimationAdapter adapter : adapters) {
                adapter.onAnimationUpdate(t, animator.getCurrentPlayTime());
            }
            t.apply();
        });
        animator.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {}

            @Override
            public void onAnimationEnd(Animator animation) {
                final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
                for (ActivityEmbeddingAnimationAdapter adapter : adapters) {
                    adapter.onAnimationEnd(t);
                }
                t.apply();
                animationFinishCallback.run();
            }

            @Override
            public void onAnimationCancel(Animator animation) {}

            @Override
            public void onAnimationRepeat(Animator animation) {}
        });
        return animator;
    }

    /**
     * Creates list of {@link ActivityEmbeddingAnimationAdapter} to handle animations on all window
     * changes.
     */
    @NonNull
    private List<ActivityEmbeddingAnimationAdapter> createAnimationAdapters(
            @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction) {
        for (TransitionInfo.Change change : info.getChanges()) {
            if (change.getMode() == TRANSIT_CHANGE
                    && !change.getStartAbsBounds().equals(change.getEndAbsBounds())) {
                return createChangeAnimationAdapters(info, startTransaction);
            }
        }
        if (Transitions.isClosingType(info.getType())) {
            return createCloseAnimationAdapters(info);
        }
        return createOpenAnimationAdapters(info);
    }

    @NonNull
    private List<ActivityEmbeddingAnimationAdapter> createOpenAnimationAdapters(
            @NonNull TransitionInfo info) {
        return createOpenCloseAnimationAdapters(info, true /* isOpening */,
                mAnimationSpec::loadOpenAnimation);
    }

    @NonNull
    private List<ActivityEmbeddingAnimationAdapter> createCloseAnimationAdapters(
            @NonNull TransitionInfo info) {
        return createOpenCloseAnimationAdapters(info, false /* isOpening */,
                mAnimationSpec::loadCloseAnimation);
    }

    /**
     * Creates {@link ActivityEmbeddingAnimationAdapter} for OPEN and CLOSE types of transition.
     * @param isOpening {@code true} for OPEN type, {@code false} for CLOSE type.
     */
    @NonNull
    private List<ActivityEmbeddingAnimationAdapter> createOpenCloseAnimationAdapters(
            @NonNull TransitionInfo info, boolean isOpening,
            @NonNull BiFunction<TransitionInfo.Change, Rect, Animation> animationProvider) {
        // We need to know if the change window is only a partial of the whole animation screen.
        // If so, we will need to adjust it to make the whole animation screen looks like one.
        final List<TransitionInfo.Change> openingChanges = new ArrayList<>();
        final List<TransitionInfo.Change> closingChanges = new ArrayList<>();
        final Rect openingWholeScreenBounds = new Rect();
        final Rect closingWholeScreenBounds = new Rect();
        for (TransitionInfo.Change change : info.getChanges()) {
            final Rect bounds = new Rect(change.getEndAbsBounds());
            final Point offset = change.getEndRelOffset();
            bounds.offsetTo(offset.x, offset.y);
            if (Transitions.isOpeningType(change.getMode())) {
                openingChanges.add(change);
                openingWholeScreenBounds.union(bounds);
            } else {
                closingChanges.add(change);
                closingWholeScreenBounds.union(bounds);
            }
        }

        // For OPEN transition, open windows should be above close windows.
        // For CLOSE transition, open windows should be below close windows.
        int offsetLayer = TYPE_LAYER_OFFSET;
        final List<ActivityEmbeddingAnimationAdapter> adapters = new ArrayList<>();
        for (TransitionInfo.Change change : openingChanges) {
            final ActivityEmbeddingAnimationAdapter adapter = createOpenCloseAnimationAdapter(
                    change, animationProvider, openingWholeScreenBounds);
            if (isOpening) {
                adapter.overrideLayer(offsetLayer++);
            }
            adapters.add(adapter);
        }
        for (TransitionInfo.Change change : closingChanges) {
            final ActivityEmbeddingAnimationAdapter adapter = createOpenCloseAnimationAdapter(
                    change, animationProvider, closingWholeScreenBounds);
            if (!isOpening) {
                adapter.overrideLayer(offsetLayer++);
            }
            adapters.add(adapter);
        }
        return adapters;
    }

    @NonNull
    private ActivityEmbeddingAnimationAdapter createOpenCloseAnimationAdapter(
            @NonNull TransitionInfo.Change change,
            @NonNull BiFunction<TransitionInfo.Change, Rect, Animation> animationProvider,
            @NonNull Rect wholeAnimationBounds) {
        final Animation animation = animationProvider.apply(change, wholeAnimationBounds);
        final Rect bounds = new Rect(change.getEndAbsBounds());
        final Point offset = change.getEndRelOffset();
        bounds.offsetTo(offset.x, offset.y);
        if (bounds.left == wholeAnimationBounds.left
                && bounds.right != wholeAnimationBounds.right) {
            // This is the left split of the whole animation window.
            return new ActivityEmbeddingAnimationAdapter.SplitAdapter(animation, change,
                    true /* isLeftHalf */, wholeAnimationBounds.width());
        } else if (bounds.left != wholeAnimationBounds.left
                && bounds.right == wholeAnimationBounds.right) {
            // This is the right split of the whole animation window.
            return new ActivityEmbeddingAnimationAdapter.SplitAdapter(animation, change,
                    false /* isLeftHalf */, wholeAnimationBounds.width());
        }
        // Open/close window that fills the whole animation.
        return new ActivityEmbeddingAnimationAdapter(animation, change);
    }

    @NonNull
    private List<ActivityEmbeddingAnimationAdapter> createChangeAnimationAdapters(
            @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction) {
        final List<ActivityEmbeddingAnimationAdapter> adapters = new ArrayList<>();
        for (TransitionInfo.Change change : info.getChanges()) {
            if (change.getMode() == TRANSIT_CHANGE
                    && !change.getStartAbsBounds().equals(change.getEndAbsBounds())) {
                // This is the window with bounds change.
                final WindowContainerToken parentToken = change.getParent();
                final Rect parentBounds;
                if (parentToken != null) {
                    TransitionInfo.Change parentChange = info.getChange(parentToken);
                    parentBounds = parentChange != null
                            ? parentChange.getEndAbsBounds()
                            : change.getEndAbsBounds();
                } else {
                    parentBounds = change.getEndAbsBounds();
                }
                final Animation[] animations =
                        mAnimationSpec.createChangeBoundsChangeAnimations(change, parentBounds);
                // Adapter for the starting screenshot leash.
                final SurfaceControl screenshotLeash = createScreenshot(change, startTransaction);
                if (screenshotLeash != null) {
                    // The screenshot leash will be removed in SnapshotAdapter#onAnimationEnd
                    adapters.add(new ActivityEmbeddingAnimationAdapter.SnapshotAdapter(
                            animations[0], change, screenshotLeash));
                } else {
                    Log.e(TAG, "Failed to take screenshot for change=" + change);
                }
                // Adapter for the ending bounds changed leash.
                adapters.add(new ActivityEmbeddingAnimationAdapter.BoundsChangeAdapter(
                        animations[1], change));
                continue;
            }

            // These are the other windows that don't have bounds change in the same transition.
            final Animation animation;
            if (!TransitionInfo.isIndependent(change, info)) {
                // No-op if it will be covered by the changing parent window.
                animation = ActivityEmbeddingAnimationSpec.createNoopAnimation(change);
            } else if (Transitions.isClosingType(change.getMode())) {
                animation = mAnimationSpec.createChangeBoundsCloseAnimation(change);
            } else {
                animation = mAnimationSpec.createChangeBoundsOpenAnimation(change);
            }
            adapters.add(new ActivityEmbeddingAnimationAdapter(animation, change));
        }
        return adapters;
    }

    /** Takes a screenshot of the given {@link TransitionInfo.Change} surface. */
    @Nullable
    private SurfaceControl createScreenshot(@NonNull TransitionInfo.Change change,
            @NonNull SurfaceControl.Transaction startTransaction) {
        final Rect cropBounds = new Rect(change.getStartAbsBounds());
        cropBounds.offsetTo(0, 0);
        return ScreenshotUtils.takeScreenshot(startTransaction, change.getLeash(), cropBounds,
                Integer.MAX_VALUE);
    }
}
+212 −0

File added.

Preview size limit exceeded, changes collapsed.

+53 −12

File changed.

Preview size limit exceeded, changes collapsed.

+4 −3
Original line number Diff line number Diff line
@@ -627,11 +627,12 @@ public abstract class WMShellBaseModule {

    @WMSingleton
    @Provides
    static ActivityEmbeddingController provideActivityEmbeddingController(
    static Optional<ActivityEmbeddingController> provideActivityEmbeddingController(
            Context context,
            ShellInit shellInit,
            Transitions transitions) {
        return new ActivityEmbeddingController(context, shellInit, transitions);
        return Optional.ofNullable(
                ActivityEmbeddingController.create(context, shellInit, transitions));
    }

    //
@@ -686,7 +687,7 @@ public abstract class WMShellBaseModule {
            Optional<RecentTasksController> recentTasksOptional,
            Optional<OneHandedController> oneHandedControllerOptional,
            Optional<HideDisplayCutoutController> hideDisplayCutoutControllerOptional,
            ActivityEmbeddingController activityEmbeddingOptional,
            Optional<ActivityEmbeddingController> activityEmbeddingOptional,
            Transitions transitions,
            StartingWindowController startingWindow,
            @ShellCreateTriggerOverride Optional<Object> overriddenCreateTrigger) {
Loading