Android7.0多窗口实现原理

Posted Jason_Lee155

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android7.0多窗口实现原理相关的知识,希望对你有一定的参考价值。

本文基于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)。

多窗口涉及到几大核心服务,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会在当前屏幕上显示一个小的窗口,如图所示。

进入画中画模式很简单,直接在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
                    // of the stack, but we do try to make sure the tasks are still contained
                    // with the bounds of the stack.
                    tempRect2.set(task.mBounds);
                    fitWithinBounds(tempRect2, bounds);
                    task.updateOverrideConfiguration(tempRect2);
                } else {
                    task.updateOverrideConfiguration(taskBounds, insetBounds);
                }
            }

            mTmpConfigs.put(task.taskId, task.mOverrideConfig);
            mTmpBounds.put(task.taskId, task.mBounds);
            if (tempTaskInsetBounds != null) {
                mTmpInsetBounds.put(task.taskId, tempTaskInsetBounds);
            }
        }

        // We might trigger a configuration change. Save the current task bounds for freezing.
        mWindowManager.prepareFreezingTaskBounds(stack.mStackId);
        stack.mFullscreen = mWindowManager.resizeStack(stack.mStackId, bounds, mTmpConfigs,
                mTmpBounds, mTmpInsetBounds);
        stack.setBounds(bounds);
    }
task.updateOverrideConfiguration

mWindowManager.resizeStack(stack.mStackId, bounds, mTmpConfigs,mTmpBounds, mTmpInsetBounds);

接下来我们会进入到设置Stack大小变化的最后一步。设置当前Stack的大小。

@WindowManagerService.java

    public boolean resizeStack(int stackId, Rect bounds,
            SparseArray<Configuration> configs, SparseArray<Rect> taskBounds,
            SparseArray<Rect> taskTempInsetBounds) {
        synchronized (mWindowMap) {
            final TaskStack stack = mStackIdToStack.get(stackId);
            if (stack == null) {
                throw new IllegalArgumentException("resizeStack: stackId " + stackId
                        + " not found.");
            }
            if (stack.setBounds(bounds, configs, taskBounds, taskTempInsetBounds)
                    && stack.isVisibleLocked()) {
                stack.getDisplayContent().layoutNeeded = true;
                mWindowPlacerLocked.performSurfacePlacement();
            }
            return stack.getRawFullscreen();
        }
    }

可以看到当前设置的是TaskStack的边界。

@TaskStack.java

    boolean setBounds(
            Rect stackBounds, SparseArray<Configuration> configs, SparseArray<Rect> taskBounds,
            SparseArray<Rect> taskTempInsetBounds) {
        setBounds(stackBounds);

        // Update bounds of containing tasks.
        for (int taskNdx = mTasks.size() - 1; taskNdx >= 0; --taskNdx) {
            final Task task = mTasks.get(taskNdx);
            Configuration config = configs.get(task.mTaskId);
            if (config != null) {
                Rect bounds = taskBounds.get(task.mTaskId);
                if (task.isTwoFingerScrollMode()) {
                    // This is a non-resizeable task that's docked (or side-by-side to the docked
                    // stack). It might have been scrolled previously, and after the stack resizing,
                    // it might no longer fully cover the stack area.
                    // Save the old bounds and re-apply the scroll. This adjusts the bounds to
                    // fit the new stack bounds.
                    task.resizeLocked(bounds, config, false /* forced */);
                    task.getBounds(mTmpRect);
                    task.scrollLocked(mTmpRect);
                } else {
                    task.resizeLocked(bounds, config, false /* forced */);
                    task.setTempInsetBounds(
                            taskTempInsetBounds != null ? taskTempInsetBounds.get(task.mTaskId)
                                    : null);
                }
            } else {
                Slog.wtf(TAG_WM, "No config for task: " + task + ", is there a mismatch with AM?");
            }
        }
        return true;
    }

在setBounds里面会更新当前TaskStack的bounds,接下来会更新TaskStack里面所有的Task的边界。

在task.resizeLocked里面,会最终设置Task的mBounds变量。也就是我们本文介绍的Task边界。至此,Task的边界bounds已经设置完毕。

显示在窗口顶端

系统所有的Window在屏幕显示的层级是按照Z轴进行排序的,当窗口发生改变的时候,系统会不断的调整Window在整个Window队列里面的层级,除了通过assignLayersLocked对正常窗口的层级进行调整之外。针对多窗口的特殊窗口,WindowLayersController.java还会进行特殊调整。位于adjustSpecialWindows,其中一个回调流程如下:

在adjustSpecialWindows里面,会对分屏模式下DockedWindow以及DockDivider的顺序调整。还有我们正在登陆的窗口mReplaceingWindows的顺序调整。把PIP模式对应的mPinnedWindows放到最后进行调整,这样对应的layer值就最大,那么从Z轴方向来看离屏幕最近。故就会显示到屏幕最顶端。

    private void adjustSpecialWindows() {
        // 省略
        // 将PIP模式的窗口Layer设置到最顶层
        while (!mPinnedWindows.isEmpty()) {
            layer = assignAndIncreaseLayerIfNeeded(mPinnedWindows.remove(), layer);
        }
    }

