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
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 java.util.Objects.requireNonNull;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.IBinder;
|
||||
import android.util.ArrayMap;
|
||||
import android.view.SurfaceControl;
|
||||
import android.window.TransitionInfo;
|
||||
import android.window.TransitionRequestInfo;
|
||||
@@ -28,6 +31,7 @@ import android.window.WindowContainerTransaction;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
import com.android.wm.shell.sysui.ShellInit;
|
||||
import com.android.wm.shell.transition.Transitions;
|
||||
|
||||
@@ -37,15 +41,37 @@ import com.android.wm.shell.transition.Transitions;
|
||||
public class ActivityEmbeddingController implements Transitions.TransitionHandler {
|
||||
|
||||
private final Context mContext;
|
||||
private final Transitions mTransitions;
|
||||
@VisibleForTesting
|
||||
final Transitions mTransitions;
|
||||
@VisibleForTesting
|
||||
final ActivityEmbeddingAnimationRunner mAnimationRunner;
|
||||
|
||||
public ActivityEmbeddingController(Context context, ShellInit shellInit,
|
||||
Transitions transitions) {
|
||||
mContext = context;
|
||||
mTransitions = transitions;
|
||||
if (Transitions.ENABLE_SHELL_TRANSITIONS) {
|
||||
shellInit.addInitCallback(this::onInit, this);
|
||||
}
|
||||
/**
|
||||
* 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);
|
||||
|
||||
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. */
|
||||
@@ -66,9 +92,9 @@ public class ActivityEmbeddingController implements Transitions.TransitionHandle
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(b/207070762) Implement AE animation.
|
||||
startTransaction.apply();
|
||||
finishCallback.onTransitionFinished(null /* wct */, null /* wctCB */);
|
||||
// Start ActivityEmbedding animation.
|
||||
mTransitionCallbacks.put(transition, finishCallback);
|
||||
mAnimationRunner.startAnimation(transition, info, startTransaction, finishTransaction);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -79,6 +105,21 @@ public class ActivityEmbeddingController implements Transitions.TransitionHandle
|
||||
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) {
|
||||
return (change.getFlags() & FLAG_IS_EMBEDDED) != 0;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -219,6 +219,8 @@ public class Transitions implements RemoteCallable<Transitions> {
|
||||
+ "use ShellInit callbacks to ensure proper ordering");
|
||||
}
|
||||
mHandlers.add(handler);
|
||||
// Set initial scale settings.
|
||||
handler.setAnimScaleSetting(mTransitionAnimationScaleSetting);
|
||||
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "addHandler: %s",
|
||||
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;
|
||||
|
||||
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.Mockito.times;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
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.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.Test;
|
||||
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:
|
||||
* atest WMShellUnitTests:ActivityEmbeddingControllerTests
|
||||
*/
|
||||
@SmallTest
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class ActivityEmbeddingControllerTests extends ShellTestCase {
|
||||
|
||||
private @Mock Context mContext;
|
||||
private @Mock ShellInit mShellInit;
|
||||
private @Mock Transitions mTransitions;
|
||||
private ActivityEmbeddingController mController;
|
||||
public class ActivityEmbeddingControllerTests extends ActivityEmbeddingAnimationTestBase {
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
mController = spy(new ActivityEmbeddingController(mContext, mShellInit, mTransitions));
|
||||
public void setup() {
|
||||
super.setUp();
|
||||
doReturn(mAnimator).when(mAnimRunner).createAnimator(any(), any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void instantiate_addInitCallback() {
|
||||
assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS);
|
||||
verify(mShellInit, times(1)).addInitCallback(any(), any());
|
||||
public void testInstantiate() {
|
||||
verify(mShellInit).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