Android 多窗口框架全解析

本文基于AOSP Android-7.1.1-R9代码进行分析。
Android N的的多窗口框架中,总共包含了三种模式。

  • Split-Screen Mode: 分屏模式。
  • Freeform Mode 自由模式:类似于Windows的窗口模式。
  • Picture In Picture Mode:画中画模式(PIP)

经过一段时间的研究,总结一句话:多窗口框架的核心思想是分栈设置栈边界。本文会从系统源码角度分析分栈以及设置栈边界的步骤和原理,从而解析多窗口三种模式的实现方式。

既然提到了分栈,那我们首先要了解这个栈是什么?在Android系统中,启动一个Activity之后,必定会将此Activity存放于某一个Stack,在Android N中,系统定义了5种Stack ID,系统所有Stack的ID属于这5种里面的一种。不同的Activity可能归属于不同的Stack,但是具有相同的Stack ID。StackID如下图所示:

        /** First static stack ID. */
        public static final int FIRST_STATIC_STACK_ID = 0;

        /** Home activity stack ID. */
        public static final int HOME_STACK_ID = FIRST_STATIC_STACK_ID;

        /** ID of stack where fullscreen activities are normally launched into. */
        public static final int FULLSCREEN_WORKSPACE_STACK_ID = 1;

        /** ID of stack where freeform/resized activities are normally launched into. */
        public static final int FREEFORM_WORKSPACE_STACK_ID = FULLSCREEN_WORKSPACE_STACK_ID + 1;

        /** ID of stack that occupies a dedicated region of the screen. */
        public static final int DOCKED_STACK_ID = FREEFORM_WORKSPACE_STACK_ID + 1;

        /** ID of stack that always on top (always visible) when it exist. */
        public static final int PINNED_STACK_ID = DOCKED_STACK_ID + 1;

正常情况下,Launcher和SystemUI进程里面的Activity所在的Stack的id是HOME_STACK_ID, 普通的Activity所在的Stack的id是FULLSCREEN_WORKSPACE_STACK_ID,自由模式下对应的栈ID是FREEFORM_WORKSPACE_STACK_ID;分屏模式下,上半部分窗口里面的Activity所处的栈ID是DOCKED_STACK_ID;画中画模式中,位于小窗口里面的Activity所在的栈的ID是PINNED_STACK_ID;

栈边界

在多窗口框架中,通过设置Stack的边界(Bounds)来控制里面每个Task的大小,最终Task的大小决定了窗口的大小。栈边界通过Rect(left,top,right,bottom)来表示,存储了四个值,分别表示矩形的4条边离坐标轴的位置,最终显示在屏幕上窗口的大小是根据Stack边界的大小来决定的。

如图1-1所示,为分屏模式下的Activity的状态。整个屏幕被分成了两个Stack,一个DockedStack,一个FullScreenStack。每个Stack里面有多个Task,每个Task里面又有多个Activity。当我们设置了Stack的大小之后,Stack里面的所有的Task的大小以及Task里面所有的Activity的窗口大小都确定了。假设屏幕的大小是1440x2560,整个屏幕的栈边界就是(0,0,1440,2560)。

图1
图1-1

多窗口涉及到几大核心服务,WindowManagerService级相关类、ActivityManagerService和相关类、以及SystemUI里面的核心类,代码主要位于如下:

frameworks/base/services/core/java/com/android/server/wm/WindowManagerService.java
frameworks/base/services/core/java/com/android/server/wm/TaskTapPointerEventListener.java
frameworks/base/services/core/java/com/android/server/wm/TaskGroup.java
frameworks/base/services/core/java/com/android/server/wm/Task.java
frameworks/base/services/core/java/com/android/server/wm/TaskStack.java
frameworks/base/services/core/java/com/android/server/wm/TaskPositioner.java
frameworks/base/services/core/java/com/android/server/am/TaskPersister.java
frameworks/base/services/core/java/com/android/server/am/TaskRecord.java
frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java
frameworks/base/services/core/java/com/android/server/am/ActivityStackSupervisor.java
frameworks/base/services/core/java/com/android/server/am/ActivityStack.java

frameworks/base/core/java/com/android/internal/policy/DividerSnapAlgorithm.java

