Android 9 画中画实现流程

本文详细介绍了Android应用如何通过调用API进入画中画模式的过程,包括检查设备支持情况、设置参数、移动任务到画中画堆栈及执行动画等关键步骤。

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);
    }
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值