分屏模式

首先直接给出两张图示。

如图,对于分屏模式而言,当长按预览键的时候,屏幕会分成上下两个窗口,不同的窗口对应不同的Stack,上面的窗口此时对应的是Docked Stack,底部窗口是Home Stack(处于多任务界面)或者FullStack(正常界面)。如图所示,手机被分成上下两块区域,也就是两个Stack,每个stack里面会包含很多的Task,而每个Task里面又包含了多个Activity,最终系统通过控制每个Stack的大小,来控制每个Task的大小,然后控制了Task里面的Activity的窗口的大小,所以最终控制了用户肉眼看到的每个小屏幕窗口大小。

接下来我们就开始分析整个分屏的流程。

创建Divider

如图4-1所示,中间黑色的分割线是DividerView,系统在开机过程中,会将这个DividerView通过WindowManager添加到系统的窗口中,默认影藏。以下是代码逻辑。

@Divider.java

    private void addDivider(Configuration configuration) {
        mView = (DividerView)
                LayoutInflater.from(mContext).inflate(R.layout.docked_stack_divider, null);
        mView.setVisibility(mVisible ? View.VISIBLE : View.INVISIBLE);
        final int size = mContext.getResources().getDimensionPixelSize(
                com.android.internal.R.dimen.docked_stack_divider_thickness);
        final boolean landscape = configuration.orientation == ORIENTATION_LANDSCAPE;
        final int width = landscape ? size : MATCH_PARENT;
        final int height = landscape ? MATCH_PARENT : size;
        mWindowManager.add(mView, width, height);
        mView.injectDependencies(mWindowManager, mDividerState);
    }

中间的白色小点是DividerHandleView.

@docked_stack_divider.xml

<com.android.systemui.stackdivider.DividerView
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_height="match_parent"
        android:layout_width="match_parent">

    <View
        style="@style/DockedDividerBackground"
        android:id="@+id/docked_divider_background"
        android:background="@color/docked_divider_background"/>

    <com.android.systemui.stackdivider.MinimizedDockShadow
        style="@style/DockedDividerMinimizedShadow"
        android:id="@+id/minimized_dock_shadow"
        android:alpha="0"/>">

    <com.android.systemui.stackdivider.DividerHandleView
        style="@style/DockedDividerHandle"
        android:id="@+id/docked_divider_handle"
        android:contentDescription="@string/accessibility_divider"
        android:background="@null"/>

</com.android.systemui.stackdivider.DividerView>

代码里面具体的细节此处不做详细介绍。

初始化SnapTarget

为了确定上下两个stack的大小,设计了SnapTarget的概念,每个SnapTarget相当于一块区域,中间的分割线可以停留在每个区域的底部。在开机过程中,系统会根据屏幕的分辨率来创建不同个数的SnapTarget.

计算SnapTarget位置的代码如下:

@DividerSnapAlgorithm.java

    private void calculateTargets(boolean isHorizontalDivision) {
        mTargets.clear();
        int dividerMax = isHorizontalDivision
                ? mDisplayHeight
                : mDisplayWidth;
        mTargets.add(new SnapTarget(-mDividerSize, -mDividerSize, SnapTarget.FLAG_DISMISS_START,
                0.35f));
        switch (mSnapMode) {
            case SNAP_MODE_16_9:
                addRatio16_9Targets(isHorizontalDivision, dividerMax);
                break;
            case SNAP_FIXED_RATIO:
                addFixedDivisionTargets(isHorizontalDivision, dividerMax);
                break;
            case SNAP_ONLY_1_1:
                addMiddleTarget(isHorizontalDivision);
                break;
        }
        int navBarSize = isHorizontalDivision ? mInsets.bottom : mInsets.right;
        mTargets.add(new SnapTarget(dividerMax - navBarSize, dividerMax,
                SnapTarget.FLAG_DISMISS_END, 0.35f));
    }

简单介绍一下SnapTarget,看构造方法。

        public SnapTarget(int position, int taskPosition, int flag, float distanceMultiplier) {
            this.position = position;
            this.taskPosition = taskPosition;
            this.flag = flag;
            this.distanceMultiplier = distanceMultiplier;
        }
  • position:离屏幕顶部的位置,最终决定了分割线停住的位置。
  • taskPostion: 和postion差不多,主要是用来计算每个Task边界的位置。
  • flag: 控制滑动到某个位置的时候是否退出分屏模式,比如我们将分割线滑动到靠近屏幕底部或者屏幕顶部的时候,会退出分屏。
  • distanceMultiplier:退出分屏模式的距离因子,值越大表示越不容易退出。假设总共高度是1000px,我们需要滑到900px的地方则退出分屏模式。如果distanceMultiplier是1,相当于没有起作用,还是900px退出。如果是0.5,那么1000-900/0.5=200,相当于我们滑动800的地方就会退出了。