frameworks/base/packages/SystemUI/src/com/android/systemui/stackdivider/

画中画模式

画中画模式(PIP)是最简单的多窗口模式,进入Android画中画模式的Activity会在当前屏幕上显示一个小的窗口,如图2-1所示。

这里写图片描述

图2-1

进入画中画模式很简单,直接在Activity里面调用enterPictureInPicture方法进入PIP模式。上面说到多窗口模式的核心是分栈和设置栈边界,接下来我们将一步步来分析画中画模式的框架原理,首先给出一张图说明下相关流程。

这里写图片描述

查看大图

本文将根据分栈设置栈边界两个核心来进行相关代码梳理。

PIP模式分栈

Step1-5

PIP模式下分栈核心代码,后面的步骤是设置栈边界的核心代码。
如前面所说,系统有5种Stack ID,PIP模式中的Activity所在的stack id是PINNED_STACK_ID。普通Activity位于id是FULLSCREEN_WORKSPACE_STACK_ID的stack里面。因此画中画模式分栈的核心工作是把activity从id是FULLSCREEN_WORKSPACE_STACK_ID的栈移动到id是PINNED_STACK_ID的stack里面。

本文会贴出部分代码加以分析,首先,Activity直接调用enterPictureInPictureMod进入画中画模式。

@Activity.java

    public void enterPictureInPictureMode() {
        try {
            ActivityManagerNative.getDefault().enterPictureInPictureMode(mToken);
        } catch (RemoteException e) {
        }
    }

紧接着在ActivityManagerService的enterPictureInPictureMode方法中,会获取PIP窗口的默认大小。窗口的默认大小是mDefaultPinnedStackBounds来控制的。如果我们想定制此窗口大小,更改config_defaultPictureInPictureBounds即可。

@ActivityManagerService.java

    public void enterPictureInPictureMode(IBinder token) {
        final long origId = Binder.clearCallingIdentity();
        try {
...

                // Use the default launch bounds for pinned stack if it doesn't exist yet or use the
                // current bounds.
                final ActivityStack pinnedStack = mStackSupervisor.getStack(PINNED_STACK_ID);
                final Rect bounds = (pinnedStack != null)
                        ? pinnedStack.mBounds : mDefaultPinnedStackBounds;

                mStackSupervisor.moveActivityToPinnedStackLocked(
                        r, "enterPictureInPictureMode", bounds);
            }
        } finally {
            Binder.restoreCallingIdentity(origId);
        }
    }

核心代码

  • mStackSupervisor.moveActivityToPinnedStackLocked(r, “enterPictureInPictureMode”, bounds);

多窗口的核心是分stack,以上方法的最后一句话会把当前Activity移动到系统为PIP分配的stack。接下来到moveActivityToPinnedStackLocked里面,默认情况下PinnedStack不存在,系统会创建这个stack,然后会根据当前Activity(正常窗口)所在的task的边界来设置PinnedStack的边界,注意此时还没有用到我们默认为PIP指定的bounds,当前activity的边界就是屏幕的可视区域,最终在WindowManagerService.java里面我们会把当前的task添加到PIP模式所在的Stack里面。

@ActivityStackSupervisor

    void moveActivityToPinnedStackLocked(ActivityRecord r, String reason, Rect bounds) {
        mWindowManager.deferSurfaceLayout();
        try {
            final TaskRecord task = r.task;

            if (r == task.stack.getVisibleBehindActivity()) {
                // An activity can't be pinned and visible behind at the same time. Go ahead and
                // release it from been visible behind before pinning.
                requestVisibleBehindLocked(r, false);
            }

            // Need to make sure the pinned stack exist so we can resize it below...
            final ActivityStack stack = getStack(PINNED_STACK_ID, CREATE_IF_NEEDED, ON_TOP);

            // 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(PINNED_STACK_ID, task.mBounds, null /* tempTaskBounds */,
                    null /* tempTaskInsetBounds */, !PRESERVE_WINDOWS,
                    true /* allowResizeInDockedMode */, !DEFER_RESUME);

            if (task.mActivities.size() == 1) {
                // There is only one activity in the task. So, we can just move the task over to
                // the stack without re-parenting the activity in a different task.
                if (task.getTaskToReturnTo() == HOME_ACTIVITY_TYPE) {
                    // Move the home stack forward if the task we just moved to the pinned stack
                    // was launched from home so home should be visible behind it.
                    moveHomeStackToFront(reason);
                }
                moveTaskToStackLocked(
                        task.taskId, PINNED_STACK_ID, ON_TOP, FORCE_FOCUS, reason, !ANIMATE);
            } else {
                stack.moveActivityToStack(r);
            }
        } finally {
            mWindowManager.continueSurfaceLayout();
        }

        // The task might have already been running and its visibility needs to be synchronized
        // with the visibility of the stack / windows.
        ensureActivitiesVisibleLocked(null, 0, !PRESERVE_WINDOWS);
        resumeFocusedStackTopActivityLocked();

        mWindowManager.animateResizePinnedStack(bounds, -1);
        mService.notifyActivityPinnedLocked();
    }

