diff options
| author | 2024-10-25 10:47:23 -0700 | |
|---|---|---|
| committer | 2024-10-25 10:53:36 -0700 | |
| commit | cf5da718513fc85c391673c0632a1d1058927ab8 (patch) | |
| tree | b66dab1106aef93276fdc8333a285b50c612d2c2 | |
| parent | c7f83672456247d1401f97fc2fa1679f12f0b478 (diff) | |
Implement content overlay in PiP2
Make sure the direct enter PiP and auto-enter PiP
with invalid source-rect-hint use app icon overlay
during the enter-animation.
Once the enter animation is done, we start fading the overlay out.
Note: with PiP2, we can finish the transition and set the state to
ENTERED_PIP right away even before overlay fadeout is done.
This allows PiP interactions earlier than in PiP1.
Bug: 373946868
Flag: com.android.wm.shell.enable_pip2
Test: auto-enter PiP in btn-nav with invalid src-rect-hint
Change-Id: I2156b7890243c13f278d6023802e41e69db09683
5 files changed, 266 insertions, 44 deletions
diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/pip/PipContentOverlay.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/pip/PipContentOverlay.java index 6c83d88032df..eb7ef1478a90 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/pip/PipContentOverlay.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/pip/PipContentOverlay.java @@ -63,8 +63,19 @@ public abstract class PipContentOverlay { * @param currentBounds {@link Rect} of the current animation bounds. * @param fraction progress of the animation ranged from 0f to 1f. */ - public abstract void onAnimationUpdate(SurfaceControl.Transaction atomicTx, - Rect currentBounds, float fraction); + public void onAnimationUpdate(SurfaceControl.Transaction atomicTx, + Rect currentBounds, float fraction) {} + + /** + * Animates the internal {@link #mLeash} by a given fraction for a config-at-end transition. + * @param atomicTx {@link SurfaceControl.Transaction} to operate, you should not explicitly + * call apply on this transaction, it should be applied on the caller side. + * @param scale scaling to apply onto the overlay. + * @param fraction progress of the animation ranged from 0f to 1f. + * @param endBounds the final bounds PiP is animating into. + */ + public void onAnimationUpdate(SurfaceControl.Transaction atomicTx, + float scale, float fraction, Rect endBounds) {} /** A {@link PipContentOverlay} uses solid color. */ public static final class PipColorOverlay extends PipContentOverlay { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipEnterAnimator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipEnterAnimator.java index 5381a626ddcf..740b9af56144 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipEnterAnimator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipEnterAnimator.java @@ -23,6 +23,7 @@ import android.animation.Animator; import android.animation.RectEvaluator; import android.animation.ValueAnimator; import android.content.Context; +import android.content.pm.ActivityInfo; import android.graphics.Matrix; import android.graphics.PointF; import android.graphics.Rect; @@ -33,10 +34,13 @@ import android.window.TransitionInfo; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.android.launcher3.icons.IconProvider; import com.android.wm.shell.R; import com.android.wm.shell.common.pip.PipUtils; import com.android.wm.shell.pip2.PipSurfaceTransactionHelper; +import com.android.wm.shell.pip2.phone.PipAppIconOverlay; import com.android.wm.shell.shared.animation.Interpolators; +import com.android.wm.shell.shared.pip.PipContentOverlay; /** * Animator that handles bounds animations for entering PIP. @@ -59,6 +63,10 @@ public class PipEnterAnimator extends ValueAnimator private final PipSurfaceTransactionHelper.SurfaceControlTransactionFactory mSurfaceControlTransactionFactory; + Matrix mTransformTensor = new Matrix(); + final float[] mMatrixTmp = new float[9]; + @Nullable private PipContentOverlay mContentOverlay; + // Internal state representing initial transform - cached to avoid recalculation. private final PointF mInitScale = new PointF(); @@ -67,9 +75,6 @@ public class PipEnterAnimator extends ValueAnimator private final PointF mInitActivityScale = new PointF(); private final PointF mInitActivityPos = new PointF(); - Matrix mTransformTensor = new Matrix(); - final float[] mMatrixTmp = new float[9]; - public PipEnterAnimator(Context context, @NonNull SurfaceControl leash, SurfaceControl.Transaction startTransaction, @@ -161,10 +166,15 @@ public class PipEnterAnimator extends ValueAnimator mRectEvaluator.evaluate(fraction, initCrop, endCrop); tx.setCrop(mLeash, mAnimatedRect); + mTransformTensor.reset(); mTransformTensor.setScale(scaleX, scaleY); mTransformTensor.postTranslate(posX, posY); mTransformTensor.postRotate(degrees); tx.setMatrix(mLeash, mTransformTensor, mMatrixTmp); + + if (mContentOverlay != null) { + mContentOverlay.onAnimationUpdate(tx, 1f / scaleX, fraction, mEndBounds); + } } // no-ops @@ -200,4 +210,48 @@ public class PipEnterAnimator extends ValueAnimator } PipUtils.calcStartTransform(pipChange, mInitScale, mInitPos, mInitCrop); } + + /** + * Initializes and attaches an app icon overlay on top of the PiP layer. + */ + public void setAppIconContentOverlay(Context context, Rect appBounds, Rect destinationBounds, + ActivityInfo activityInfo, int appIconSizePx) { + reattachAppIconOverlay( + new PipAppIconOverlay(context, appBounds, destinationBounds, + new IconProvider(context).getIcon(activityInfo), appIconSizePx)); + } + + private void reattachAppIconOverlay(PipAppIconOverlay overlay) { + final SurfaceControl.Transaction tx = + mSurfaceControlTransactionFactory.getTransaction(); + if (mContentOverlay != null) { + mContentOverlay.detach(tx); + } + mContentOverlay = overlay; + mContentOverlay.attach(tx, mLeash); + } + + /** + * Clears the {@link #mContentOverlay}, this should be done after the content overlay is + * faded out. + */ + public void clearAppIconOverlay() { + if (mContentOverlay == null) { + return; + } + SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); + mContentOverlay.detach(tx); + mContentOverlay = null; + } + + /** + * @return the app icon overlay leash; null if no overlay is attached. + */ + @Nullable + public SurfaceControl getContentOverlayLeash() { + if (mContentOverlay == null) { + return null; + } + return mContentOverlay.getLeash(); + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipAppIconOverlay.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipAppIconOverlay.java new file mode 100644 index 000000000000..b4cf8905d02e --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipAppIconOverlay.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2024 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.pip2.phone; + +import static android.util.TypedValue.COMPLEX_UNIT_DIP; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Matrix; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.util.TypedValue; +import android.view.SurfaceControl; + +import com.android.wm.shell.shared.pip.PipContentOverlay; + +/** A {@link PipContentOverlay} shows app icon on solid color background. */ +public final class PipAppIconOverlay extends PipContentOverlay { + private static final String TAG = PipAppIconOverlay.class.getSimpleName(); + // The maximum size for app icon in pixel. + private static final int MAX_APP_ICON_SIZE_DP = 72; + + private final Context mContext; + private final int mAppIconSizePx; + private final Rect mAppBounds; + private final int mOverlayHalfSize; + private final Matrix mTmpTransform = new Matrix(); + private final float[] mTmpFloat9 = new float[9]; + + private Bitmap mBitmap; + + public PipAppIconOverlay(Context context, Rect appBounds, Rect destinationBounds, + Drawable appIcon, int appIconSizePx) { + mContext = context; + final int maxAppIconSizePx = (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP, + MAX_APP_ICON_SIZE_DP, context.getResources().getDisplayMetrics()); + mAppIconSizePx = Math.min(maxAppIconSizePx, appIconSizePx); + + final int overlaySize = getOverlaySize(appBounds, destinationBounds); + mOverlayHalfSize = overlaySize >> 1; + + // When the activity is in the secondary split, make sure the scaling center is not + // offset. + mAppBounds = new Rect(0, 0, appBounds.width(), appBounds.height()); + + mBitmap = Bitmap.createBitmap(overlaySize, overlaySize, Bitmap.Config.ARGB_8888); + prepareAppIconOverlay(appIcon); + mLeash = new SurfaceControl.Builder() + .setCallsite(TAG) + .setName(LAYER_NAME) + .build(); + } + + /** + * Returns the size of the app icon overlay. + * + * In order to have the overlay always cover the pip window during the transition, + * the overlay will be drawn with the max size of the start and end bounds in different + * rotation. + */ + public static int getOverlaySize(Rect appBounds, Rect destinationBounds) { + final int appWidth = appBounds.width(); + final int appHeight = appBounds.height(); + + return Math.max(Math.max(appWidth, appHeight), + Math.max(destinationBounds.width(), destinationBounds.height())) + 1; + } + + @Override + public void attach(SurfaceControl.Transaction tx, SurfaceControl parentLeash) { + tx.show(mLeash); + tx.setLayer(mLeash, Integer.MAX_VALUE); + tx.setBuffer(mLeash, mBitmap.getHardwareBuffer()); + tx.setAlpha(mLeash, 0f); + tx.reparent(mLeash, parentLeash); + tx.apply(); + } + + @Override + public void onAnimationUpdate(SurfaceControl.Transaction atomicTx, + float scale, float fraction, Rect endBounds) { + mTmpTransform.reset(); + // Scale back the bitmap with the pivot at parent origin + mTmpTransform.setScale(scale, scale); + // We are negative-cropping away from the final bounds crop in config-at-end enter PiP; + // this means that the overlay shift depends on the final bounds. + // Note: translation is also dependent on the scaling of the parent. + mTmpTransform.postTranslate(endBounds.width() / 2f - mOverlayHalfSize * scale, + endBounds.height() / 2f - mOverlayHalfSize * scale); + atomicTx.setMatrix(mLeash, mTmpTransform, mTmpFloat9) + .setAlpha(mLeash, fraction < 0.5f ? 0 : (fraction - 0.5f) * 2); + } + + + + @Override + public void detach(SurfaceControl.Transaction tx) { + super.detach(tx); + if (mBitmap != null && !mBitmap.isRecycled()) { + mBitmap.recycle(); + } + } + + private void prepareAppIconOverlay(Drawable appIcon) { + final Canvas canvas = new Canvas(); + canvas.setBitmap(mBitmap); + final TypedArray ta = mContext.obtainStyledAttributes(new int[] { + android.R.attr.colorBackground }); + try { + int colorAccent = ta.getColor(0, 0); + canvas.drawRGB( + Color.red(colorAccent), + Color.green(colorAccent), + Color.blue(colorAccent)); + } finally { + ta.recycle(); + } + final Rect appIconBounds = new Rect( + mOverlayHalfSize - mAppIconSizePx / 2, + mOverlayHalfSize - mAppIconSizePx / 2, + mOverlayHalfSize + mAppIconSizePx / 2, + mOverlayHalfSize + mAppIconSizePx / 2); + appIcon.setBounds(appIconBounds); + appIcon.draw(canvas); + mBitmap = mBitmap.copy(Bitmap.Config.HARDWARE, false /* mutable */); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java index 0427294579dc..9a93371621c9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java @@ -456,6 +456,10 @@ public class PipController implements ConfigurationChangeListener, } } + private void setLauncherAppIconSize(int iconSizePx) { + mPipBoundsState.getLauncherState().setAppIconSizePx(iconSizePx); + } + /** * The interface for calls from outside the Shell, within the host process. */ @@ -571,7 +575,10 @@ public class PipController implements ConfigurationChangeListener, } @Override - public void setLauncherAppIconSize(int iconSizePx) {} + public void setLauncherAppIconSize(int iconSizePx) { + executeRemoteCallWithTaskPermission(mController, "setLauncherAppIconSize", + (controller) -> controller.setLauncherAppIconSize(iconSizePx)); + } @Override public void setPipAnimationListener(IPipAnimationListener listener) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java index def76a0d0819..e7fb5f24401a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java @@ -29,9 +29,6 @@ import static android.view.WindowManager.TRANSIT_TO_FRONT; import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP; import static com.android.wm.shell.transition.Transitions.TRANSIT_RESIZE_PIP; -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.ValueAnimator; import android.annotation.NonNull; import android.app.ActivityManager; import android.app.PictureInPictureParams; @@ -361,31 +358,17 @@ public class PipTransition extends PipTransitionController implements animator.setEnterStartState(pipChange, pipActivityChange); animator.onEnterAnimationUpdate(1.0f /* fraction */, startTransaction); startTransaction.apply(); - finishInner(); - return true; - } - private void startOverlayFadeoutAnimation() { - ValueAnimator animator = ValueAnimator.ofFloat(1f, 0f); - animator.setDuration(CONTENT_OVERLAY_FADE_OUT_DELAY_MS); - animator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - super.onAnimationEnd(animation); + if (swipePipToHomeOverlay != null) { + // fadeout the overlay if needed. + startOverlayFadeoutAnimation(swipePipToHomeOverlay, () -> { SurfaceControl.Transaction tx = new SurfaceControl.Transaction(); - tx.remove(mPipTransitionState.getSwipePipToHomeOverlay()); + tx.remove(swipePipToHomeOverlay); tx.apply(); - - // We have fully completed enter-PiP animation after the overlay is gone. - mPipTransitionState.setState(PipTransitionState.ENTERED_PIP); - } - }); - animator.addUpdateListener(animation -> { - float alpha = (float) animation.getAnimatedValue(); - SurfaceControl.Transaction tx = new SurfaceControl.Transaction(); - tx.setAlpha(mPipTransitionState.getSwipePipToHomeOverlay(), alpha).apply(); - }); - animator.start(); + }); + } + finishInner(); + return true; } private boolean startBoundsTypeEnterAnimation(@NonNull TransitionInfo info, @@ -404,15 +387,18 @@ public class PipTransition extends PipTransitionController implements return false; } - Rect endBounds = pipChange.getEndAbsBounds(); - SurfaceControl pipLeash = mPipTransitionState.mPinnedTaskLeash; - Preconditions.checkNotNull(pipLeash, "Leash is null for bounds transition."); + final Rect startBounds = pipChange.getStartAbsBounds(); + final Rect endBounds = pipChange.getEndAbsBounds(); - Rect sourceRectHint = null; - if (pipChange.getTaskInfo() != null - && pipChange.getTaskInfo().pictureInPictureParams != null) { - sourceRectHint = pipChange.getTaskInfo().pictureInPictureParams.getSourceRectHint(); - } + final PictureInPictureParams params = pipChange.getTaskInfo().pictureInPictureParams; + final float aspectRatio = mPipBoundsAlgorithm.getAspectRatioOrDefault(params); + + final Rect sourceRectHint = PipBoundsAlgorithm.getValidSourceHintRect(params, startBounds, + endBounds); + + final SurfaceControl pipLeash = mPipTransitionState.mPinnedTaskLeash; + final Rect adjustedSourceRectHint = sourceRectHint != null ? new Rect(sourceRectHint) + : PipUtils.getEnterPipWithOverlaySrcRectHint(startBounds, aspectRatio); // For opening type transitions, if there is a change of mode TO_FRONT/OPEN, // make sure that change has alpha of 1f, since it's init state might be set to alpha=0f @@ -440,14 +426,36 @@ public class PipTransition extends PipTransitionController implements } PipEnterAnimator animator = new PipEnterAnimator(mContext, pipLeash, - startTransaction, finishTransaction, endBounds, sourceRectHint, delta); + startTransaction, finishTransaction, endBounds, adjustedSourceRectHint, delta); + if (sourceRectHint == null) { + // update the src-rect-hint in params in place, to set up initial animator transform. + params.getSourceRectHint().set(adjustedSourceRectHint); + animator.setAppIconContentOverlay( + mContext, startBounds, endBounds, pipChange.getTaskInfo().topActivityInfo, + mPipBoundsState.getLauncherState().getAppIconSizePx()); + } animator.setAnimationStartCallback(() -> animator.setEnterStartState(pipChange, pipActivityChange)); - animator.setAnimationEndCallback(this::finishInner); + animator.setAnimationEndCallback(() -> { + if (animator.getContentOverlayLeash() != null) { + startOverlayFadeoutAnimation(animator.getContentOverlayLeash(), + animator::clearAppIconOverlay); + } + finishInner(); + }); animator.start(); return true; } + private void startOverlayFadeoutAnimation(@NonNull SurfaceControl overlayLeash, + @NonNull Runnable onAnimationEnd) { + PipAlphaAnimator animator = new PipAlphaAnimator(mContext, overlayLeash, + null /* startTx */, PipAlphaAnimator.FADE_OUT); + animator.setDuration(CONTENT_OVERLAY_FADE_OUT_DELAY_MS); + animator.setAnimationEndCallback(onAnimationEnd); + animator.start(); + } + private void handleBoundsTypeFixedRotation(TransitionInfo.Change pipTaskChange, TransitionInfo.Change pipActivityChange, int endRotation) { final Rect endBounds = pipTaskChange.getEndAbsBounds(); @@ -692,9 +700,7 @@ public class PipTransition extends PipTransitionController implements private void finishInner() { finishTransition(null /* tx */); - if (mPipTransitionState.getSwipePipToHomeOverlay() != null) { - startOverlayFadeoutAnimation(); - } else if (mPipTransitionState.getState() == PipTransitionState.ENTERING_PIP) { + if (mPipTransitionState.getState() == PipTransitionState.ENTERING_PIP) { // If we were entering PiP (i.e. playing the animation) with a valid srcRectHint, // and then we get a signal on client finishing its draw after the transition // has ended, then we have fully entered PiP. |