From 18728b32f8bb757b5ce58bdb3c84a435f37aff21 Mon Sep 17 00:00:00 2001 From: Pablo Gamito Date: Wed, 15 Dec 2021 17:00:14 +0100 Subject: [PATCH 1/2] Add API for extending windows during animations Provides an API for windows to extend themselves outwards during animations. In contrast to the scale animation the activity is not distorted but rather the surface is extended outwards by the specified amount without modifying the original window. The new "extension" space that is added is filled by clamping the edges of the surface. In the future the API could be extended to support different modes of filling the extension areas if required. This API allows for the extension amount of the window to be animated much like a scale or cliprect animation is, by providing a starting amount for the extension and transitioning to the end amount by the end of the animation. This is required for the go/t-activity-transitions (https://direct.googleplex.com/#/spec/241720001&247800001) which we want to use as the default transition for T. Test: atest CtsWindowManagerDeviceTestCases:AnimationEdgeExtensionTests Bug: 202844659 Change-Id: Ic21e139fe329c271e71abdc6e77714c49365d5d6 --- core/api/current.txt | 8 + .../android/view/animation/Animation.java | 9 + .../android/view/animation/AnimationSet.java | 11 ++ .../view/animation/AnimationUtils.java | 2 + .../view/animation/ExtendAnimation.java | 176 ++++++++++++++++++ .../view/animation/Transformation.java | 46 ++++- core/res/res/values/attrs.xml | 32 +++- core/res/res/values/public.xml | 8 + 8 files changed, 281 insertions(+), 11 deletions(-) create mode 100644 core/java/android/view/animation/ExtendAnimation.java diff --git a/core/api/current.txt b/core/api/current.txt index d5be3495d371b..9b41f44ad6c08 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -729,6 +729,10 @@ package android { field public static final int freezesText = 16843116; // 0x101016c field public static final int fromAlpha = 16843210; // 0x10101ca field public static final int fromDegrees = 16843187; // 0x10101b3 + field public static final int fromExtendBottom; + field public static final int fromExtendLeft; + field public static final int fromExtendRight; + field public static final int fromExtendTop; field public static final int fromId = 16843850; // 0x101044a field public static final int fromScene = 16843741; // 0x10103dd field public static final int fromXDelta = 16843206; // 0x10101c6 @@ -1574,6 +1578,10 @@ package android { field public static final int titleTextStyle = 16843512; // 0x10102f8 field public static final int toAlpha = 16843211; // 0x10101cb field public static final int toDegrees = 16843188; // 0x10101b4 + field public static final int toExtendBottom; + field public static final int toExtendLeft; + field public static final int toExtendRight; + field public static final int toExtendTop; field public static final int toId = 16843849; // 0x1010449 field public static final int toScene = 16843742; // 0x10103de field public static final int toXDelta = 16843207; // 0x10101c7 diff --git a/core/java/android/view/animation/Animation.java b/core/java/android/view/animation/Animation.java index ab749ee284a86..c42c816f217ec 100644 --- a/core/java/android/view/animation/Animation.java +++ b/core/java/android/view/animation/Animation.java @@ -864,6 +864,15 @@ public abstract class Animation implements Cloneable { return mHasRoundedCorners; } + /** + * @return if a window animation has outsets applied to it. + * + * @hide + */ + public boolean hasExtension() { + return false; + } + /** * If showBackground is {@code true} and this animation is applied on a window, then the windows * in the animation will animate with the background associated with this window behind them. diff --git a/core/java/android/view/animation/AnimationSet.java b/core/java/android/view/animation/AnimationSet.java index 03c6ca69a592c..a5ea6f6b058af 100644 --- a/core/java/android/view/animation/AnimationSet.java +++ b/core/java/android/view/animation/AnimationSet.java @@ -517,4 +517,15 @@ public class AnimationSet extends Animation { public boolean willChangeBounds() { return (mFlags & PROPERTY_CHANGE_BOUNDS_MASK) == PROPERTY_CHANGE_BOUNDS_MASK; } + + /** @hide */ + @Override + public boolean hasExtension() { + for (Animation animation : mAnimations) { + if (animation.hasExtension()) { + return true; + } + } + return false; + } } diff --git a/core/java/android/view/animation/AnimationUtils.java b/core/java/android/view/animation/AnimationUtils.java index 7ce0f4571f762..7d1dc76608718 100644 --- a/core/java/android/view/animation/AnimationUtils.java +++ b/core/java/android/view/animation/AnimationUtils.java @@ -190,6 +190,8 @@ public class AnimationUtils { anim = new TranslateAnimation(c, attrs); } else if (name.equals("cliprect")) { anim = new ClipRectAnimation(c, attrs); + } else if (name.equals("extend")) { + anim = new ExtendAnimation(c, attrs); } else { throw new RuntimeException("Unknown animation name: " + parser.getName()); } diff --git a/core/java/android/view/animation/ExtendAnimation.java b/core/java/android/view/animation/ExtendAnimation.java new file mode 100644 index 0000000000000..fd627e50ab0e5 --- /dev/null +++ b/core/java/android/view/animation/ExtendAnimation.java @@ -0,0 +1,176 @@ +/* + * 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 android.view.animation; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Insets; +import android.util.AttributeSet; + +/** + * An animation that controls the outset of an object. + * + * @hide + */ +public class ExtendAnimation extends Animation { + protected Insets mFromInsets = Insets.NONE; + protected Insets mToInsets = Insets.NONE; + + private int mFromLeftType = ABSOLUTE; + private int mFromTopType = ABSOLUTE; + private int mFromRightType = ABSOLUTE; + private int mFromBottomType = ABSOLUTE; + + private int mToLeftType = ABSOLUTE; + private int mToTopType = ABSOLUTE; + private int mToRightType = ABSOLUTE; + private int mToBottomType = ABSOLUTE; + + private float mFromLeftValue; + private float mFromTopValue; + private float mFromRightValue; + private float mFromBottomValue; + + private float mToLeftValue; + private float mToTopValue; + private float mToRightValue; + private float mToBottomValue; + + /** + * Constructor used when an ExtendAnimation is loaded from a resource. + * + * @param context Application context to use + * @param attrs Attribute set from which to read values + */ + public ExtendAnimation(Context context, AttributeSet attrs) { + super(context, attrs); + + TypedArray a = context.obtainStyledAttributes(attrs, + com.android.internal.R.styleable.ExtendAnimation); + + Description d = Description.parseValue(a.peekValue( + com.android.internal.R.styleable.ExtendAnimation_fromExtendLeft)); + mFromLeftType = d.type; + mFromLeftValue = d.value; + + d = Description.parseValue(a.peekValue( + com.android.internal.R.styleable.ExtendAnimation_fromExtendTop)); + mFromTopType = d.type; + mFromTopValue = d.value; + + d = Description.parseValue(a.peekValue( + com.android.internal.R.styleable.ExtendAnimation_fromExtendRight)); + mFromRightType = d.type; + mFromRightValue = d.value; + + d = Description.parseValue(a.peekValue( + com.android.internal.R.styleable.ExtendAnimation_fromExtendBottom)); + mFromBottomType = d.type; + mFromBottomValue = d.value; + + + d = Description.parseValue(a.peekValue( + com.android.internal.R.styleable.ExtendAnimation_toExtendLeft)); + mToLeftType = d.type; + mToLeftValue = d.value; + + d = Description.parseValue(a.peekValue( + com.android.internal.R.styleable.ExtendAnimation_toExtendTop)); + mToTopType = d.type; + mToTopValue = d.value; + + d = Description.parseValue(a.peekValue( + com.android.internal.R.styleable.ExtendAnimation_toExtendRight)); + mToRightType = d.type; + mToRightValue = d.value; + + d = Description.parseValue(a.peekValue( + com.android.internal.R.styleable.ExtendAnimation_toExtendBottom)); + mToBottomType = d.type; + mToBottomValue = d.value; + + a.recycle(); + } + + /** + * Constructor to use when building an ExtendAnimation from code + * + * @param fromInsets the insets to animate from + * @param toInsets the insets to animate to + */ + public ExtendAnimation(Insets fromInsets, Insets toInsets) { + if (fromInsets == null || toInsets == null) { + throw new RuntimeException("Expected non-null animation outsets"); + } + mFromLeftValue = -fromInsets.left; + mFromTopValue = -fromInsets.top; + mFromRightValue = -fromInsets.right; + mFromBottomValue = -fromInsets.bottom; + + mToLeftValue = -toInsets.left; + mToTopValue = -toInsets.top; + mToRightValue = -toInsets.right; + mToBottomValue = -toInsets.bottom; + } + + /** + * Constructor to use when building an ExtendAnimation from code + */ + public ExtendAnimation(int fromL, int fromT, int fromR, int fromB, + int toL, int toT, int toR, int toB) { + this(Insets.of(-fromL, -fromT, -fromR, -fromB), Insets.of(-toL, -toT, -toR, -toB)); + } + + @Override + protected void applyTransformation(float it, Transformation tr) { + int l = mFromInsets.left + (int) ((mToInsets.left - mFromInsets.left) * it); + int t = mFromInsets.top + (int) ((mToInsets.top - mFromInsets.top) * it); + int r = mFromInsets.right + (int) ((mToInsets.right - mFromInsets.right) * it); + int b = mFromInsets.bottom + (int) ((mToInsets.bottom - mFromInsets.bottom) * it); + tr.setInsets(l, t, r, b); + } + + @Override + public boolean willChangeTransformationMatrix() { + return false; + } + + /** @hide */ + @Override + public boolean hasExtension() { + return mFromInsets.left < 0 || mFromInsets.top < 0 || mFromInsets.right < 0 + || mFromInsets.bottom < 0; + } + + @Override + public void initialize(int width, int height, int parentWidth, int parentHeight) { + super.initialize(width, height, parentWidth, parentHeight); + // We remove any negative extension (i.e. positive insets) and set those to 0 + mFromInsets = Insets.min(Insets.of( + -(int) resolveSize(mFromLeftType, mFromLeftValue, width, parentWidth), + -(int) resolveSize(mFromTopType, mFromTopValue, height, parentHeight), + -(int) resolveSize(mFromRightType, mFromRightValue, width, parentWidth), + -(int) resolveSize(mFromBottomType, mFromBottomValue, height, parentHeight) + ), Insets.NONE); + mToInsets = Insets.min(Insets.of( + -(int) resolveSize(mToLeftType, mToLeftValue, width, parentWidth), + -(int) resolveSize(mToTopType, mToTopValue, height, parentHeight), + -(int) resolveSize(mToRightType, mToRightValue, width, parentWidth), + -(int) resolveSize(mToBottomType, mToBottomValue, height, parentHeight) + ), Insets.NONE); + } +} diff --git a/core/java/android/view/animation/Transformation.java b/core/java/android/view/animation/Transformation.java index b35a66e6eb26b..bd623088017a3 100644 --- a/core/java/android/view/animation/Transformation.java +++ b/core/java/android/view/animation/Transformation.java @@ -18,6 +18,7 @@ package android.view.animation; import android.annotation.FloatRange; import android.compat.annotation.UnsupportedAppUsage; +import android.graphics.Insets; import android.graphics.Matrix; import android.graphics.Rect; @@ -53,6 +54,8 @@ public class Transformation { private boolean mHasClipRect; private Rect mClipRect = new Rect(); + private Insets mInsets = Insets.NONE; + /** * Creates a new transformation with alpha = 1 and the identity matrix. */ @@ -132,8 +135,9 @@ public class Transformation { setClipRect(bounds); } } + setInsets(Insets.add(getInsets(), t.getInsets())); } - + /** * Like {@link #compose(Transformation)} but does this.postConcat(t) of * the transformation matrix. @@ -160,7 +164,7 @@ public class Transformation { public Matrix getMatrix() { return mMatrix; } - + /** * Sets the degree of transparency * @param alpha 1.0 means fully opaqe and 0.0 means fully transparent @@ -169,6 +173,13 @@ public class Transformation { mAlpha = alpha; } + /** + * @return The degree of transparency + */ + public float getAlpha() { + return mAlpha; + } + /** * Sets the current Transform's clip rect * @hide @@ -203,12 +214,29 @@ public class Transformation { } /** - * @return The degree of transparency + * Sets the current Transform's insets + * @hide */ - public float getAlpha() { - return mAlpha; + public void setInsets(Insets insets) { + mInsets = insets; } - + + /** + * Sets the current Transform's insets + * @hide + */ + public void setInsets(int left, int top, int right, int bottom) { + mInsets = Insets.of(left, top, right, bottom); + } + + /** + * Returns the current Transform's outset rect + * @hide + */ + public Insets getInsets() { + return mInsets; + } + @Override public String toString() { StringBuilder sb = new StringBuilder(64); @@ -216,7 +244,7 @@ public class Transformation { toShortString(sb); return sb.toString(); } - + /** * Return a string representation of the transformation in a compact form. */ @@ -225,7 +253,7 @@ public class Transformation { toShortString(sb); return sb.toString(); } - + /** * @hide */ @@ -234,7 +262,7 @@ public class Transformation { sb.append(" matrix="); sb.append(mMatrix.toShortString()); sb.append('}'); } - + /** * Print short string, to optimize dumping. * @hide diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index 7916ef43da509..04e29890568a9 100644 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -2885,7 +2885,7 @@ public void sayHello(View v) method of your context (typically, your Activity). {@deprecated View actually traverses the Context - hierarchy looking for the relevant method, which is fragile (an intermediate + hierarchy looking for the relevant method, which is fragile (an intermediate ContextWrapper adding a same-named method would change behavior) and restricts bytecode optimizers such as R8. Instead, use View.setOnClickListener.}--> @@ -6975,6 +6975,34 @@ + + + + + + + + + + + + + + + + + + + + @@ -7827,7 +7855,7 @@ diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml index 2e96c65ef17ff..047c04b3596c7 100644 --- a/core/res/res/values/public.xml +++ b/core/res/res/values/public.xml @@ -3260,6 +3260,14 @@ + + + + + + + + From 108a2ad186563cc614d4213414a56c815eb8e154 Mon Sep 17 00:00:00 2001 From: Pablo Gamito Date: Wed, 15 Dec 2021 17:00:53 +0100 Subject: [PATCH 2/2] Support edge extension of windows during animations in shell This is required for the go/t-activity-transition (https://direct.googleplex.com/#/spec/241720001&247800001) which we want to use as the default transition for T Test: atest FlickerTests Bug: 202844659 Change-Id: I316ddfb937953cf462ac999b8d27ae4cb0128847 --- .../android/view/animation/Animation.java | 18 +- .../android/view/animation/AnimationSet.java | 20 ++ .../transition/DefaultTransitionHandler.java | 181 +++++++++++++++++- 3 files changed, 211 insertions(+), 8 deletions(-) diff --git a/core/java/android/view/animation/Animation.java b/core/java/android/view/animation/Animation.java index c42c816f217ec..3914a3c963b6a 100644 --- a/core/java/android/view/animation/Animation.java +++ b/core/java/android/view/animation/Animation.java @@ -950,6 +950,21 @@ public abstract class Animation implements Cloneable { return (getStartOffset() + getDuration()) * (getRepeatCount() + 1); } + /** + * Gets the transformation to apply a specific point in time. Implementations of this method + * should always be kept in sync with getTransformation. + * + * @param normalizedTime time between 0 and 1 where 0 is the start of the animation and 1 the + * end. + * @param outTransformation A transformation object that is provided by the + * caller and will be filled in by the animation. + * @hide + */ + public void getTransformationAt(float normalizedTime, Transformation outTransformation) { + final float interpolatedTime = mInterpolator.getInterpolation(normalizedTime); + applyTransformation(interpolatedTime, outTransformation); + } + /** * Gets the transformation to apply at a specified point in time. Implementations of this * method should always replace the specified Transformation or document they are doing @@ -996,8 +1011,7 @@ public abstract class Animation implements Cloneable { normalizedTime = 1.0f - normalizedTime; } - final float interpolatedTime = mInterpolator.getInterpolation(normalizedTime); - applyTransformation(interpolatedTime, outTransformation); + getTransformationAt(normalizedTime, outTransformation); } if (expired) { diff --git a/core/java/android/view/animation/AnimationSet.java b/core/java/android/view/animation/AnimationSet.java index a5ea6f6b058af..a2f3544c70ab3 100644 --- a/core/java/android/view/animation/AnimationSet.java +++ b/core/java/android/view/animation/AnimationSet.java @@ -359,6 +359,26 @@ public class AnimationSet extends Animation { } } + /** + * The transformation of an animation set is the concatenation of all of its + * component animations. + * + * @see android.view.animation.Animation#getTransformationAt + * @hide + */ + @Override + public void getTransformationAt(float interpolatedTime, Transformation t) { + final Transformation temp = mTempTransformation; + + for (int i = mAnimations.size() - 1; i >= 0; --i) { + final Animation a = mAnimations.get(i); + + temp.clear(); + a.getTransformationAt(interpolatedTime, t); + t.compose(temp); + } + } + /** * The transformation of an animation set is the concatenation of all of its * component animations. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java index 27b6dc5b304f0..ddf01a8c5ee95 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java @@ -67,7 +67,11 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.graphics.Canvas; import android.graphics.Color; +import android.graphics.Insets; +import android.graphics.Paint; +import android.graphics.PixelFormat; import android.graphics.Point; import android.graphics.Rect; import android.graphics.drawable.Drawable; @@ -78,6 +82,7 @@ import android.os.SystemProperties; import android.os.UserHandle; import android.util.ArrayMap; import android.view.Choreographer; +import android.view.Surface; import android.view.SurfaceControl; import android.view.SurfaceSession; import android.view.WindowManager; @@ -103,6 +108,8 @@ import com.android.wm.shell.common.TransactionPool; import com.android.wm.shell.protolog.ShellProtoLogGroup; import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; /** The default handler that handles anything not already handled. */ public class DefaultTransitionHandler implements Transitions.TransitionHandler { @@ -331,6 +338,9 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { finishCallback.onTransitionFinished(null /* wct */, null /* wctCB */); }; + final List> postStartTransactionCallbacks = + new ArrayList<>(); + @ColorInt int backgroundColorForTransition = 0; final int wallpaperTransit = getWallpaperTransitType(info); for (int i = info.getChanges().size() - 1; i >= 0; --i) { @@ -402,13 +412,15 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { } } - float cornerRadius = 0; + final float cornerRadius; if (a.hasRoundedCorners() && isTask) { // hasRoundedCorners is currently only enabled for tasks final Context displayContext = mDisplayController.getDisplayContext(change.getTaskInfo().displayId); cornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(displayContext); + } else { + cornerRadius = 0; } if (a.getShowBackground()) { @@ -424,12 +436,37 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { } } + boolean delayedEdgeExtension = false; + if (!isTask && a.hasExtension()) { + if (!Transitions.isOpeningType(change.getMode())) { + // Can screenshot now (before startTransaction is applied) + edgeExtendWindow(change, a, startTransaction, finishTransaction); + } else { + // Need to screenshot after startTransaction is applied otherwise activity + // may not be visible or ready yet. + postStartTransactionCallbacks + .add(t -> edgeExtendWindow(change, a, t, finishTransaction)); + delayedEdgeExtension = true; + } + } + final Rect clipRect = Transitions.isClosingType(change.getMode()) ? mRotator.getEndBoundsInStartRotation(change) : change.getEndAbsBounds(); - startSurfaceAnimation(animations, a, change.getLeash(), onAnimFinish, - mTransactionPool, mMainExecutor, mAnimExecutor, null /* position */, - cornerRadius, clipRect); + + if (delayedEdgeExtension) { + // If the edge extension needs to happen after the startTransition has been + // applied, then we want to only start the animation after the edge extension + // postStartTransaction callback has been run + postStartTransactionCallbacks.add(t -> + startSurfaceAnimation(animations, a, change.getLeash(), onAnimFinish, + mTransactionPool, mMainExecutor, mAnimExecutor, + null /* position */, cornerRadius, clipRect)); + } else { + startSurfaceAnimation(animations, a, change.getLeash(), onAnimFinish, + mTransactionPool, mMainExecutor, mAnimExecutor, null /* position */, + cornerRadius, clipRect); + } if (info.getAnimationOptions() != null) { attachThumbnail(animations, onAnimFinish, change, info.getAnimationOptions(), @@ -443,7 +480,20 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { startTransaction, finishTransaction); } - startTransaction.apply(); + // postStartTransactionCallbacks require that the start transaction is already + // applied to run otherwise they may result in flickers and UI inconsistencies. + boolean waitForStartTransactionApply = postStartTransactionCallbacks.size() > 0; + startTransaction.apply(waitForStartTransactionApply); + + // Run tasks that require startTransaction to already be applied + for (Consumer postStartTransactionCallback : + postStartTransactionCallbacks) { + final SurfaceControl.Transaction t = mTransactionPool.acquire(); + postStartTransactionCallback.accept(t); + t.apply(); + mTransactionPool.release(t); + } + mRotator.cleanUp(finishTransaction); TransitionMetrics.getInstance().reportAnimationStart(transition); // run finish now in-case there are no animations @@ -451,6 +501,117 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { return true; } + private void edgeExtendWindow(TransitionInfo.Change change, + Animation a, SurfaceControl.Transaction startTransaction, + SurfaceControl.Transaction finishTransaction) { + final Transformation transformationAtStart = new Transformation(); + a.getTransformationAt(0, transformationAtStart); + final Transformation transformationAtEnd = new Transformation(); + a.getTransformationAt(1, transformationAtEnd); + + // We want to create an extension surface that is the maximal size and the animation will + // take care of cropping any part that overflows. + final Insets maxExtensionInsets = Insets.min( + transformationAtStart.getInsets(), transformationAtEnd.getInsets()); + + final int targetSurfaceHeight = Math.max(change.getStartAbsBounds().height(), + change.getEndAbsBounds().height()); + final int targetSurfaceWidth = Math.max(change.getStartAbsBounds().width(), + change.getEndAbsBounds().width()); + if (maxExtensionInsets.left < 0) { + final Rect edgeBounds = new Rect(0, 0, 1, targetSurfaceHeight); + final Rect extensionRect = new Rect(0, 0, + -maxExtensionInsets.left, targetSurfaceHeight); + final int xPos = maxExtensionInsets.left; + final int yPos = 0; + createExtensionSurface(change.getLeash(), edgeBounds, extensionRect, xPos, yPos, + "Left Edge Extension", startTransaction, finishTransaction); + } + + if (maxExtensionInsets.top < 0) { + final Rect edgeBounds = new Rect(0, 0, targetSurfaceWidth, 1); + final Rect extensionRect = new Rect(0, 0, + targetSurfaceWidth, -maxExtensionInsets.top); + final int xPos = 0; + final int yPos = maxExtensionInsets.top; + createExtensionSurface(change.getLeash(), edgeBounds, extensionRect, xPos, yPos, + "Top Edge Extension", startTransaction, finishTransaction); + } + + if (maxExtensionInsets.right < 0) { + final Rect edgeBounds = new Rect(targetSurfaceWidth - 1, 0, + targetSurfaceWidth, targetSurfaceHeight); + final Rect extensionRect = new Rect(0, 0, + -maxExtensionInsets.right, targetSurfaceHeight); + final int xPos = targetSurfaceWidth; + final int yPos = 0; + createExtensionSurface(change.getLeash(), edgeBounds, extensionRect, xPos, yPos, + "Right Edge Extension", startTransaction, finishTransaction); + } + + if (maxExtensionInsets.bottom < 0) { + final Rect edgeBounds = new Rect(0, targetSurfaceHeight - 1, + targetSurfaceWidth, targetSurfaceHeight); + final Rect extensionRect = new Rect(0, 0, + targetSurfaceWidth, -maxExtensionInsets.bottom); + final int xPos = maxExtensionInsets.left; + final int yPos = targetSurfaceHeight; + createExtensionSurface(change.getLeash(), edgeBounds, extensionRect, xPos, yPos, + "Bottom Edge Extension", startTransaction, finishTransaction); + } + } + + private SurfaceControl createExtensionSurface(SurfaceControl surfaceToExtend, Rect edgeBounds, + Rect extensionRect, int xPos, int yPos, String layerName, + SurfaceControl.Transaction startTransaction, + SurfaceControl.Transaction finishTransaction) { + final SurfaceControl edgeExtensionLayer = new SurfaceControl.Builder() + .setName(layerName) + .setParent(surfaceToExtend) + .setHidden(true) + .setCallsite("DefaultTransitionHandler#startAnimation") + .setOpaque(true) + .setBufferSize(extensionRect.width(), extensionRect.height()) + .build(); + + SurfaceControl.LayerCaptureArgs captureArgs = + new SurfaceControl.LayerCaptureArgs.Builder(surfaceToExtend) + .setSourceCrop(edgeBounds) + .setFrameScale(1) + .setPixelFormat(PixelFormat.RGBA_8888) + .setChildrenOnly(true) + .setAllowProtected(true) + .build(); + final SurfaceControl.ScreenshotHardwareBuffer edgeBuffer = + SurfaceControl.captureLayers(captureArgs); + + if (edgeBuffer == null) { + ProtoLog.e(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, + "Failed to capture edge of window."); + return null; + } + + android.graphics.BitmapShader shader = + new android.graphics.BitmapShader(edgeBuffer.asBitmap(), + android.graphics.Shader.TileMode.CLAMP, + android.graphics.Shader.TileMode.CLAMP); + final Paint paint = new Paint(); + paint.setShader(shader); + + final Surface surface = new Surface(edgeExtensionLayer); + Canvas c = surface.lockHardwareCanvas(); + c.drawRect(extensionRect, paint); + surface.unlockCanvasAndPost(c); + surface.release(); + + startTransaction.setLayer(edgeExtensionLayer, Integer.MIN_VALUE); + startTransaction.setPosition(edgeExtensionLayer, xPos, yPos); + startTransaction.setVisibility(edgeExtensionLayer, true); + finishTransaction.remove(edgeExtensionLayer); + + return edgeExtensionLayer; + } + private void addBackgroundToTransition( @NonNull SurfaceControl rootLeash, @ColorInt int color, @@ -778,9 +939,17 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { } t.setMatrix(leash, transformation.getMatrix(), matrix); t.setAlpha(leash, transformation.getAlpha()); + + Insets extensionInsets = Insets.min(transformation.getInsets(), Insets.NONE); + if (!extensionInsets.equals(Insets.NONE) && clipRect != null && !clipRect.isEmpty()) { + // Clip out any overflowing edge extension + clipRect.inset(extensionInsets); + t.setCrop(leash, clipRect); + } + if (anim.hasRoundedCorners() && cornerRadius > 0 && clipRect != null) { // We can only apply rounded corner if a crop is set - t.setWindowCrop(leash, clipRect); + t.setCrop(leash, clipRect); t.setCornerRadius(leash, cornerRadius); }