核心方法

  • moveTaskToStackLocked(task.taskId, PINNED_STACK_ID, ON_TOP, FORCE_FOCUS, reason, !ANIMATE);
  • mWindowManager.animateResizePinnedStack(bounds, -1);

至此分栈的过程就完成了。

PIP模式设置栈边界

接下来我们分析一下设置栈边界的过程。
接着分栈的分析,最后会调用WindowManager的animateResizePinnedStack(bounds, -1)方法,根据当前Stack的大小和指定的PIP窗口的边界,通过动画慢慢更改当前窗口的大小,直到最后显示画中画模式的窗口。
@BoundsAnimationController.java

    public void animateResizePinnedStack(final Rect bounds, final int animationDuration) {
        synchronized (mWindowMap) {
...
            UiThread.getHandler().post(new Runnable() {
                @Override
                public void run() {
                    mBoundsAnimationController.animateBounds(
                            stack, originalBounds, bounds, animationDuration);
                }
            });
        }
    }

mBoundsAnimationController.animateBoundsfromto参数,分别表示在全屏stack id下的栈边界和指定的PIP模式的栈边界。

    void animateBounds(final AnimateBoundsUser target, Rect from, Rect to, int animationDuration) {
...
        final BoundsAnimator animator =
                new BoundsAnimator(target, from, to, moveToFullscreen, replacing);
        mRunningAnimations.put(target, animator);
        animator.setFloatValues(0f, 1f);
        animator.setDuration((animationDuration != -1 ? animationDuration
                : DEFAULT_APP_TRANSITION_DURATION) * DEBUG_ANIMATION_SLOW_DOWN_FACTOR);
        animator.setInterpolator(new LinearInterpolator());
        animator.start();
    }

在动画的执行过程中,不断的去更改当前stack的大小。

        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            // ... 
            if (!mTarget.setPinnedStackSize(mTmpRect, mTmpTaskBounds)) {
            // ...
            }
        }

省略掉中间的一些步骤。直接到ActivityStackSupervisor.java的resizeStackUncheckedLocked。由于我们的Stack将要发生变化,所以会更新当前stack里面的所有task的相关配置。且会通知应用当前的多窗口状态发生了变化,此时会更新Task对应的最小宽度和最小高度等config信息。

@ActivityStackSupervisor.java

    void resizeStackUncheckedLocked(ActivityStack stack, Rect bounds, Rect tempTaskBounds,
            Rect tempTaskInsetBounds) {
        bounds = TaskRecord.validateBounds(bounds);

        if (!stack.updateBoundsAllowed(bounds, tempTaskBounds, tempTaskInsetBounds)) {
            return;
        }

        mTmpBounds.clear();
        mTmpConfigs.clear();
        mTmpInsetBounds.clear();
        final ArrayList<TaskRecord> tasks = stack.getAllTasks();
        final Rect taskBounds = tempTaskBounds != null ? tempTaskBounds : bounds;
        final Rect insetBounds = tempTaskInsetBounds != null ? tempTaskInsetBounds : taskBounds;
        for (int i = tasks.size() - 1; i >= 0; i--) {
            final TaskRecord task = tasks.get(i);
            if (task.isResizeable()) {
                if (stack.mStackId == FREEFORM_WORKSPACE_STACK_ID) {
                    // For freeform stack we don't adjust the size of the tasks to match that
   
  • 12
    点赞
  • 89
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值