Activity.java
/**
* Puts the activity in picture-in-picture mode if possible in the current system state. The
* set parameters in {@param params} will be combined with the parameters from prior calls to
* {@link #setPictureInPictureParams(PictureInPictureParams)}.
*
* The system may disallow entering picture-in-picture in various cases, including when the
* activity is not visible, if the screen is locked or if the user has an activity pinned.
*
* @see android.R.attr#supportsPictureInPicture
* @see PictureInPictureParams
*
* @param params non-null parameters to be combined with previously set parameters when entering
* picture-in-picture.
*
* @return true if the system successfully put this activity into picture-in-picture mode or was
* already in picture-in-picture mode (@see {@link #isInPictureInPictureMode()). If the device
* does not support picture-in-picture, return false.
*/
public boolean enterPictureInPictureMode(@NonNull PictureInPictureParams params) {
try {
if (!deviceSupportsPictureInPictureMode()) {
return false;
}
if (params == null) {
throw new IllegalArgumentException("Expected non-null picture-in-picture params");
}
if (!mCanEnterPictureInPicture) {
throw new IllegalStateException("Activity must be resumed to enter"
+ " picture-in-picture");
}
return ActivityManagerNative.getDefault().enterPictureInPictureMode(mToken, params);//1
} catch (RemoteException e) {
return false;
}
}
注释 1 getDefault 得到 IActivityManager 对象
/**
* Retrieve the system's default/global activity manager.
*
* @deprecated use ActivityManager.getService instead.
*/
static public IActivityManager getDefault() {
return ActivityManager.getService();
}
顺着 IActivityManager.aidl 该方法传递给 ActivityManagerService
boolean enterPictureInPictureMode(in IBinder token, in PictureInPictureParams params);
ActivityManagerService.java
@Override
public boolean enterPictureInPictureMode(IBinder token, final PictureInPictureParams params) {
final long origId = Binder.clearCallingIdentity();
try {
synchronized(this) {
final ActivityRecord r = ensureValidPictureInPictureActivityParamsLocked(
"enterPictureInPictureMode", token, params);
// If the activity is already in picture in picture mode, then just return early
if (isInPictureInPictureMode(r)) {
return true;
}
// Activity supports picture-in-picture, now check that we can enter PiP at this
// point, if it is
if (!r.checkEnterPictureInPictureState("enterPictureInPictureMode",
false /* beforeStopping */)) {
return false;
}
final Runnable enterPipRunnable = () -> { //1
// Only update the saved args from the args that are set
r.pictureInPictureArgs.copyOnlySet(params);
final float aspectRatio = r.pictureInPictureArgs.getAspectRatio();
final List<RemoteAction> actions = r.pictureInPictureArgs.getActions();
// Adjust the source bounds by the insets for the transition down
final Rect sourceBounds = new Rect(r.pictureInPictureArgs.getSourceRectHint());
mStackSupervisor.moveActivityToPinnedStackLocked(r, sourceBounds, aspectRatio,
"enterPictureInPictureMode"); //2
final PinnedActivityStack stack = r.getStack();
stack.setPictureInPictureAspectRatio(aspectRatio);
stack.setPictureInPictureActions(actions);
MetricsLoggerWrapper.logPictureInPictureEnter(mContext, r.appInfo.uid,
r.shortComponentName, r.supportsEnterPipOnTaskSwitch);
logPictureInPictureArgs(params);
};
if (isKeyguardLocked()) {
// If the keyguard is showing or occluded, then try and dismiss it before
// entering picture-in-picture (this will prompt the user to authenticate if the
// device is currently locked).
try {
dismissKeyguard(token, new KeyguardDismissCallback() {
@Override
public void onDismissSucceeded() throws RemoteException {
mHandler.post(enterPipRunnable);
}
}, null /* message */);
} catch (RemoteException e) {
// Local call
}
} else {
// Enter picture in picture immediately otherwise
enterPipRunnable.run(); //3
}
return true;
}
} finally {
Binder.restoreCallingIdentity(origId);
}
}
注释 1 创建了一个 Runnable ,该 Runnable 会在注释 3 被执行,其中核心代码是注释 2
mStackSupervisor.moveActivityToPinnedStackLocked(r, sourceBounds, aspectRatio,
"enterPictureInPictureMode"); //2
ActivityStackSupervisor.java
void moveActivityToPinnedStackLocked(ActivityRecord r, Rect sourceHintBounds, float aspectRatio,
String reason) {
mWindowManager.deferSurfaceLayout();
final ActivityDisplay display = r.getStack().getDisplay();
PinnedActivityStack stack = display.getPinnedStack();
// This will clear the pinned stack by moving an existing task to the full screen stack,
// ensuring only one task is present.
if (stack != null) {
moveTasksToFullscreenStackLocked(stack, !ON_TOP);
}
// Need to make sure the pinned stack exist so we can resize it below...
stack = display.getOrCreateStack(WINDOWING_MODE_PINNED, r.getActivityType(), ON_TOP); // 1
// Calculate the target bounds here before the task is reparented back into pinned windowing
// mode (which will reset the saved bounds)
final Rect destBounds = stack.getDefaultPictureInPictureBounds(aspectRatio);
try {
final TaskRecord task = r.getTask();
// Resize the pinned stack to match the current size of the task the activity we are
// going to be moving is currently contained in. We do this to have the right starting
// animation bounds for the pinned stack to the desired bounds the caller wants.
resizeStackLocked(stack, task.getOverrideBounds(), null /* tempTaskBounds */,
null /* tempTaskInsetBounds */, !PRESERVE_WINDOWS,
true /* allowResizeInDockedMode */, !DEFER_RESUME);
if (task.mActivities.size() == 1) {
// Defer resume until below, and do not schedule PiP changes until we animate below
task.reparent(stack, ON_TOP, REPARENT_MOVE_STACK_TO_FRONT, !ANIMATE, DEFER_RESUME,
false /* schedulePictureInPictureModeChange */, reason); // 2
} else {
// There are multiple activities in the task and moving the top activity should
// reveal/leave the other activities in their original task.
// Currently, we don't support reparenting activities across tasks in two different
// stacks, so instead, just create a new task in the same stack, reparent the
// activity into that task, and then reparent the whole task to the new stack. This
// ensures that all the necessary work to migrate states in the old and new stacks
// is also done.
final TaskRecord newTask = task.getStack().createTaskRecord(
getNextTaskIdForUserLocked(r.userId), r.info, r.intent, null, null, true);
r.reparent(newTask, MAX_VALUE, "moveActivityToStack");
// Defer resume until below, and do not schedule PiP changes until we animate below
newTask.reparent(stack, ON_TOP, REPARENT_MOVE_STACK_TO_FRONT, !ANIMATE,
DEFER_RESUME, false /* schedulePictureInPictureModeChange */, reason);
}
// Reset the state that indicates it can enter PiP while pausing after we've moved it
// to the pinned stack
r.supportsEnterPipOnTaskSwitch = false;
} finally {
mWindowManager.continueSurfaceLayout();
}
stack.animateResizePinnedStack(sourceHintBounds, destBounds, -1 /* animationDuration */,
true /* fromFullscreen */); //3
// Update the visibility of all activities after the they have been reparented to the new
// stack. This MUST run after the animation above is scheduled to ensure that the windows
// drawn signal is scheduled after the bounds animation start call on the bounds animator
// thread.
ensureActivitiesVisibleLocked(null, 0, !PRESERVE_WINDOWS); //5
resumeFocusedStackTopActivityLocked();
mService.mTaskChangeNotificationController.notifyActivityPinned(r);
}
注释 1 创建画中画模式的 stack,注释 2 将 Activity 的 Task 放到新建的画中画 stack 中,注释 3 执行变化动画,注释 4 用来显示该 task
进入画中画模式后,新的 activity 样式定义在 DecorView.java,当该 Window 进入画中画后
PhoneWindow.java
@Override
public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode) {
if (mDecor != null) {
mDecor.updatePictureInPictureOutlineProvider(isInPictureInPictureMode);
}
}
DecorView.java
/**
* Overrides the view outline when the activity enters picture-in-picture to ensure that it has
* an opaque shadow even if the window background is completely transparent. This only applies
* to activities that are currently the task root.
*/
public void updatePictureInPictureOutlineProvider(boolean isInPictureInPictureMode) {
if (mIsInPictureInPictureMode == isInPictureInPictureMode) {
return;
}
if (isInPictureInPictureMode) {
final Window.WindowControllerCallback callback =
mWindow.getWindowControllerCallback();
if (callback != null && callback.isTaskRoot()) {
// Call super implementation directly as we don't want to save the PIP outline
// provider to be restored
super.setOutlineProvider(PIP_OUTLINE_PROVIDER); // 1
}
} else {
// Restore the previous outline provider
if (getOutlineProvider() != mLastOutlineProvider) {
setOutlineProvider(mLastOutlineProvider);
}
}
mIsInPictureInPictureMode = isInPictureInPictureMode;
}
注释 1 给 DecorView 设置了一个外轮廓,外轮廓定义如下
DecorView.java
// This is used to workaround an issue where the PiP shadow can be transparent if the window
// background is transparent
private static final ViewOutlineProvider PIP_OUTLINE_PROVIDER = new ViewOutlineProvider() {
@Override
public void getOutline(View view, Outline outline) {
outline.setRect(0, 0, view.getWidth(), view.getHeight());
outline.setAlpha(1f);
}
};