SnapTarget的作用主要是用来确认上下小屏的大小以及中间分割线的位置。后面还会继续介绍。

如果屏幕高度足够,上下小屏则可以调整大小。那么会创建5个SnapTargets,相当于是在手机屏幕上下方向确定了5个位置。中间的分割线根据这五个位置来确认自己的位置。举例说明:假设屏幕的分辨率是1440x2560,density是560。那么Android定义的状态栏的高度是84,导航栏的高度是168,默认小屏是按照16:9来分配大小。分割线本身的大小是 34,以上单位都是px。

那么上半部分的高度是(1440-0)X 9/16 = 810。  topPosition是810+84 = 894;  bottomPostion是:2560 - 810 - 34. = 1548.

在Android系统中,默认配置小屏的大小是220dp。

<dimenname="default_minimal_size_resizable_task">220dp</dimen>
 mMinimalSizeResizableTask = res.getDimensionPixelSize(
                com.android.internal.R.dimen.default_minimal_size_resizable_task);

mMinimaSizeResizableTask = 220dp,560/160 * 220 = 770px

如果小屏的高度大于770px,才会添加5个SnapTarget,否则就只添加3个,由于我们上半屏的高度是810px,所以就会添加5个SnapTarget。当添加3个Target的时候,中间的分割线相当于只能停留在中间Target的postion处。也就是不能改变上下小屏幕的大小。

我们列出5个Target的postion和TaskPostion,单位是px,分别如下:

  • mDismissStartTarget : -34px,分割线滑到此处,会退出分屏模式。实际上由于distanceMultiplier 的作用,分割线不需要滑动到这个位置则会退出。
  • mFirstSplitTarget: 894 894
  • mMiddleTarget: 1221 1221 分割线默认位置
  • mLastSplitTarget:1548 1548
  • mDismissEndTarget : 2392 2560

黑色的分割线DividerView默认位于1221处,可以停在894px,1221px和1548px的位置,也就是可以调整上下小屏的大小。当我们滑动中间分割线的时候,分割线会停到离滑动位置最近的postion,比如手指现在滑动的位置是900,那么分割线就会位于894px的地方。

接下来我们分析是如何分栈和设置栈边界

首先附上一张流程图,分为三个颜色,分别表示分栈,设置上半部分的栈边界,设置下半部分的栈边界。

分屏模式分栈

step1. 当我们长按多任务按键之后,系统判断当前是否支持分屏模式,如果手机屏幕过小或者没有配置支持分屏,那么直接返回,否则触发分屏模式。

@PhoneStatusBar.java

    private View.OnLongClickListener mRecentsLongClickListener = new View.OnLongClickListener() {

        @Override
        public boolean onLongClick(View v) {
            if (mRecents == null || !ActivityManager.supportsMultiWindow()
                    || !getComponent(Divider.class).getView().getSnapAlgorithm()
                            .isSplitScreenFeasible()) {
                return false;
            }

            toggleSplitScreenMode(MetricsEvent.ACTION_WINDOW_DOCK_LONGPRESS,
                    MetricsEvent.ACTION_WINDOW_UNDOCK_LONGPRESS);
            return true;
        }
    };

