diff --git a/quickstep/res/layout-land/keyboard_quick_switch_taskview.xml b/quickstep/res/layout-land/keyboard_quick_switch_taskview.xml
index ebcbdcd2f2..04e87be412 100644
--- a/quickstep/res/layout-land/keyboard_quick_switch_taskview.xml
+++ b/quickstep/res/layout-land/keyboard_quick_switch_taskview.xml
@@ -25,28 +25,40 @@
android:clipToOutline="true"
launcher:borderColor="?androidprv:attr/materialColorOutline">
-
* To use this class: - * 1. Create an instance in the target view. + * 1. Create an instance in the target view. NOTE: The border will animate outwards from the + * provided border bounds. If the border will not be visible outside of those bounds, then a + * {@link ViewScaleTargetProvider} must be provided in the constructor. * 2. Override the target view's {@link android.view.View#draw(Canvas)} method and call * {@link BorderAnimator#drawBorder(Canvas)} after {@code super.draw(canvas)}. * 3. Call {@link BorderAnimator#buildAnimator(boolean)} and start the animation or call @@ -55,6 +59,7 @@ public final class BorderAnimator { @Px private final int mBorderWidthPx; @Px private final int mBorderRadiusPx; @NonNull private final Runnable mInvalidateViewCallback; + @Nullable private final ViewScaleTargetProvider mViewScaleTargetProvider; private final long mAppearanceDurationMs; private final long mDisappearanceDurationMs; @NonNull private final Interpolator mInterpolator; @@ -75,6 +80,22 @@ public final class BorderAnimator { borderRadiusPx, borderColor, invalidateViewCallback, + /* viewScaleTargetProvider= */ null); + } + + public BorderAnimator( + @NonNull BorderBoundsBuilder borderBoundsBuilder, + int borderWidthPx, + int borderRadiusPx, + @ColorInt int borderColor, + @NonNull Runnable invalidateViewCallback, + @Nullable ViewScaleTargetProvider viewScaleTargetProvider) { + this(borderBoundsBuilder, + borderWidthPx, + borderRadiusPx, + borderColor, + invalidateViewCallback, + viewScaleTargetProvider, DEFAULT_APPEARANCE_ANIMATION_DURATION_MS, DEFAULT_DISAPPEARANCE_ANIMATION_DURATION_MS, DEFAULT_INTERPOLATOR); @@ -86,6 +107,7 @@ public final class BorderAnimator { int borderRadiusPx, @ColorInt int borderColor, @NonNull Runnable invalidateViewCallback, + @Nullable ViewScaleTargetProvider viewScaleTargetProvider, long appearanceDurationMs, long disappearanceDurationMs, @NonNull Interpolator interpolator) { @@ -93,6 +115,7 @@ public final class BorderAnimator { mBorderWidthPx = borderWidthPx; mBorderRadiusPx = borderRadiusPx; mInvalidateViewCallback = invalidateViewCallback; + mViewScaleTargetProvider = viewScaleTargetProvider; mAppearanceDurationMs = appearanceDurationMs; mDisappearanceDurationMs = disappearanceDurationMs; mInterpolator = interpolator; @@ -106,8 +129,10 @@ public final class BorderAnimator { float interpolatedProgress = mInterpolator.getInterpolation( mBorderAnimationProgress.value); float borderWidth = mBorderWidthPx * interpolatedProgress; - // Inset the border by half the width to create an inwards-growth animation - mAlignmentAdjustment = borderWidth / 2f; + // Outset the border by half the width to create an outwards-growth animation + mAlignmentAdjustment = (-borderWidth / 2f) + // Inset the border if we are scaling the container up + + (mViewScaleTargetProvider == null ? 0 : mBorderWidthPx); mBorderPaint.setAlpha(Math.round(255 * interpolatedProgress)); mBorderPaint.setStrokeWidth(borderWidth); @@ -121,13 +146,16 @@ public final class BorderAnimator { * calling super. */ public void drawBorder(Canvas canvas) { + // Increase the radius if we are scaling the container up + float radiusAdjustment = mViewScaleTargetProvider == null + ? -mAlignmentAdjustment : mAlignmentAdjustment; canvas.drawRoundRect( /* left= */ mBorderBounds.left + mAlignmentAdjustment, /* top= */ mBorderBounds.top + mAlignmentAdjustment, /* right= */ mBorderBounds.right - mAlignmentAdjustment, /* bottom= */ mBorderBounds.bottom - mAlignmentAdjustment, - /* rx= */ mBorderRadiusPx - mAlignmentAdjustment, - /* ry= */ mBorderRadiusPx - mAlignmentAdjustment, + /* rx= */ mBorderRadiusPx + radiusAdjustment, + /* ry= */ mBorderRadiusPx + radiusAdjustment, /* paint= */ mBorderPaint); } @@ -141,22 +169,81 @@ public final class BorderAnimator { mRunningBorderAnimation.setDuration( isAppearing ? mAppearanceDurationMs : mDisappearanceDurationMs); + mRunningBorderAnimation.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + setViewScales(); + } + }); mRunningBorderAnimation.addListener( - AnimatorListeners.forEndCallback(() -> mRunningBorderAnimation = null)); + AnimatorListeners.forEndCallback(() -> { + mRunningBorderAnimation = null; + if (isAppearing) { + return; + } + resetViewScales(); + })); return mRunningBorderAnimation; } /** * Immediately shows/hides the border without an animation. - * + *
* To animate the appearance/disappearance, see {@link BorderAnimator#buildAnimator(boolean)} */ public void setBorderVisible(boolean visible) { if (mRunningBorderAnimation != null) { mRunningBorderAnimation.end(); } + mBorderBoundsBuilder.updateBorderBounds(mBorderBounds); + if (visible) { + setViewScales(); + } mBorderAnimationProgress.updateValue(visible ? 1f : 0f); + if (!visible) { + resetViewScales(); + } + } + + private void setViewScales() { + if (mViewScaleTargetProvider == null) { + return; + } + View container = mViewScaleTargetProvider.getContainerView(); + float width = container.getWidth(); + float height = container.getHeight(); + // scale up just enough to make room for the border + float scaleX = 1f + ((2 * mBorderWidthPx) / width); + float scaleY = 1f + ((2 * mBorderWidthPx) / height); + + container.setPivotX(width / 2); + container.setPivotY(height / 2); + container.setScaleX(scaleX); + container.setScaleY(scaleY); + + View contentView = mViewScaleTargetProvider.getContentView(); + contentView.setPivotX(contentView.getWidth() / 2f); + contentView.setPivotY(contentView.getHeight() / 2f); + contentView.setScaleX(1f / scaleX); + contentView.setScaleY(1f / scaleY); + } + + private void resetViewScales() { + if (mViewScaleTargetProvider == null) { + return; + } + View container = mViewScaleTargetProvider.getContainerView(); + container.setPivotX(container.getWidth()); + container.setPivotY(container.getHeight()); + container.setScaleX(1f); + container.setScaleY(1f); + + View contentView = mViewScaleTargetProvider.getContentView(); + contentView.setPivotX(contentView.getWidth() / 2f); + contentView.setPivotY(contentView.getHeight() / 2f); + contentView.setScaleX(1f); + contentView.setScaleY(1f); } /** @@ -169,4 +256,25 @@ public final class BorderAnimator { */ void updateBorderBounds(Rect rect); } + + /** + * Provider for scaling target views for the beginning and end of this animation. + */ + public interface ViewScaleTargetProvider { + + /** + * Returns the content view's container. This view will be scaled up to make room for the + * border. + */ + @NonNull + View getContainerView(); + + /** + * Returns the content view. This view will be scaled down reciprocally to the container's + * up-scaling to maintain its original size. This should be the view containing all of the + * content being surrounded by the border. + */ + @NonNull + View getContentView(); + } }