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

Commit bdfb18ab authored by Chris Li's avatar Chris Li
Browse files

Animate ActivityEmbedding transition with Shell Transition

Make a copy of TaskFragmentAnimation* to ActivityEmbeddingAnimation*
to fit into Shell transition model.

Bug: 207070762
Test: atest WMShellUnitTests:ActivityEmbeddingControllerTests
Test: atest WMShellUnitTests:ActivityEmbeddingAnimationRunnerTests
Change-Id: I44118e1415f85bcd49fa77bff10c67094ec9cad5
parent 4e9c5b44
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