step2-3. 其他代码细节此处不做详表,在RecentsImpl里面,会涉及到我们上面提到的两步核心。分栈和设置栈边界。分栈是通过moveTaskToDockedStack来实现。将当前的Task移动到对应的Docked Stack里面。设置栈边界是通过EventBus.getDefault().send(new DockedTopTaskEvent
(dragMode, initialBounds)来实现。

@RecentsImpl.java

    public void dockTopTask(int topTaskId, int dragMode,
            int stackCreateMode, Rect initialBounds) {
        SystemServicesProxy ssp = Recents.getSystemServices();

        // Make sure we inform DividerView before we actually start the activity so we can change
        // the resize mode already.
        if (ssp.moveTaskToDockedStack(topTaskId, stackCreateMode, initialBounds)) {
            EventBus.getDefault().send(new DockedTopTaskEvent(dragMode, initialBounds));
            showRecents(
                    false /* triggeredFromAltTab */,
                    dragMode == NavigationBarGestureHelper.DRAG_MODE_RECENTS,
                    false /* animate */,
                    true /* launchedWhileDockingTask*/,
                    false /* fromHome */,
                    DividerView.INVALID_RECENTS_GROW_TARGET);
        }
    }

Step4-9 .通过SystemServiceProxy代理,直接调用到ActivityManagerService.java的moveTaskToDockedStack,首先,系统会调用mWindowManager.setDockedStackCreateState,为了方便后面在WindowManager里面计算stack的大小。接下来调用moveTaskToStackLocked将当前的Task移动到Docked Stack里面。

    @Override
    public boolean moveTaskToDockedStack(int taskId, int createMode, boolean toTop, boolean animate,
            Rect initialBounds, boolean moveHomeStackFront) {
        enforceCallingPermission(MANAGE_ACTIVITY_STACKS, "moveTaskToDockedStack()");
        synchronized (this) {
            long ident = Binder.clearCallingIdentity();
            try {
                if (DEBUG_STACK) Slog.d(TAG_STACK, "moveTaskToDockedStack: moving task=" + taskId
                        + " to createMode=" + createMode + " toTop=" + toTop);
                mWindowManager.setDockedStackCreateState(createMode, initialBounds);
                final boolean moved = mStackSupervisor.moveTaskToStackLocked(
                        taskId, DOCKED_STACK_ID, toTop, !FORCE_FOCUS, "moveTaskToDockedStack",
                        animate, DEFER_RESUME);
                if (moved) {
                    if (moveHomeStackFront) {
                        mStackSupervisor.moveHomeStackToFront("moveTaskToDockedStack");
                    }
                    mStackSupervisor.ensureActivitiesVisibleLocked(null, 0, !PRESERVE_WINDOWS);
                }
                return moved;
            } finally {
                Binder.restoreCallingIdentity(ident);
            }
        }
    }
mStackSupervisor.moveTaskToStackLocked

在ActivityStackSupervisor.java里面会调用WindowManager的moveTaskToStack方法,然后通过TaskStack的addTask方法,最终将当前的Task添加进mStack变量里面。在ActivityStackSupervisor.java里面,如红线所示,如果当前的task在移动之前有焦点。那么就会将当前的task移动到栈的最前面,而且会重新更新所有的windows,这样当系统在后面重新请求绘制Window的时候,Window在Z轴上的位置是正确的。

@ActivityStackSupervisor.java

    ActivityStack moveTaskToStackUncheckedLocked(
            TaskRecord task, int stackId, boolean toTop, boolean forceFocus, String reason) {

        // omitted code 
        final ActivityStack stack = getStack(stackId, CREATE_IF_NEEDED, toTop);
        task.mTemporarilyUnresizable = false;
        mWindowManager.moveTaskToStack(task.taskId, stack.mStackId, toTop);
        stack.addTask(task, toTop, reason);

        // If the task had focus before (or we're requested to move focus),
        // move focus to the new stack by moving the stack to the front.
        stack.moveToFrontAndResumeStateIfNeeded(
                r, forceFocus || wasFocused || wasFront, wasResumed, reason);

        return stack;
    }

分栈的整个流程我们就介绍到这里了。具体的细节不妨碍我们了解整个架构,不在详细描述。接下来我们看是如何设置栈边界的。

分屏模式设置栈边界

step11-13

接下来我们看EventBus.getDefault().send(new DockedTopTaskEvent(dragMode, initialBounds)方法。EventBus是SystemUI里面用来发送消息的一个机制,通过反射来实现相关功能。使用方法大致如下:

  • 首先在需要订阅的类里面通过EventBus.getDefault().register(this)注册监听,表示订阅了某一种详细。
  • 然后定制public final void onBusEvent()方法。
  • 最后调用send或者post方法,将消息发布出去,所有订阅了此消息的类都会收到此消息。

在DividerView里面,注册了EventBus事件。

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        EventBus.getDefault().register(this);
        mSurfaceFlingerOffsetMs = calculateAppSurfaceFlingerVsyncOffsetMs();
    }

当我们执行了send之后,由于参数是DockedTopTaskEvent,那么会执行参数是DockedTopTaskEvent的onBusEvent方法。

@DividerView

    public final void onBusEvent(DockedTopTaskEvent event) {
        if (event.dragMode == NavigationBarGestureHelper.DRAG_MODE_NONE) {
            mState.growAfterRecentsDrawn = false;
            mState.animateAfterRecentsDrawn = true;
            startDragging(false /* animate */, false /* touching */);
        }
        updateDockSide();
        int position = DockedDividerUtils.calculatePositionForBounds(event.initialRect,
                mDockSide, mDividerSize);
        mEntranceAnimationRunning = true;

        // Insets might not have been fetched yet, so fetch manually if needed.
        if (mStableInsets.isEmpty()) {
            SystemServicesProxy.getInstance(mContext).getStableInsets(mStableInsets);
            mSnapAlgorithm = null;
            initializeSnapAlgorithm();
        }

        resizeStack(position, mSnapAlgorithm.getMiddleTarget().position,
                mSnapAlgorithm.getMiddleTarget());
    }

接下来根据事件的初始化边界position和middle target的位置来对栈进行resize操作。注意此时并没有完全确认上下屏的大小。

由于RecentsActivity.java也注册了订阅者。当我们调用了RecentsImpl.java里面的showRecents方法的时候,同时也会调用RecentsActivity.java里面的onBusEvent方法。

@RecentsActivity.java

    public final void onBusEvent(final DockedTopTaskEvent event) {
        mRecentsView.getViewTreeObserver().addOnPreDrawListener(mRecentsDrawnEventListener);
        mRecentsView.invalidate();
    }

当前View进行PreDraw的时候,就会调用回调onPreDraw()方法。

    public boolean onPreDraw() {
        mRecentsView.getViewTreeObserver().removeOnPreDrawListener(this);
        // We post to make sure that this information is delivered after this traversals is
        // finished.
        mRecentsView.post(new Runnable() {
            @Override
            public void run() {
                Recents.getSystemServices().endProlongedAnimations();
            }
        });
        return true;
    }

接下来会根据中间分割线的位置,以及最终分割停留的位置(middle snap target的位置),在stopDragging里面,不断的通过动画,最终将当前Task的边界的底部设置成middle snaptarget的postion.然后请求ActivityManagerService.java进行resize的动作。

@DividerView.java

    public final void onBusEvent(RecentsDrawnEvent drawnEvent) {
        if (mState.animateAfterRecentsDrawn) {
            mState.animateAfterRecentsDrawn = false;
            updateDockSide();

            mHandler.post(() -> {
                // Delay switching resizing mode because this might cause jank in recents animation
                // that's longer than this animation.
                stopDragging(getCurrentPosition(), mSnapAlgorithm.getMiddleTarget(),
                        mLongPressEntraceAnimDuration, Interpolators.FAST_OUT_SLOW_IN,
                        200 /* endDelay */);
            });
        }
        if (mState.growAfterRecentsDrawn) {
            mState.growAfterRecentsDrawn = false;
            updateDockSide();
            EventBus.getDefault().send(new RecentsGrowingEvent());
            stopDragging(getCurrentPosition(), mSnapAlgorithm.getMiddleTarget(), 336,
                    Interpolators.FAST_OUT_SLOW_IN);
        }
    }

step 15.

ActivityManagerService.java里面会直接请求ActivityStackSupervisor.java 重新设置上下屏的边界。如下代码所示,1表示设置DockedStack的bounds,2表示获取下屏的Bounds,3表示根据第2步获取的bounds设置当前stack的bounds。

@ActivityStackSupervisor.java

void resizeDockedStackLocked(Rect dockedBounds, Rect tempDockedTaskBounds,
            Rect tempDockedTaskInsetBounds, Rect tempOtherTaskBounds, Rect tempOtherTaskInsetBounds,
            boolean preserveWindows, boolean deferResume) {

        //  1...
            resizeStackUncheckedLocked(stack, dockedBounds, tempDockedTaskBounds,
                    tempDockedTaskInsetBounds);

            // 2 ...
                mWindowManager.getStackDockedModeBounds(
                        HOME_STACK_ID, tempRect, true /* ignoreVisibility */);
                for (int i = FIRST_STATIC_STACK_ID; i <= LAST_STATIC_STACK_ID; i++) {
                    if (StackId.isResizeableByDockedStack(i) && getStack(i) != null) {
                    // 3
                        resizeStackLocked(i, tempRect, tempOtherTaskBounds,
                                tempOtherTaskInsetBounds, preserveWindows,
                                true /* allowResizeInDockedMode */, deferResume);
                    }
                }
            }

            //...
    }

