Implement app icon overlay when entering PiP

This is done by
- draw a bitmap with the theme background and app icon at its center
- reparent the hardware buffer of that bitmap to the task leash
- transform the bitmap leash to be unscaled and centered during the
  animation

Video: http://recall/-/aaaaaabFQoRHlzixHdtY/cyuEoL0G1HOS8hjfRmlyPt
Bug: 265998256
Test: turn on the flag and enter PiP, see video
Change-Id: Ia7354d83e9b5701f1358b44b0db7bb32d48ccd2a
This commit is contained in:
Hongwei Wang
2023-01-30 09:53:22 -08:00
parent 3a07ba9745
commit 39b030394f
4 changed files with 165 additions and 19 deletions

View File

@@ -28,6 +28,7 @@ import android.annotation.IntDef;
import android.annotation.NonNull;
import android.app.TaskInfo;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.graphics.Rect;
import android.view.Surface;
import android.view.SurfaceControl;
@@ -361,22 +362,26 @@ public class PipAnimationController {
}
void setColorContentOverlay(Context context) {
final SurfaceControl.Transaction tx =
mSurfaceControlTransactionFactory.getTransaction();
if (mContentOverlay != null) {
mContentOverlay.detach(tx);
}
mContentOverlay = new PipContentOverlay.PipColorOverlay(context);
mContentOverlay.attach(tx, mLeash);
reattachContentOverlay(new PipContentOverlay.PipColorOverlay(context));
}
void setSnapshotContentOverlay(TaskSnapshot snapshot, Rect sourceRectHint) {
reattachContentOverlay(
new PipContentOverlay.PipSnapshotOverlay(snapshot, sourceRectHint));
}
void setAppIconContentOverlay(Context context, Rect bounds, ActivityInfo activityInfo) {
reattachContentOverlay(
new PipContentOverlay.PipAppIconOverlay(context, bounds, activityInfo));
}
private void reattachContentOverlay(PipContentOverlay overlay) {
final SurfaceControl.Transaction tx =
mSurfaceControlTransactionFactory.getTransaction();
if (mContentOverlay != null) {
mContentOverlay.detach(tx);
}
mContentOverlay = new PipContentOverlay.PipSnapshotOverlay(snapshot, sourceRectHint);
mContentOverlay = overlay;
mContentOverlay.attach(tx, mLeash);
}
@@ -570,8 +575,9 @@ public class PipAnimationController {
final Rect base = getBaseValue();
final Rect start = getStartValue();
final Rect end = getEndValue();
Rect bounds = mRectEvaluator.evaluate(fraction, start, end);
if (mContentOverlay != null) {
mContentOverlay.onAnimationUpdate(tx, fraction);
mContentOverlay.onAnimationUpdate(tx, bounds, fraction);
}
if (rotatedEndRect != null) {
// Animate the bounds in a different orientation. It only happens when
@@ -579,7 +585,6 @@ public class PipAnimationController {
applyRotation(tx, leash, fraction, start, end);
return;
}
Rect bounds = mRectEvaluator.evaluate(fraction, start, end);
float angle = (1.0f - fraction) * startingAngle;
setCurrentValue(bounds);
if (inScaleTransition() || sourceHintRect == null) {

View File

@@ -16,11 +16,21 @@
package com.android.wm.shell.pip;
import static android.util.TypedValue.COMPLEX_UNIT_DIP;
import android.annotation.Nullable;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.res.Resources;
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 android.view.SurfaceSession;
import android.window.TaskSnapshot;
@@ -51,9 +61,11 @@ public abstract class PipContentOverlay {
* Animates the internal {@link #mLeash} by a given fraction.
* @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 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, float fraction);
public abstract void onAnimationUpdate(SurfaceControl.Transaction atomicTx,
Rect currentBounds, float fraction);
/**
* Callback when reaches the end of animation on the internal {@link #mLeash}.
@@ -66,13 +78,15 @@ public abstract class PipContentOverlay {
/** A {@link PipContentOverlay} uses solid color. */
public static final class PipColorOverlay extends PipContentOverlay {
private static final String TAG = PipColorOverlay.class.getSimpleName();
private final Context mContext;
public PipColorOverlay(Context context) {
mContext = context;
mLeash = new SurfaceControl.Builder(new SurfaceSession())
.setCallsite("PipAnimation")
.setName(PipColorOverlay.class.getSimpleName())
.setCallsite(TAG)
.setName(TAG)
.setColorLayer()
.build();
}
@@ -88,7 +102,8 @@ public abstract class PipContentOverlay {
}
@Override
public void onAnimationUpdate(SurfaceControl.Transaction atomicTx, float fraction) {
public void onAnimationUpdate(SurfaceControl.Transaction atomicTx,
Rect currentBounds, float fraction) {
atomicTx.setAlpha(mLeash, fraction < 0.5f ? 0 : (fraction - 0.5f) * 2);
}
@@ -114,6 +129,8 @@ public abstract class PipContentOverlay {
/** A {@link PipContentOverlay} uses {@link TaskSnapshot}. */
public static final class PipSnapshotOverlay extends PipContentOverlay {
private static final String TAG = PipSnapshotOverlay.class.getSimpleName();
private final TaskSnapshot mSnapshot;
private final Rect mSourceRectHint;
@@ -121,8 +138,8 @@ public abstract class PipContentOverlay {
mSnapshot = snapshot;
mSourceRectHint = new Rect(sourceRectHint);
mLeash = new SurfaceControl.Builder(new SurfaceSession())
.setCallsite("PipAnimation")
.setName(PipSnapshotOverlay.class.getSimpleName())
.setCallsite(TAG)
.setName(TAG)
.build();
}
@@ -143,7 +160,8 @@ public abstract class PipContentOverlay {
}
@Override
public void onAnimationUpdate(SurfaceControl.Transaction atomicTx, float fraction) {
public void onAnimationUpdate(SurfaceControl.Transaction atomicTx,
Rect currentBounds, float fraction) {
// Do nothing. Keep the snapshot till animation ends.
}
@@ -152,4 +170,113 @@ public abstract class PipContentOverlay {
atomicTx.remove(mLeash);
}
}
/** A {@link PipContentOverlay} shows app icon on solid color background. */
public static final class PipAppIconOverlay extends PipContentOverlay {
private static final String TAG = PipAppIconOverlay.class.getSimpleName();
private static final int APP_ICON_SIZE_DP = 48;
private final Context mContext;
private final int mAppIconSizePx;
private final Rect mAppBounds;
private final Matrix mTmpTransform = new Matrix();
private final float[] mTmpFloat9 = new float[9];
private Bitmap mBitmap;
public PipAppIconOverlay(Context context, Rect appBounds, ActivityInfo activityInfo) {
mContext = context;
mAppIconSizePx = (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP, APP_ICON_SIZE_DP,
context.getResources().getDisplayMetrics());
mAppBounds = new Rect(appBounds);
mBitmap = Bitmap.createBitmap(appBounds.width(), appBounds.height(),
Bitmap.Config.ARGB_8888);
prepareAppIconOverlay(activityInfo);
mLeash = new SurfaceControl.Builder(new SurfaceSession())
.setCallsite(TAG)
.setName(TAG)
.build();
}
@Override
public void attach(SurfaceControl.Transaction tx, SurfaceControl parentLeash) {
tx.show(mLeash);
tx.setLayer(mLeash, Integer.MAX_VALUE);
tx.setBuffer(mLeash, mBitmap.getHardwareBuffer());
tx.reparent(mLeash, parentLeash);
tx.apply();
}
@Override
public void onAnimationUpdate(SurfaceControl.Transaction atomicTx,
Rect currentBounds, float fraction) {
mTmpTransform.reset();
// Scale back the bitmap with the pivot point at center.
mTmpTransform.postScale(
(float) mAppBounds.width() / currentBounds.width(),
(float) mAppBounds.height() / currentBounds.height(),
mAppBounds.centerX(),
mAppBounds.centerY());
atomicTx.setMatrix(mLeash, mTmpTransform, mTmpFloat9)
.setAlpha(mLeash, fraction < 0.5f ? 0 : (fraction - 0.5f) * 2);
}
@Override
public void onAnimationEnd(SurfaceControl.Transaction atomicTx, Rect destinationBounds) {
atomicTx.remove(mLeash);
}
@Override
public void detach(SurfaceControl.Transaction tx) {
super.detach(tx);
if (mBitmap != null && !mBitmap.isRecycled()) {
mBitmap.recycle();
}
}
private void prepareAppIconOverlay(ActivityInfo activityInfo) {
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 Drawable appIcon = loadActivityInfoIcon(activityInfo,
mContext.getResources().getConfiguration().densityDpi);
final Rect appIconBounds = new Rect(
mAppBounds.centerX() - mAppIconSizePx / 2,
mAppBounds.centerY() - mAppIconSizePx / 2,
mAppBounds.centerX() + mAppIconSizePx / 2,
mAppBounds.centerY() + mAppIconSizePx / 2);
appIcon.setBounds(appIconBounds);
appIcon.draw(canvas);
mBitmap = mBitmap.copy(Bitmap.Config.HARDWARE, false /* mutable */);
}
// Copied from com.android.launcher3.icons.IconProvider#loadActivityInfoIcon
private Drawable loadActivityInfoIcon(ActivityInfo ai, int density) {
final int iconRes = ai.getIconResource();
Drawable icon = null;
// Get the preferred density icon from the app's resources
if (density != 0 && iconRes != 0) {
try {
final Resources resources = mContext.getPackageManager()
.getResourcesForApplication(ai.applicationInfo);
icon = resources.getDrawableForDensity(iconRes, density);
} catch (PackageManager.NameNotFoundException | Resources.NotFoundException exc) { }
}
// Get the default density icon
if (icon == null) {
icon = ai.loadIcon(mContext.getPackageManager());
}
return icon;
}
}
}

View File

@@ -62,6 +62,7 @@ import android.content.res.Configuration;
import android.graphics.Rect;
import android.os.RemoteException;
import android.os.SystemClock;
import android.os.SystemProperties;
import android.util.Log;
import android.view.Choreographer;
import android.view.Display;
@@ -1568,7 +1569,13 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener,
// Similar to auto-enter-pip transition, we use content overlay when there is no
// source rect hint to enter PiP use bounds animation.
if (sourceHintRect == null) {
animator.setColorContentOverlay(mContext);
if (SystemProperties.getBoolean(
"persist.wm.debug.enable_pip_app_icon_overlay", false)) {
animator.setAppIconContentOverlay(
mContext, currentBounds, mTaskInfo.topActivityInfo);
} else {
animator.setColorContentOverlay(mContext);
}
} else {
final TaskSnapshot snapshot = PipUtils.getTaskSnapshot(
mTaskInfo.launchIntoPipHostTaskId, false /* isLowResolution */);

View File

@@ -50,6 +50,7 @@ import android.graphics.Matrix;
import android.graphics.Point;
import android.graphics.Rect;
import android.os.IBinder;
import android.os.SystemProperties;
import android.view.Surface;
import android.view.SurfaceControl;
import android.view.WindowManager;
@@ -792,7 +793,13 @@ public class PipTransition extends PipTransitionController {
if (sourceHintRect == null) {
// We use content overlay when there is no source rect hint to enter PiP use bounds
// animation.
animator.setColorContentOverlay(mContext);
if (SystemProperties.getBoolean(
"persist.wm.debug.enable_pip_app_icon_overlay", false)) {
animator.setAppIconContentOverlay(
mContext, currentBounds, taskInfo.topActivityInfo);
} else {
animator.setColorContentOverlay(mContext);
}
}
} else if (mOneShotAnimationType == ANIM_TYPE_ALPHA) {
animator = mPipAnimationController.getAnimator(taskInfo, leash, destinationBounds,