Merge "Animate ActivityEmbedding transition with Shell Transition" into tm-qpr-dev
This commit is contained in:
@@ -0,0 +1,234 @@
|
|||||||
|
/*
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,290 @@
|
|||||||
|
/*
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
/*
|
||||||
|
* 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 android.content.Context;
|
||||||
|
import android.graphics.Point;
|
||||||
|
import android.graphics.Rect;
|
||||||
|
import android.view.animation.AlphaAnimation;
|
||||||
|
import android.view.animation.Animation;
|
||||||
|
import android.view.animation.AnimationSet;
|
||||||
|
import android.view.animation.AnimationUtils;
|
||||||
|
import android.view.animation.ClipRectAnimation;
|
||||||
|
import android.view.animation.Interpolator;
|
||||||
|
import android.view.animation.LinearInterpolator;
|
||||||
|
import android.view.animation.ScaleAnimation;
|
||||||
|
import android.view.animation.TranslateAnimation;
|
||||||
|
import android.window.TransitionInfo;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import com.android.internal.R;
|
||||||
|
import com.android.internal.policy.TransitionAnimation;
|
||||||
|
import com.android.wm.shell.transition.Transitions;
|
||||||
|
|
||||||
|
/** Animation spec for ActivityEmbedding transition. */
|
||||||
|
// TODO(b/206557124): provide an easier way to customize animation
|
||||||
|
class ActivityEmbeddingAnimationSpec {
|
||||||
|
|
||||||
|
private static final String TAG = "ActivityEmbeddingAnimSpec";
|
||||||
|
private static final int CHANGE_ANIMATION_DURATION = 517;
|
||||||
|
private static final int CHANGE_ANIMATION_FADE_DURATION = 80;
|
||||||
|
private static final int CHANGE_ANIMATION_FADE_OFFSET = 30;
|
||||||
|
|
||||||
|
private final Context mContext;
|
||||||
|
private final TransitionAnimation mTransitionAnimation;
|
||||||
|
private final Interpolator mFastOutExtraSlowInInterpolator;
|
||||||
|
private final LinearInterpolator mLinearInterpolator;
|
||||||
|
private float mTransitionAnimationScaleSetting;
|
||||||
|
|
||||||
|
ActivityEmbeddingAnimationSpec(@NonNull Context context) {
|
||||||
|
mContext = context;
|
||||||
|
mTransitionAnimation = new TransitionAnimation(mContext, false /* debug */, TAG);
|
||||||
|
mFastOutExtraSlowInInterpolator = AnimationUtils.loadInterpolator(
|
||||||
|
mContext, android.R.interpolator.fast_out_extra_slow_in);
|
||||||
|
mLinearInterpolator = new LinearInterpolator();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets transition animation scale settings value.
|
||||||
|
* @param scale The setting value of transition animation scale.
|
||||||
|
*/
|
||||||
|
void setAnimScaleSetting(float scale) {
|
||||||
|
mTransitionAnimationScaleSetting = scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** For window that doesn't need to be animated. */
|
||||||
|
@NonNull
|
||||||
|
static Animation createNoopAnimation(@NonNull TransitionInfo.Change change) {
|
||||||
|
// Noop but just keep the window showing/hiding.
|
||||||
|
final float alpha = Transitions.isClosingType(change.getMode()) ? 0f : 1f;
|
||||||
|
return new AlphaAnimation(alpha, alpha);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Animation for window that is opening in a change transition. */
|
||||||
|
@NonNull
|
||||||
|
Animation createChangeBoundsOpenAnimation(@NonNull TransitionInfo.Change change) {
|
||||||
|
final Rect bounds = change.getEndAbsBounds();
|
||||||
|
final Point offset = change.getEndRelOffset();
|
||||||
|
// The window will be animated in from left or right depends on its position.
|
||||||
|
final int startLeft = offset.x == 0 ? -bounds.width() : bounds.width();
|
||||||
|
|
||||||
|
// The position should be 0-based as we will post translate in
|
||||||
|
// ActivityEmbeddingAnimationAdapter#onAnimationUpdate
|
||||||
|
final Animation animation = new TranslateAnimation(startLeft, 0, 0, 0);
|
||||||
|
animation.setInterpolator(mFastOutExtraSlowInInterpolator);
|
||||||
|
animation.setDuration(CHANGE_ANIMATION_DURATION);
|
||||||
|
animation.initialize(bounds.width(), bounds.height(), bounds.width(), bounds.height());
|
||||||
|
animation.scaleCurrentDuration(mTransitionAnimationScaleSetting);
|
||||||
|
return animation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Animation for window that is closing in a change transition. */
|
||||||
|
@NonNull
|
||||||
|
Animation createChangeBoundsCloseAnimation(@NonNull TransitionInfo.Change change) {
|
||||||
|
final Rect bounds = change.getEndAbsBounds();
|
||||||
|
final Point offset = change.getEndRelOffset();
|
||||||
|
// The window will be animated out to left or right depends on its position.
|
||||||
|
final int endLeft = offset.x == 0 ? -bounds.width() : bounds.width();
|
||||||
|
|
||||||
|
// The position should be 0-based as we will post translate in
|
||||||
|
// ActivityEmbeddingAnimationAdapter#onAnimationUpdate
|
||||||
|
final Animation animation = new TranslateAnimation(0, endLeft, 0, 0);
|
||||||
|
animation.setInterpolator(mFastOutExtraSlowInInterpolator);
|
||||||
|
animation.setDuration(CHANGE_ANIMATION_DURATION);
|
||||||
|
animation.initialize(bounds.width(), bounds.height(), bounds.width(), bounds.height());
|
||||||
|
animation.scaleCurrentDuration(mTransitionAnimationScaleSetting);
|
||||||
|
return animation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Animation for window that is changing (bounds change) in a change transition.
|
||||||
|
* @return the return array always has two elements. The first one is for the start leash, and
|
||||||
|
* the second one is for the end leash.
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
Animation[] createChangeBoundsChangeAnimations(@NonNull TransitionInfo.Change change,
|
||||||
|
@NonNull Rect parentBounds) {
|
||||||
|
// Both start bounds and end bounds are in screen coordinates. We will post translate
|
||||||
|
// to the local coordinates in ActivityEmbeddingAnimationAdapter#onAnimationUpdate
|
||||||
|
final Rect startBounds = change.getStartAbsBounds();
|
||||||
|
final Rect endBounds = change.getEndAbsBounds();
|
||||||
|
float scaleX = ((float) startBounds.width()) / endBounds.width();
|
||||||
|
float scaleY = ((float) startBounds.height()) / endBounds.height();
|
||||||
|
// Start leash is a child of the end leash. Reverse the scale so that the start leash won't
|
||||||
|
// be scaled up with its parent.
|
||||||
|
float startScaleX = 1.f / scaleX;
|
||||||
|
float startScaleY = 1.f / scaleY;
|
||||||
|
|
||||||
|
// The start leash will be fade out.
|
||||||
|
final AnimationSet startSet = new AnimationSet(false /* shareInterpolator */);
|
||||||
|
final Animation startAlpha = new AlphaAnimation(1f, 0f);
|
||||||
|
startAlpha.setInterpolator(mLinearInterpolator);
|
||||||
|
startAlpha.setDuration(CHANGE_ANIMATION_FADE_DURATION);
|
||||||
|
startAlpha.setStartOffset(CHANGE_ANIMATION_FADE_OFFSET);
|
||||||
|
startSet.addAnimation(startAlpha);
|
||||||
|
final Animation startScale = new ScaleAnimation(startScaleX, startScaleX, startScaleY,
|
||||||
|
startScaleY);
|
||||||
|
startScale.setInterpolator(mFastOutExtraSlowInInterpolator);
|
||||||
|
startScale.setDuration(CHANGE_ANIMATION_DURATION);
|
||||||
|
startSet.addAnimation(startScale);
|
||||||
|
startSet.initialize(startBounds.width(), startBounds.height(), endBounds.width(),
|
||||||
|
endBounds.height());
|
||||||
|
startSet.scaleCurrentDuration(mTransitionAnimationScaleSetting);
|
||||||
|
|
||||||
|
// The end leash will be moved into the end position while scaling.
|
||||||
|
final AnimationSet endSet = new AnimationSet(true /* shareInterpolator */);
|
||||||
|
endSet.setInterpolator(mFastOutExtraSlowInInterpolator);
|
||||||
|
final Animation endScale = new ScaleAnimation(scaleX, 1, scaleY, 1);
|
||||||
|
endScale.setDuration(CHANGE_ANIMATION_DURATION);
|
||||||
|
endSet.addAnimation(endScale);
|
||||||
|
// The position should be 0-based as we will post translate in
|
||||||
|
// ActivityEmbeddingAnimationAdapter#onAnimationUpdate
|
||||||
|
final Animation endTranslate = new TranslateAnimation(startBounds.left - endBounds.left, 0,
|
||||||
|
0, 0);
|
||||||
|
endTranslate.setDuration(CHANGE_ANIMATION_DURATION);
|
||||||
|
endSet.addAnimation(endTranslate);
|
||||||
|
// The end leash is resizing, we should update the window crop based on the clip rect.
|
||||||
|
final Rect startClip = new Rect(startBounds);
|
||||||
|
final Rect endClip = new Rect(endBounds);
|
||||||
|
startClip.offsetTo(0, 0);
|
||||||
|
endClip.offsetTo(0, 0);
|
||||||
|
final Animation clipAnim = new ClipRectAnimation(startClip, endClip);
|
||||||
|
clipAnim.setDuration(CHANGE_ANIMATION_DURATION);
|
||||||
|
endSet.addAnimation(clipAnim);
|
||||||
|
endSet.initialize(startBounds.width(), startBounds.height(), parentBounds.width(),
|
||||||
|
parentBounds.height());
|
||||||
|
endSet.scaleCurrentDuration(mTransitionAnimationScaleSetting);
|
||||||
|
|
||||||
|
return new Animation[]{startSet, endSet};
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
Animation loadOpenAnimation(@NonNull TransitionInfo.Change change,
|
||||||
|
@NonNull Rect wholeAnimationBounds) {
|
||||||
|
final boolean isEnter = Transitions.isOpeningType(change.getMode());
|
||||||
|
final Animation animation;
|
||||||
|
// TODO(b/207070762):
|
||||||
|
// 1. Implement clearTop version: R.anim.task_fragment_clear_top_close_enter/exit
|
||||||
|
// 2. Implement edgeExtension version
|
||||||
|
animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter
|
||||||
|
? R.anim.task_fragment_open_enter
|
||||||
|
: R.anim.task_fragment_open_exit);
|
||||||
|
final Rect bounds = change.getEndAbsBounds();
|
||||||
|
animation.initialize(bounds.width(), bounds.height(),
|
||||||
|
wholeAnimationBounds.width(), wholeAnimationBounds.height());
|
||||||
|
animation.scaleCurrentDuration(mTransitionAnimationScaleSetting);
|
||||||
|
return animation;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
Animation loadCloseAnimation(@NonNull TransitionInfo.Change change,
|
||||||
|
@NonNull Rect wholeAnimationBounds) {
|
||||||
|
final boolean isEnter = Transitions.isOpeningType(change.getMode());
|
||||||
|
final Animation animation;
|
||||||
|
// TODO(b/207070762):
|
||||||
|
// 1. Implement clearTop version: R.anim.task_fragment_clear_top_close_enter/exit
|
||||||
|
// 2. Implement edgeExtension version
|
||||||
|
animation = mTransitionAnimation.loadDefaultAnimationRes(isEnter
|
||||||
|
? R.anim.task_fragment_close_enter
|
||||||
|
: R.anim.task_fragment_close_exit);
|
||||||
|
final Rect bounds = change.getEndAbsBounds();
|
||||||
|
animation.initialize(bounds.width(), bounds.height(),
|
||||||
|
wholeAnimationBounds.width(), wholeAnimationBounds.height());
|
||||||
|
animation.scaleCurrentDuration(mTransitionAnimationScaleSetting);
|
||||||
|
return animation;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,8 +18,11 @@ package com.android.wm.shell.activityembedding;
|
|||||||
|
|
||||||
import static android.window.TransitionInfo.FLAG_IS_EMBEDDED;
|
import static android.window.TransitionInfo.FLAG_IS_EMBEDDED;
|
||||||
|
|
||||||
|
import static java.util.Objects.requireNonNull;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
|
import android.util.ArrayMap;
|
||||||
import android.view.SurfaceControl;
|
import android.view.SurfaceControl;
|
||||||
import android.window.TransitionInfo;
|
import android.window.TransitionInfo;
|
||||||
import android.window.TransitionRequestInfo;
|
import android.window.TransitionRequestInfo;
|
||||||
@@ -28,6 +31,7 @@ import android.window.WindowContainerTransaction;
|
|||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import com.android.internal.annotations.VisibleForTesting;
|
||||||
import com.android.wm.shell.sysui.ShellInit;
|
import com.android.wm.shell.sysui.ShellInit;
|
||||||
import com.android.wm.shell.transition.Transitions;
|
import com.android.wm.shell.transition.Transitions;
|
||||||
|
|
||||||
@@ -37,15 +41,37 @@ import com.android.wm.shell.transition.Transitions;
|
|||||||
public class ActivityEmbeddingController implements Transitions.TransitionHandler {
|
public class ActivityEmbeddingController implements Transitions.TransitionHandler {
|
||||||
|
|
||||||
private final Context mContext;
|
private final Context mContext;
|
||||||
private final Transitions mTransitions;
|
@VisibleForTesting
|
||||||
|
final Transitions mTransitions;
|
||||||
|
@VisibleForTesting
|
||||||
|
final ActivityEmbeddingAnimationRunner mAnimationRunner;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keeps track of the currently-running transition callback associated with each transition
|
||||||
|
* token.
|
||||||
|
*/
|
||||||
|
private final ArrayMap<IBinder, Transitions.TransitionFinishCallback> mTransitionCallbacks =
|
||||||
|
new ArrayMap<>();
|
||||||
|
|
||||||
|
private ActivityEmbeddingController(@NonNull Context context, @NonNull ShellInit shellInit,
|
||||||
|
@NonNull Transitions transitions) {
|
||||||
|
mContext = requireNonNull(context);
|
||||||
|
mTransitions = requireNonNull(transitions);
|
||||||
|
mAnimationRunner = new ActivityEmbeddingAnimationRunner(context, this);
|
||||||
|
|
||||||
public ActivityEmbeddingController(Context context, ShellInit shellInit,
|
|
||||||
Transitions transitions) {
|
|
||||||
mContext = context;
|
|
||||||
mTransitions = transitions;
|
|
||||||
if (Transitions.ENABLE_SHELL_TRANSITIONS) {
|
|
||||||
shellInit.addInitCallback(this::onInit, this);
|
shellInit.addInitCallback(this::onInit, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates {@link ActivityEmbeddingController}, returns {@code null} if the feature is not
|
||||||
|
* supported.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public static ActivityEmbeddingController create(@NonNull Context context,
|
||||||
|
@NonNull ShellInit shellInit, @NonNull Transitions transitions) {
|
||||||
|
return Transitions.ENABLE_SHELL_TRANSITIONS
|
||||||
|
? new ActivityEmbeddingController(context, shellInit, transitions)
|
||||||
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Registers to handle transitions. */
|
/** Registers to handle transitions. */
|
||||||
@@ -66,9 +92,9 @@ public class ActivityEmbeddingController implements Transitions.TransitionHandle
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(b/207070762) Implement AE animation.
|
// Start ActivityEmbedding animation.
|
||||||
startTransaction.apply();
|
mTransitionCallbacks.put(transition, finishCallback);
|
||||||
finishCallback.onTransitionFinished(null /* wct */, null /* wctCB */);
|
mAnimationRunner.startAnimation(transition, info, startTransaction, finishTransaction);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,6 +105,21 @@ public class ActivityEmbeddingController implements Transitions.TransitionHandle
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setAnimScaleSetting(float scale) {
|
||||||
|
mAnimationRunner.setAnimScaleSetting(scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Called when the animation is finished. */
|
||||||
|
void onAnimationFinished(@NonNull IBinder transition) {
|
||||||
|
final Transitions.TransitionFinishCallback callback =
|
||||||
|
mTransitionCallbacks.remove(transition);
|
||||||
|
if (callback == null) {
|
||||||
|
throw new IllegalStateException("No finish callback found");
|
||||||
|
}
|
||||||
|
callback.onTransitionFinished(null /* wct */, null /* wctCB */);
|
||||||
|
}
|
||||||
|
|
||||||
private static boolean isEmbedded(@NonNull TransitionInfo.Change change) {
|
private static boolean isEmbedded(@NonNull TransitionInfo.Change change) {
|
||||||
return (change.getFlags() & FLAG_IS_EMBEDDED) != 0;
|
return (change.getFlags() & FLAG_IS_EMBEDDED) != 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -627,11 +627,12 @@ public abstract class WMShellBaseModule {
|
|||||||
|
|
||||||
@WMSingleton
|
@WMSingleton
|
||||||
@Provides
|
@Provides
|
||||||
static ActivityEmbeddingController provideActivityEmbeddingController(
|
static Optional<ActivityEmbeddingController> provideActivityEmbeddingController(
|
||||||
Context context,
|
Context context,
|
||||||
ShellInit shellInit,
|
ShellInit shellInit,
|
||||||
Transitions transitions) {
|
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<RecentTasksController> recentTasksOptional,
|
||||||
Optional<OneHandedController> oneHandedControllerOptional,
|
Optional<OneHandedController> oneHandedControllerOptional,
|
||||||
Optional<HideDisplayCutoutController> hideDisplayCutoutControllerOptional,
|
Optional<HideDisplayCutoutController> hideDisplayCutoutControllerOptional,
|
||||||
ActivityEmbeddingController activityEmbeddingOptional,
|
Optional<ActivityEmbeddingController> activityEmbeddingOptional,
|
||||||
Transitions transitions,
|
Transitions transitions,
|
||||||
StartingWindowController startingWindow,
|
StartingWindowController startingWindow,
|
||||||
@ShellCreateTriggerOverride Optional<Object> overriddenCreateTrigger) {
|
@ShellCreateTriggerOverride Optional<Object> overriddenCreateTrigger) {
|
||||||
|
|||||||
@@ -219,6 +219,8 @@ public class Transitions implements RemoteCallable<Transitions> {
|
|||||||
+ "use ShellInit callbacks to ensure proper ordering");
|
+ "use ShellInit callbacks to ensure proper ordering");
|
||||||
}
|
}
|
||||||
mHandlers.add(handler);
|
mHandlers.add(handler);
|
||||||
|
// Set initial scale settings.
|
||||||
|
handler.setAnimScaleSetting(mTransitionAnimationScaleSetting);
|
||||||
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "addHandler: %s",
|
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "addHandler: %s",
|
||||||
handler.getClass().getSimpleName());
|
handler.getClass().getSimpleName());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
/*
|
||||||
|
* 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_OPEN;
|
||||||
|
import static android.window.TransitionInfo.FLAG_IS_EMBEDDED;
|
||||||
|
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.doNothing;
|
||||||
|
import static org.mockito.Mockito.doReturn;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||||
|
|
||||||
|
import android.window.TransitionInfo;
|
||||||
|
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
|
import androidx.test.filters.SmallTest;
|
||||||
|
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link ActivityEmbeddingAnimationRunner}.
|
||||||
|
*
|
||||||
|
* Build/Install/Run:
|
||||||
|
* atest WMShellUnitTests:ActivityEmbeddingAnimationRunnerTests
|
||||||
|
*/
|
||||||
|
@SmallTest
|
||||||
|
@RunWith(AndroidJUnit4.class)
|
||||||
|
public class ActivityEmbeddingAnimationRunnerTests extends ActivityEmbeddingAnimationTestBase {
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setup() {
|
||||||
|
super.setUp();
|
||||||
|
doNothing().when(mController).onAnimationFinished(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testStartAnimation() {
|
||||||
|
final TransitionInfo info = new TransitionInfo(TRANSIT_OPEN, 0);
|
||||||
|
final TransitionInfo.Change embeddingChange = createChange();
|
||||||
|
embeddingChange.setFlags(FLAG_IS_EMBEDDED);
|
||||||
|
info.addChange(embeddingChange);
|
||||||
|
doReturn(mAnimator).when(mAnimRunner).createAnimator(any(), any(), any(), any());
|
||||||
|
|
||||||
|
mAnimRunner.startAnimation(mTransition, info, mStartTransaction, mFinishTransaction);
|
||||||
|
|
||||||
|
final ArgumentCaptor<Runnable> finishCallback = ArgumentCaptor.forClass(Runnable.class);
|
||||||
|
verify(mAnimRunner).createAnimator(eq(info), eq(mStartTransaction), eq(mFinishTransaction),
|
||||||
|
finishCallback.capture());
|
||||||
|
verify(mStartTransaction).apply();
|
||||||
|
verify(mAnimator).start();
|
||||||
|
verifyNoMoreInteractions(mFinishTransaction);
|
||||||
|
verify(mController, never()).onAnimationFinished(any());
|
||||||
|
|
||||||
|
// Call onAnimationFinished() when the animation is finished.
|
||||||
|
finishCallback.getValue().run();
|
||||||
|
|
||||||
|
verify(mController).onAnimationFinished(mTransition);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
import static org.junit.Assume.assumeTrue;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
|
import android.animation.Animator;
|
||||||
|
import android.annotation.CallSuper;
|
||||||
|
import android.os.IBinder;
|
||||||
|
import android.view.SurfaceControl;
|
||||||
|
import android.window.TransitionInfo;
|
||||||
|
import android.window.WindowContainerToken;
|
||||||
|
|
||||||
|
import com.android.wm.shell.ShellTestCase;
|
||||||
|
import com.android.wm.shell.sysui.ShellInit;
|
||||||
|
import com.android.wm.shell.transition.Transitions;
|
||||||
|
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.MockitoAnnotations;
|
||||||
|
|
||||||
|
/** TestBase for ActivityEmbedding animation. */
|
||||||
|
abstract class ActivityEmbeddingAnimationTestBase extends ShellTestCase {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
ShellInit mShellInit;
|
||||||
|
@Mock
|
||||||
|
Transitions mTransitions;
|
||||||
|
@Mock
|
||||||
|
IBinder mTransition;
|
||||||
|
@Mock
|
||||||
|
SurfaceControl.Transaction mStartTransaction;
|
||||||
|
@Mock
|
||||||
|
SurfaceControl.Transaction mFinishTransaction;
|
||||||
|
@Mock
|
||||||
|
Transitions.TransitionFinishCallback mFinishCallback;
|
||||||
|
@Mock
|
||||||
|
Animator mAnimator;
|
||||||
|
|
||||||
|
ActivityEmbeddingController mController;
|
||||||
|
ActivityEmbeddingAnimationRunner mAnimRunner;
|
||||||
|
ActivityEmbeddingAnimationSpec mAnimSpec;
|
||||||
|
|
||||||
|
@CallSuper
|
||||||
|
@Before
|
||||||
|
public void setUp() {
|
||||||
|
assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS);
|
||||||
|
MockitoAnnotations.initMocks(this);
|
||||||
|
mController = ActivityEmbeddingController.create(mContext, mShellInit, mTransitions);
|
||||||
|
assertNotNull(mController);
|
||||||
|
mAnimRunner = mController.mAnimationRunner;
|
||||||
|
assertNotNull(mAnimRunner);
|
||||||
|
mAnimSpec = mAnimRunner.mAnimationSpec;
|
||||||
|
assertNotNull(mAnimSpec);
|
||||||
|
spyOn(mController);
|
||||||
|
spyOn(mAnimRunner);
|
||||||
|
spyOn(mAnimSpec);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Creates a mock {@link TransitionInfo.Change}. */
|
||||||
|
static TransitionInfo.Change createChange() {
|
||||||
|
return new TransitionInfo.Change(mock(WindowContainerToken.class),
|
||||||
|
mock(SurfaceControl.class));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,52 +16,117 @@
|
|||||||
|
|
||||||
package com.android.wm.shell.activityembedding;
|
package com.android.wm.shell.activityembedding;
|
||||||
|
|
||||||
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy;
|
import static android.view.WindowManager.TRANSIT_OPEN;
|
||||||
|
import static android.window.TransitionInfo.FLAG_IS_EMBEDDED;
|
||||||
|
|
||||||
import static org.junit.Assume.assumeTrue;
|
import static com.android.dx.mockito.inline.extended.ExtendedMockito.never;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.Mockito.times;
|
import static org.mockito.Mockito.doReturn;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.window.TransitionInfo;
|
||||||
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
import androidx.test.filters.SmallTest;
|
import androidx.test.filters.SmallTest;
|
||||||
|
|
||||||
import com.android.wm.shell.ShellTestCase;
|
|
||||||
import com.android.wm.shell.sysui.ShellInit;
|
|
||||||
import com.android.wm.shell.transition.Transitions;
|
|
||||||
|
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
import org.mockito.Mock;
|
|
||||||
import org.mockito.MockitoAnnotations;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests for the activity embedding controller.
|
* Tests for {@link ActivityEmbeddingController}.
|
||||||
*
|
*
|
||||||
* Build/Install/Run:
|
* Build/Install/Run:
|
||||||
* atest WMShellUnitTests:ActivityEmbeddingControllerTests
|
* atest WMShellUnitTests:ActivityEmbeddingControllerTests
|
||||||
*/
|
*/
|
||||||
@SmallTest
|
@SmallTest
|
||||||
@RunWith(AndroidJUnit4.class)
|
@RunWith(AndroidJUnit4.class)
|
||||||
public class ActivityEmbeddingControllerTests extends ShellTestCase {
|
public class ActivityEmbeddingControllerTests extends ActivityEmbeddingAnimationTestBase {
|
||||||
|
|
||||||
private @Mock Context mContext;
|
|
||||||
private @Mock ShellInit mShellInit;
|
|
||||||
private @Mock Transitions mTransitions;
|
|
||||||
private ActivityEmbeddingController mController;
|
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void setUp() {
|
public void setup() {
|
||||||
MockitoAnnotations.initMocks(this);
|
super.setUp();
|
||||||
mController = spy(new ActivityEmbeddingController(mContext, mShellInit, mTransitions));
|
doReturn(mAnimator).when(mAnimRunner).createAnimator(any(), any(), any(), any());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void instantiate_addInitCallback() {
|
public void testInstantiate() {
|
||||||
assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS);
|
verify(mShellInit).addInitCallback(any(), any());
|
||||||
verify(mShellInit, times(1)).addInitCallback(any(), any());
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testOnInit() {
|
||||||
|
mController.onInit();
|
||||||
|
|
||||||
|
verify(mTransitions).addHandler(mController);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSetAnimScaleSetting() {
|
||||||
|
mController.setAnimScaleSetting(1.0f);
|
||||||
|
|
||||||
|
verify(mAnimRunner).setAnimScaleSetting(1.0f);
|
||||||
|
verify(mAnimSpec).setAnimScaleSetting(1.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testStartAnimation_containsNonActivityEmbeddingChange() {
|
||||||
|
final TransitionInfo info = new TransitionInfo(TRANSIT_OPEN, 0);
|
||||||
|
final TransitionInfo.Change embeddingChange = createChange();
|
||||||
|
embeddingChange.setFlags(FLAG_IS_EMBEDDED);
|
||||||
|
final TransitionInfo.Change nonEmbeddingChange = createChange();
|
||||||
|
info.addChange(embeddingChange);
|
||||||
|
info.addChange(nonEmbeddingChange);
|
||||||
|
|
||||||
|
// No-op
|
||||||
|
assertFalse(mController.startAnimation(mTransition, info, mStartTransaction,
|
||||||
|
mFinishTransaction, mFinishCallback));
|
||||||
|
verify(mAnimRunner, never()).startAnimation(any(), any(), any(), any());
|
||||||
|
verifyNoMoreInteractions(mStartTransaction);
|
||||||
|
verifyNoMoreInteractions(mFinishTransaction);
|
||||||
|
verifyNoMoreInteractions(mFinishCallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testStartAnimation_onlyActivityEmbeddingChange() {
|
||||||
|
final TransitionInfo info = new TransitionInfo(TRANSIT_OPEN, 0);
|
||||||
|
final TransitionInfo.Change embeddingChange = createChange();
|
||||||
|
embeddingChange.setFlags(FLAG_IS_EMBEDDED);
|
||||||
|
info.addChange(embeddingChange);
|
||||||
|
|
||||||
|
// No-op
|
||||||
|
assertTrue(mController.startAnimation(mTransition, info, mStartTransaction,
|
||||||
|
mFinishTransaction, mFinishCallback));
|
||||||
|
verify(mAnimRunner).startAnimation(mTransition, info, mStartTransaction,
|
||||||
|
mFinishTransaction);
|
||||||
|
verify(mStartTransaction).apply();
|
||||||
|
verifyNoMoreInteractions(mFinishTransaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testOnAnimationFinished() {
|
||||||
|
// Should not call finish when there is no transition.
|
||||||
|
assertThrows(IllegalStateException.class,
|
||||||
|
() -> mController.onAnimationFinished(mTransition));
|
||||||
|
|
||||||
|
final TransitionInfo info = new TransitionInfo(TRANSIT_OPEN, 0);
|
||||||
|
final TransitionInfo.Change embeddingChange = createChange();
|
||||||
|
embeddingChange.setFlags(FLAG_IS_EMBEDDED);
|
||||||
|
info.addChange(embeddingChange);
|
||||||
|
mController.startAnimation(mTransition, info, mStartTransaction,
|
||||||
|
mFinishTransaction, mFinishCallback);
|
||||||
|
|
||||||
|
verify(mFinishCallback, never()).onTransitionFinished(any(), any());
|
||||||
|
mController.onAnimationFinished(mTransition);
|
||||||
|
verify(mFinishCallback).onTransitionFinished(any(), any());
|
||||||
|
|
||||||
|
// Should not call finish when the finish has already been called.
|
||||||
|
assertThrows(IllegalStateException.class,
|
||||||
|
() -> mController.onAnimationFinished(mTransition));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user