在ActivityStackSupervisor.java里面,由于上下小屏的最小宽度以及屏幕高度和宽度等发生了变化。故会对当前的Task的config进行重新配置.

    void resizeStackUncheckedLocked(ActivityStack stack, Rect bounds, Rect tempTaskBounds,
            Rect tempTaskInsetBounds) {
            //...
        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
                    // of the stack, but we do try to make sure the tasks are still contained
                    // with the bounds of the stack.
                    tempRect2.set(task.mBounds);
                    fitWithinBounds(tempRect2, bounds);
                    task.updateOverrideConfiguration(tempRect2);
                } else {
                    task.updateOverrideConfiguration(taskBounds, insetBounds);
                }
            }

//...
        stack.mFullscreen = mWindowManager.resizeStack(stack.mStackId, bounds, mTmpConfigs,
                mTmpBounds, mTmpInsetBounds);
        stack.setBounds(bounds);
    }
task.updateOverrideConfiguration

mWindowManager.resizeStack

step16-22.

在WindowManagerService.java里面设置当前Stack的bounds。接下来的步骤和画中画模式里面介绍的一样,故不再解释。

@WindowManagerService.java

    public void resizeTask(int taskId, Rect bounds, Configuration configuration,
            boolean relayout, boolean forced) {
        synchronized (mWindowMap) {
            Task task = mTaskIdToTask.get(taskId);
            if (task == null) {
                throw new IllegalArgumentException("resizeTask: taskId " + taskId
                        + " not found.");
            }

            if (task.resizeLocked(bounds, configuration, forced) && relayout) {
                task.getDisplayContent().layoutNeeded = true;
                mWindowPlacerLocked.performSurfacePlacement();
            }
        }
    }

至此,PIP模式的分栈和设置栈边界就分析完了。接下来我们分析自由模式。

自由模式

自由模式默认情况下是关闭的。如果需要打开,以下两种方式选择其一即可。

  • adb shell settings put globalenable_freeform_supporttrue
  • 给系统添加feature:android.software.freeform_window_management

