From 33a00c00dd6e11096bd84bba7b0eb50cda234e8e Mon Sep 17 00:00:00 2001 From: Chris Li Date: Sun, 3 Oct 2021 14:38:42 +0800 Subject: [PATCH] Treat two TaskFragment leashes as one for TASK_FRAGMENT_OPEN/CLOSE We use the default activity open animation for TaskFragment open. However, when two split TaskFragments are opening together, it would look like their separate. We can't promote it to the Task level because the Task contains both opening and closing TaskFragments. The workaround here is to offset the left/right TaskFragments to make them look like one window. Bug: 201623109 Bug: 196173550 Test: manually open two TaskFragment in split with demo app Change-Id: Ic391262dcbd7f35d4e5a88588d39c74b4c5d5aae --- .../TaskFragmentAnimationAdapter.java | 153 ++++++++++++++---- .../TaskFragmentAnimationController.java | 1 - .../TaskFragmentAnimationRunner.java | 81 +++++++--- .../embedding/TaskFragmentAnimationSpec.java | 22 ++- 4 files changed, 198 insertions(+), 59 deletions(-) diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationAdapter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationAdapter.java index 06f6228623d8b..194b6330d92cf 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationAdapter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationAdapter.java @@ -16,7 +16,8 @@ package androidx.window.extensions.embedding; -import android.graphics.Point; +import static android.graphics.Matrix.MSCALE_X; + import android.graphics.Rect; import android.view.Choreographer; import android.view.RemoteAnimationTarget; @@ -25,58 +26,151 @@ import android.view.animation.Animation; import android.view.animation.Transformation; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; /** * Wrapper to handle the TaskFragment animation update in one {@link SurfaceControl.Transaction}. + * + * The base adapter can be used for {@link RemoteAnimationTarget} that is simple open/close. */ class TaskFragmentAnimationAdapter { - private final Animation mAnimation; - private final RemoteAnimationTarget mTarget; - private final SurfaceControl mLeash; - private final boolean mSizeChanged; - private final Point mPosition; - private final Transformation mTransformation = new Transformation(); - private final float[] mMatrix = new float[9]; - private final float[] mVecs = new float[4]; - private final Rect mRect = new Rect(); + final Animation mAnimation; + final RemoteAnimationTarget mTarget; + final SurfaceControl mLeash; + + final Transformation mTransformation = new Transformation(); + final float[] mMatrix = new float[9]; private boolean mIsFirstFrame = true; TaskFragmentAnimationAdapter(@NonNull Animation animation, @NonNull RemoteAnimationTarget target) { - this(animation, target, target.leash, false /* sizeChanged */, null /* position */); + this(animation, target, target.leash); } /** - * @param sizeChanged whether the surface size needs to be changed. + * @param leash the surface to animate. */ TaskFragmentAnimationAdapter(@NonNull Animation animation, - @NonNull RemoteAnimationTarget target, @NonNull SurfaceControl leash, - boolean sizeChanged, @Nullable Point position) { + @NonNull RemoteAnimationTarget target, @NonNull SurfaceControl leash) { mAnimation = animation; mTarget = target; mLeash = leash; - mSizeChanged = sizeChanged; - mPosition = position != null - ? position - : new Point(target.localBounds.left, target.localBounds.top); } /** Called on frame update. */ - void onAnimationUpdate(@NonNull SurfaceControl.Transaction t, long currentPlayTime) { + final void onAnimationUpdate(@NonNull SurfaceControl.Transaction t, long currentPlayTime) { if (mIsFirstFrame) { t.show(mLeash); mIsFirstFrame = false; } - currentPlayTime = Math.min(currentPlayTime, mAnimation.getDuration()); - mAnimation.getTransformation(currentPlayTime, mTransformation); - mTransformation.getMatrix().postTranslate(mPosition.x, mPosition.y); + // 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) { + mTransformation.getMatrix().postTranslate( + mTarget.localBounds.left, mTarget.localBounds.top); t.setMatrix(mLeash, mTransformation.getMatrix(), mMatrix); t.setAlpha(mLeash, mTransformation.getAlpha()); - t.setFrameTimelineVsync(Choreographer.getInstance().getVsyncId()); + } + + /** Called after animation finished. */ + final void onAnimationEnd(@NonNull SurfaceControl.Transaction t) { + onAnimationUpdate(t, mAnimation.getDuration()); + } + + final long getDurationHint() { + return mAnimation.computeDurationHint(); + } + + /** + * Should be used when the {@link RemoteAnimationTarget} is in split with others, and want 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 TaskFragmentAnimationAdapter { + 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 RemoteAnimationTarget target, + boolean isLeftHalf, int wholeAnimationWidth) { + super(animation, target); + mIsLeftHalf = isLeftHalf; + mWholeAnimationWidth = wholeAnimationWidth; + if (wholeAnimationWidth == 0) { + throw new IllegalArgumentException("SplitAdapter must provide wholeAnimationWidth"); + } + } + + @Override + void onAnimationUpdateInner(@NonNull SurfaceControl.Transaction t) { + float posX = mTarget.localBounds.left; + final float posY = mTarget.localBounds.top; + // 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 targetWidth = mTarget.localBounds.width(); + final float scaleX = mMatrix[MSCALE_X]; + final float totalOffset = mWholeAnimationWidth * (1 - scaleX) / 2; + final float curOffset = targetWidth * (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 RemoteAnimationTarget} that has + * size change. + */ + static class SnapshotAdapter extends TaskFragmentAnimationAdapter { + + SnapshotAdapter(@NonNull Animation animation, @NonNull RemoteAnimationTarget target) { + // Start leash is the snapshot of the starting surface. + super(animation, target, target.startLeash); + } + + @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()); + } + } + + /** + * Should be used for the animation of the {@link RemoteAnimationTarget} that has size change. + */ + static class BoundsChangeAdapter extends TaskFragmentAnimationAdapter { + private final float[] mVecs = new float[4]; + private final Rect mRect = new Rect(); + + BoundsChangeAdapter(@NonNull Animation animation, @NonNull RemoteAnimationTarget target) { + super(animation, target); + } + + @Override + void onAnimationUpdateInner(@NonNull SurfaceControl.Transaction t) { + mTransformation.getMatrix().postTranslate( + mTarget.localBounds.left, mTarget.localBounds.top); + t.setMatrix(mLeash, mTransformation.getMatrix(), mMatrix); + t.setAlpha(mLeash, mTransformation.getAlpha()); - if (mSizeChanged) { // 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; @@ -92,13 +186,4 @@ class TaskFragmentAnimationAdapter { t.setWindowCrop(mLeash, mRect); } } - - /** Called after animation finished. */ - void onAnimationEnd(@NonNull SurfaceControl.Transaction t) { - onAnimationUpdate(t, mAnimation.getDuration()); - } - - long getDurationHint() { - return mAnimation.computeDurationHint(); - } } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationController.java index 65797667687e1..535dac1a51016 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationController.java @@ -29,7 +29,6 @@ import android.window.TaskFragmentOrganizer; class TaskFragmentAnimationController { private static final String TAG = "TaskFragAnimationCtrl"; - // TODO(b/196173550) turn off when finalize static final boolean DEBUG = false; private final TaskFragmentOrganizer mOrganizer; diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java index 3980d077a7dd5..412559e34070a 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationRunner.java @@ -23,7 +23,7 @@ import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_OPEN; import android.animation.Animator; import android.animation.ValueAnimator; -import android.graphics.Point; +import android.graphics.Rect; import android.os.Handler; import android.os.Looper; import android.os.RemoteException; @@ -40,6 +40,7 @@ import androidx.annotation.Nullable; import java.util.ArrayList; import java.util.List; +import java.util.function.BiFunction; /** To run the TaskFragment animations. */ class TaskFragmentAnimationRunner extends IRemoteAnimationRunner.Stub { @@ -167,42 +168,86 @@ class TaskFragmentAnimationRunner extends IRemoteAnimationRunner.Stub { private List createOpenAnimationAdapters( @NonNull RemoteAnimationTarget[] targets) { - final List adapters = new ArrayList<>(); - for (RemoteAnimationTarget target : targets) { - final Animation animation = - mAnimationSpec.loadOpenAnimation(target.mode != MODE_CLOSING /* isEnter */); - adapters.add(new TaskFragmentAnimationAdapter(animation, target)); - } - return adapters; + return createOpenCloseAnimationAdapters(targets, + mAnimationSpec::loadOpenAnimation); } private List createCloseAnimationAdapters( @NonNull RemoteAnimationTarget[] targets) { - final List adapters = new ArrayList<>(); + return createOpenCloseAnimationAdapters(targets, + mAnimationSpec::loadCloseAnimation); + } + + private List createOpenCloseAnimationAdapters( + @NonNull RemoteAnimationTarget[] targets, + @NonNull BiFunction animationProvider) { + // We need to know if the target 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 openingTargets = new ArrayList<>(); + final List closingTargets = new ArrayList<>(); + final Rect openingWholeScreenBounds = new Rect(); + final Rect closingWholeScreenBounds = new Rect(); for (RemoteAnimationTarget target : targets) { - final Animation animation = - mAnimationSpec.loadCloseAnimation(target.mode != MODE_CLOSING /* isEnter */); - adapters.add(new TaskFragmentAnimationAdapter(animation, target)); + if (target.mode != MODE_CLOSING) { + openingTargets.add(target); + openingWholeScreenBounds.union(target.localBounds); + } else { + closingTargets.add(target); + closingWholeScreenBounds.union(target.localBounds); + } + } + + final List adapters = new ArrayList<>(); + for (RemoteAnimationTarget target : openingTargets) { + adapters.add(createOpenCloseAnimationAdapter(target, animationProvider, + openingWholeScreenBounds)); + } + for (RemoteAnimationTarget target : closingTargets) { + adapters.add(createOpenCloseAnimationAdapter(target, animationProvider, + closingWholeScreenBounds)); } return adapters; } + private TaskFragmentAnimationAdapter createOpenCloseAnimationAdapter( + @NonNull RemoteAnimationTarget target, + @NonNull BiFunction animationProvider, + @NonNull Rect wholeAnimationBounds) { + final Animation animation = animationProvider.apply(target, wholeAnimationBounds); + final Rect targetBounds = target.localBounds; + if (targetBounds.left == wholeAnimationBounds.left + && targetBounds.right != wholeAnimationBounds.right) { + // This is the left split of the whole animation window. + return new TaskFragmentAnimationAdapter.SplitAdapter(animation, target, + true /* isLeftHalf */, wholeAnimationBounds.width()); + } else if (targetBounds.left != wholeAnimationBounds.left + && targetBounds.right == wholeAnimationBounds.right) { + // This is the right split of the whole animation window. + return new TaskFragmentAnimationAdapter.SplitAdapter(animation, target, + false /* isLeftHalf */, wholeAnimationBounds.width()); + } + // Open/close window that fills the whole animation. + return new TaskFragmentAnimationAdapter(animation, target); + } + private List createChangeAnimationAdapters( @NonNull RemoteAnimationTarget[] targets) { final List adapters = new ArrayList<>(); for (RemoteAnimationTarget target : targets) { if (target.startBounds != null) { + // This is the target with bounds change. final Animation[] animations = mAnimationSpec.createChangeBoundsChangeAnimations(target); - // The snapshot surface will always be at (0, 0) of its parent. - adapters.add(new TaskFragmentAnimationAdapter(animations[0], target, - target.startLeash, false /* sizeChanged */, new Point(0, 0))); - // The end surface will have size change for scaling. - adapters.add(new TaskFragmentAnimationAdapter(animations[1], target, - target.leash, true /* sizeChanged */, null /* position */)); + // Adapter for the starting snapshot leash. + adapters.add(new TaskFragmentAnimationAdapter.SnapshotAdapter( + animations[0], target)); + // Adapter for the ending bounds changed leash. + adapters.add(new TaskFragmentAnimationAdapter.BoundsChangeAdapter( + animations[1], target)); continue; } + // These are the other targets that don't have bounds change in the same transition. final Animation animation; if (target.hasAnimatingParent) { // No-op if it will be covered by the changing parent window. diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationSpec.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationSpec.java index 11a79b2f4a27d..c0908a548501d 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationSpec.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentAnimationSpec.java @@ -176,18 +176,28 @@ class TaskFragmentAnimationSpec { return new Animation[]{startSet, endSet}; } - Animation loadOpenAnimation(boolean isEnter) { - // TODO(b/196173550) We need to customize the animation to handle two open window as one. - return mTransitionAnimation.loadDefaultAnimationAttr(isEnter + Animation loadOpenAnimation(@NonNull RemoteAnimationTarget target, + @NonNull Rect wholeAnimationBounds) { + final boolean isEnter = target.mode != MODE_CLOSING; + final Animation animation = mTransitionAnimation.loadDefaultAnimationAttr(isEnter ? R.styleable.WindowAnimation_activityOpenEnterAnimation : R.styleable.WindowAnimation_activityOpenExitAnimation); + animation.initialize(target.localBounds.width(), target.localBounds.height(), + wholeAnimationBounds.width(), wholeAnimationBounds.height()); + animation.scaleCurrentDuration(mTransitionAnimationScaleSetting); + return animation; } - Animation loadCloseAnimation(boolean isEnter) { - // TODO(b/196173550) We need to customize the animation to handle two open window as one. - return mTransitionAnimation.loadDefaultAnimationAttr(isEnter + Animation loadCloseAnimation(@NonNull RemoteAnimationTarget target, + @NonNull Rect wholeAnimationBounds) { + final boolean isEnter = target.mode != MODE_CLOSING; + final Animation animation = mTransitionAnimation.loadDefaultAnimationAttr(isEnter ? R.styleable.WindowAnimation_activityCloseEnterAnimation : R.styleable.WindowAnimation_activityCloseExitAnimation); + animation.initialize(target.localBounds.width(), target.localBounds.height(), + wholeAnimationBounds.width(), wholeAnimationBounds.height()); + animation.scaleCurrentDuration(mTransitionAnimationScaleSetting); + return animation; } private class SettingsObserver extends ContentObserver {