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

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

Change-Id: Ibe0c526301acbe1d3fe8effe7d861dcc09466b4c
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
Chris Li
2022-08-24 06:46:44 +00:00
committed by Automerger Merge Worker
9 changed files with 1044 additions and 37 deletions

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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) {

View File

@@ -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());
}

View File

@@ -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);
}
}

View File

@@ -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));
}
}

View File

@@ -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));
}
}