如果打开了Freeform模式,当我们按了多任务按键之后,在每个任务View的标题栏上会多出一个方框,如图所示,当我们点击了方框之后,会进入FreeForm模式。如图,我们可以看到Settings窗口的顶部多出了一部分,并且右边还有两个操作按钮,表示放大当前窗口,相当于是“”全屏”,关掉当前的窗口。

SystemUI和ActivityManagerService里面有一个值控制是否支持freeform格式。

@ActivityManagerService.java

        // ...
        final boolean freeformWindowManagement =
                mContext.getPackageManager().hasSystemFeature(FEATURE_FREEFORM_WINDOW_MANAGEMENT)
                        || Settings.Global.getInt(
                                resolver, DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT, 0) != 0;
        final boolean supportsPictureInPicture =
                mContext.getPackageManager().hasSystemFeature(FEATURE_PICTURE_IN_PICTURE);
                // ...
                }

@SystemServiceProxy.java

private SystemServicesProxy(Context context) {
        // ...
        mHasFreeformWorkspaceSupport =
                mPm.hasSystemFeature(PackageManager.FEATURE_FREEFORM_WINDOW_MANAGEMENT) ||
                        Settings.Global.getInt(context.getContentResolver(),
                                DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT, 0) != 0;

自由模式UI

Freeform模式对比PIP模式和分屏模式,除了窗口大小有区别之外,顶部还多出了几个可控图标。通过Android Layout Inspector查看,和正常的View对比,DecoreView下面多了一个DecorCaptionView,我们看到的按钮就是Maximize和Close。

DecorCaptionView的创建过程和正常setContentView类似,当我们点击顶部方框进入freeform模式,会创建当前界面的Activity,而且会创建DecorCaptionView。

在创建的过程中,我们会判断当前的Stack id是不是freeform_stack_id。如果不是,那么就不创建顶部标题栏,具体创建过程此处不再描述。

@DecorView.java

    private DecorCaptionView createDecorCaptionView(LayoutInflater inflater) {
        // ...
        if (!mWindow.isFloating() && isApplication && StackId.hasWindowDecor(mStackId)) {
            // Dependent on the brightness of the used title we either use the
            // dark or the light button frame.
            if (decorCaptionView == null) {
                decorCaptionView = inflateDecorCaptionView(inflater);
            }
            decorCaptionView.setPhoneWindow(mWindow, true /*showDecor*/);
        } else {
            decorCaptionView = null;
        }

        // Tell the decor if it has a visible caption.
        enableCaption(decorCaptionView != null);
        return decorCaptionView;
    }

我们还是按照分栈和设置栈边界来分析自由模式。

自由模式分栈

当我们在Recent界面点击了进入freedom模式的按钮之后,当前界面会进入自由模式。首先贴出时序图。

step1-3

可以看到,当我们点击按钮之后,还是基于EventBus机制,发送LauncherTaskEvent。Freeform模式下,Stack的初始化大小的bounds,可以在TaskViewHeader.java里面进行配置。代码如下:

@TaskViewHeader.java

    @Override
    public void onClick(View v) {
        // ...
        } else if (v == mMoveTaskButton) {
            TaskView tv = Utilities.findParent(this, TaskView.class);
            Rect bounds = mMoveTaskTargetStackId == FREEFORM_WORKSPACE_STACK_ID
                    ? new Rect(mTaskViewRect)
                    : new Rect();
            EventBus.getDefault().send(new LaunchTaskEvent(tv, mTask, bounds,
                    mMoveTaskTargetStackId, false));
        } 
        // ...
    }

基于EventBus机制,接下来会调用RecentsView的onBusEvent方法。 然后调用launchTaskFromRecents开始启动Task。在freeForm模式中,有一个很重要的特性就是ActivityOptions,系统根据ActivityOptions来控制启动的Task的栈边界的大小。

@RecentsTransitionHelper.java

    public void launchTaskFromRecents(final TaskStack stack, @Nullable final Task task,
            final TaskStackView stackView, final TaskView taskView,
            final boolean screenPinningRequested, final Rect bounds, final int destinationStack) {
        final ActivityOptions opts = ActivityOptions.makeBasic();
        if (bounds != null) {
            opts.setLaunchBounds(bounds.isEmpty() ? null : bounds);
        }
        // ... 
}

step4-10

省略掉其他的细节,直接进入ActivityManagerService的startActivityFromRecents方法。会获取在前面设置的stack id,在step 5中,如果当前Task的stack id是docked_stack_id,才会设置LauncherStackID,正常情况下是不会进行设置的,也就是说从DockedStack进入Freeform才会设置bounds。

@AMS.java

 final int startActivityFromRecentsInner(int taskId, Bundle bOptions) {
        // ... 
        final ActivityOptions activityOptions = (bOptions != null)
                ? new ActivityOptions(bOptions) : null;
        final int launchStackId = (activityOptions != null)
                ? activityOptions.getLaunchStackId() : INVALID_STACK_ID;
        // ...
}

所以在接下来的判断中launcherStackId等于INVALID_STACK_ID,也就不会走moveTask的操作。如下:

        if (launchStackId != INVALID_STACK_ID) {
            if (task.stack.mStackId != launchStackId) {
                moveTaskToStackLocked(
                        taskId, launchStackId, ON_TOP, FORCE_FOCUS, "startActivityFromRecents",
                        ANIMATE);
            }
        }

那move操作时在哪儿进行的呢?我们接着往后看,会有个mService.moveTaskToFrontLocked方法。

   // If the user must confirm credentials (e.g. when first launching a work app and the
        // Work Challenge is present) let startActivityInPackage handle the intercepting.
        if (!mService.mUserController.shouldConfirmCredentials(task.userId)
                && task.getRootActivity() != null) {
            mService.mActivityStarter.sendPowerHintForLaunchStartIfNeeded(true /* forceSend */);
            mActivityMetricsLogger.notifyActivityLaunching();
            mService.moveTaskToFrontLocked(task.taskId, 0, bOptions);
            // ...

            mService.mActivityStarter.postStartActivityUncheckedProcessing(task.getTopActivity(),
                    ActivityManager.START_TASK_TO_FRONT,
                    sourceRecord != null ? sourceRecord.task.stack.mStackId : INVALID_STACK_ID,
                    sourceRecord, task.stack);
            return ActivityManager.START_TASK_TO_FRONT;
        }
mService.moveTaskToFrontLocked

直接进入到ActivityStackSupervisor里面的moveTaskToFrontLocked方法,如果当前的Task支持多窗口、options不会null而且当前是PIP或者自由模式,会更新当前Task的bounds。由于当前的stackId是不合法,那么就会重新后去获取stackID。

@ActivityStackSupervisor.java

void findTaskToMoveToFrontLocked(TaskRecord task, int flags, ActivityOptions options,
            String reason, boolean forceNonResizeable) {
       //...

        if (task.isResizeable() && options != null) {
            int stackId = options.getLaunchStackId();
            //1. 判断是否使用ActivityOptions
            if (canUseActivityOptionsLaunchBounds(options, stackId)) {
                final Rect bounds = TaskRecord.validateBounds(options.getLaunchBounds());
                task.updateOverrideConfiguration(bounds);
                if (stackId == INVALID_STACK_ID) {
                    stackId = task.getLaunchStackId();
                }
                if (stackId != task.stack.mStackId) {
                    // 2. 移动
                    final ActivityStack stack = moveTaskToStackUncheckedLocked(
                            task, stackId, ON_TOP, !FORCE_FOCUS, reason);
                    stackId = stack.mStackId;
                    // moveTaskToStackUncheckedLocked() should already placed the task on top,
                    // still need moveTaskToFrontLocked() below for any transition settings.
                }
                if (StackId.resizeStackWithLaunchBounds(stackId)) {
                    //3. resize栈
                    resizeStackLocked(stackId, bounds,
                            null /* tempTaskBounds */, null /* tempTaskInsetBounds */,
                            !PRESERVE_WINDOWS, true /* allowResizeInDockedMode */, !DEFER_RESUME);
                } else {
                    // WM resizeTask must be done after the task is moved to the correct stack,
                    // because Task's setBounds() also updates dim layer's bounds, but that has
                    // dependency on the stack.
                    mWindowManager.resizeTask(task.taskId, task.mBounds, task.mOverrideConfig,
                            false /* relayout */, false /* forced */);
                }
            }
        }

       //...
    }

根据getLaunchStackID获取当前的STACK_ID,在LaunchTaskFromRecentss里面我们设置了mBounds,所以最终返回FREEFORM_WORKSPACE_STACK_ID。

@ActivityRecord.java

    int getLaunchStackId() {
        if (!isApplicationTask()) {
            return HOME_STACK_ID;
        }
        if (mBounds != null) {
            return FREEFORM_WORKSPACE_STACK_ID;
        }
        return FULLSCREEN_WORKSPACE_STACK_ID;
    }

后面调用moveTaskToStackUncheckedLocked移动Stack,调用resizeTask设置栈边界大小,步骤和PIP模式的类似,不在描述。

自由模式设置栈边界

在ActivityStackSupervisor.java里面进行相应的判断,如果stack支持分屏,且ActiviyOptions不为null,那么就会根据我们的stackid和bounds对当前的Stack进行resize的动作。rezise逻辑和PIP一致,此处不再描述。

缩放窗口

自由模式里面,窗口支持放大缩小以及移动位置。原理是不断的更改Task的边界(用Rect表示),然后根据Task的边界来重新缩放Task,从而达到窗口缩放和拖动的作用。对于拖动来说,边界的宽和高保持不变,变的是坐标的位置。缩放来说,坐标改变的同时,边界宽和高也发生了变化。

对于每一个窗口来说,如果我们的触摸事件要能够正常响应,我们必须注册全双工的输入管道。一个用来分发事件给对应的窗口,一个用来接受事件并进行处理。

首先我们分析缩放窗口的原理,在Android系统启动WMS之后,会在InputManagerService的monitorInput方法创建一个可以用来接受所有输入事件的输入管道。先上一张流程图。

客户端的输入管道会接受服务端发送过来的输入事件,系统所有的事件都会先分发到PointerEventDispatcher.java的onInputEvent方法。由于TaskTapPointerEventListener加入了监听,最终会执行TaskTapPointerEventListener的onPointerEvent方法。

@PointerEventDispatcher.java

    @Override
    public void onInputEvent(InputEvent event) {
        try {
            if (event instanceof MotionEvent
                    && (event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) {
                final MotionEvent motionEvent = (MotionEvent)event;
                PointerEventListener[] listeners;
                synchronized (mListeners) {
                    if (mListenersArray == null) {
                        mListenersArray = new PointerEventListener[mListeners.size()];
                        mListeners.toArray(mListenersArray);
                    }
                    listeners = mListenersArray;
                }
                for (int i = 0; i < listeners.length; ++i) {
                    listeners[i].onPointerEvent(motionEvent);
                }
            }
        } finally {
            finishInputEvent(event, false);
        }
    }

接下来

@TaskTapPointerEventListener.java

    @Override
    public void onPointerEvent(MotionEvent motionEvent) {
        doGestureDetection(motionEvent);

        final int action = motionEvent.getAction();
        switch (action & MotionEvent.ACTION_MASK) {
            case MotionEvent.ACTION_DOWN: {
                final int x = (int) motionEvent.getX();
                final int y = (int) motionEvent.getY();

                synchronized (this) {
                    if (!mTouchExcludeRegion.contains(x, y)) {
                        mService.mH.obtainMessage(H.TAP_OUTSIDE_TASK,
                                x, y, mDisplayContent).sendToTarget();
                    }
                }
                break;
            }
            // ...
    }

mTouchExcludeRegion: 表示当前获取焦点的栈的区域。

根据native层输入的event里面的x和y坐标可以判断我们当前点击的位置,如果点击在栈的外面,那么就会发送TAP_OUTSICE_TASK的消息。 然后会执行findTaskForControlPoint,请注意此时会判断我们点击的位置是不是在当前窗口周围RESIZE_HANDLE_WIDTH_IN_DP=30dp以内,如果是才会进行缩放操作。

@WindowManagerService.java

    private void handleTapOutsideTask(DisplayContent displayContent, int x, int y) {
        int taskId = -1;
        synchronized (mWindowMap) {
            final Task task = displayContent.findTaskForControlPoint(x, y);
            if (task != null) {
                if (!startPositioningLocked(
                        task.getTopVisibleAppMainWindow(), true /*resize*/, x, y)) {
                    return;
                }
        //...
    }
 Task findTaskForControlPoint(int x, int y) {
        final int delta = mService.dipToPixel(RESIZE_HANDLE_WIDTH_IN_DP, mDisplayMetrics);
        for (int stackNdx = mStacks.size() - 1; stackNdx >= 0; --stackNdx) {
            TaskStack stack = mStacks.get(stackNdx);
            if (!StackId.isTaskResizeAllowed(stack.mStackId)) {
                break;
            }
            final ArrayList<Task> tasks = stack.getTasks();
            for (int taskNdx = tasks.size() - 1; taskNdx >= 0; --taskNdx) {
                final Task task = tasks.get(taskNdx);
                if (task.isFullscreen()) {
                    return null;
                }

                // We need to use the task's dim bounds (which is derived from the visible
                // bounds of its apps windows) for any touch-related tests. Can't use
                // the task's original bounds because it might be adjusted to fit the
                // content frame. One example is when the task is put to top-left quadrant,
                // the actual visible area would not start at (0,0) after it's adjusted
                // for the status bar.
                task.getDimBounds(mTmpRect);
                //判断我们账号点击在栈边缘30dp的区域
                mTmpRect.inset(-delta, -delta);
                if (mTmpRect.contains(x, y)) {
                    mTmpRect.inset(delta, delta);
                    if (!mTmpRect.contains(x, y)) {
                        return task;
                    }
                    // User touched inside the task. No need to look further,
                    // focus transfer will be handled in ACTION_UP.
                    return null;
                }
            }
        }

接下来会进入到WindowManagerService的startPositioningLocked方法,有如下两个动作。

  • 注册新的专门用于处于移动或者缩放的全双工输入管道,用来处理在当前Task边界周围30px以内的触摸事件。
  • 告诉TaskPointer,当前的缩放模式以及首次点击的坐标位置。

@WindowManagerService.java

以上是关于Android7.0多窗口实现原理的主要内容,如果未能解决你的问题,请参考以下文章

WmS详解之如何理解Window和窗口的关系?基于Android7.0源码

Android 7.0 通过FileProvider实现应用间文件共享

Androd7.0-新特性

Android7.0 Ninja编译原理

Nougat多窗口支持

执行代码时有时不显示对话框片段