Android 多窗口框架全解析

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;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

正常情況下,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) {
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

緊接着在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);
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

核心代碼

  • 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();
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46

核心方法

  • 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);
                }
            });
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

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();
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

在動畫的執行過程中,不斷的去更改當前stack的大小。

        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            // ... 
            if (!mTarget.setPinnedStackSize(mTmpRect, mTmpTaskBounds)) {
            // ...
            }
        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

省略掉中間的一些步驟。直接到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);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 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();
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

可以看到當前設置的是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;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

在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);
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

分屏模式

首先直接給出兩張圖示。

這裏寫圖片描述 這裏寫描述

圖4-1 圖4-2

如圖4-1,對於分屏模式而言,當長按預覽鍵的時候,屏幕會分成上下兩個窗口,不同的窗口對應不同的Stack,上面的窗口此時對應的是Docked Stack,底部窗口是Home Stack(處於多任務界面)或者FullStack(正常界面)。如圖4-2所示,手機被分成上下兩塊區域,也就是兩個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);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

中間的白色小點是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>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

代碼裏面具體的細節此處不做詳細介紹。

初始化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));
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

簡單介紹一下SnapTarget,看構造方法。

        public SnapTarget(int position, int taskPosition, int flag, float distanceMultiplier) {
            this.position = position;
            this.taskPosition = taskPosition;
            this.flag = flag;
            this.distanceMultiplier = distanceMultiplier;
        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 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>
  • 1
 mMinimalSizeResizableTask = res.getDimensionPixelSize(
                com.android.internal.R.dimen.default_minimal_size_resizable_task);
  • 1
  • 2

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;
        }
    };
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

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);
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

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);
            }
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 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;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

分棧的整個流程我們就介紹到這裏了。具體的細節不妨礙我們瞭解整個架構,不在詳細描述。接下來我們看是如何設置棧邊界的。

分屏模式設置棧邊界

step11-13

接下來我們看EventBus.getDefault().send(new DockedTopTaskEvent(dragMode, initialBounds)方法。EventBus是SystemUI裏面用來發送消息的一個機制,通過反射來實現相關功能。使用方法大致如下:

  • 首先在需要訂閱的類裏面通過EventBus.getDefault().register(this)註冊監聽,表示訂閱了某一種詳細。
  • 然後定製public final void onBusEvent()方法。
  • 最後調用send或者post方法,將消息發佈出去,所有訂閱了此消息的類都會收到此消息。

在DividerView裏面,註冊了EventBus事件。

@DividerView

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        EventBus.getDefault().register(this);
        mSurfaceFlingerOffsetMs = calculateAppSurfaceFlingerVsyncOffsetMs();
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

當我們執行了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());
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

接下來根據事件的初始化邊界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();
    }
  • 1
  • 2
  • 3
  • 4

當前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;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

接下來會根據中間分割線的位置,以及最終分割停留的位置(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);
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

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);
                    }
                }
            }

            //...
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

在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);
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 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();
            }
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

至此,PIP模式的分棧和設置棧邊界就分析完了。接下來我們分析自由模式。

自由模式


圖5-1          圖5-2

自由模式默認情況下是關閉的。如果需要打開,以下兩種方式選擇其一即可。

  • adb shell settings put globalenable_freeform_supporttrue
  • 給系統添加feature:android.software.freeform_window_management

如果打開了Freeform模式,當我們按了多任務按鍵之後,在每個任務View的標題欄上會多出一個方框,如圖5-1所示,當我們點擊了方框之後,會進入FreeForm模式。如圖5-2,我們可以看到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);
                // ...
                }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

@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;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

自由模式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;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

我們還是按照分棧和設置棧邊界來分析自由模式。

自由模式分棧

當我們在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));
        } 
        // ...
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

基於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);
        }
        // ... 
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

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;
        // ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

所以在接下來的判斷中launcherStackId等於INVALID_STACK_ID,也就不會走moveTask的操作。如下:

        if (launchStackId != INVALID_STACK_ID) {
            if (task.stack.mStackId != launchStackId) {
                moveTaskToStackLocked(
                        taskId, launchStackId, ON_TOP, FORCE_FOCUS, "startActivityFromRecents",
                        ANIMATE);
            }
        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

那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;
        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 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 */);
                }
            }
        }

       //...
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38

根據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;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

後面調用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);
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

接下來

@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;
            }
            // ...
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

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;
                }
        //...
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
 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;
                }
            }
        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

接下來會進入到WindowManagerService的startPositioningLocked方法,有如下兩個動作。

  • 註冊新的專門用於處於移動或者縮放的全雙工輸入管道,用來處理在當前Task邊界周圍30px以內的觸摸事件。
  • 告訴TaskPointer,當前的縮放模式以及首次點擊的座標位置。

@WindowManagerService.java

    private boolean startPositioningLocked(
            WindowState win, boolean resize, float startX, float startY) {
        // ...
        Display display = displayContent.getDisplay();
        mTaskPositioner = new TaskPositioner(this);
        mTaskPositioner.register(display);
        mInputMonitor.updateInputWindowsLw(true /*force*/);
        // ...
       mTaskPositioner.startDragLocked(win, resize, startX, startY);
     }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

流程圖如下:

這裏寫圖片描述

查看大圖

經過以上步驟,當我們繼續滑動手指的時候,此時的響應事件會轉移到WindowPositionerEventReceiver的onInputEvent方法。

這裏寫圖片描述

查看大圖

在notifyMoveLocked方法裏面,根據當前縮放模式以及初始化的位置和當前位置確認縮放窗口的大小。mCtrlType控制當前的縮放模式。CTRl_LEFT表示手指處於屏幕左邊,其他類似。

@TaskPositioner.java

private boolean notifyMoveLocked(float x, float y) {
        if (DEBUG_TASK_POSITIONING) {
            Slog.d(TAG, "notifyMoveLocked: {" + x + "," + y + "}");
        }

        if (mCtrlType != CTRL_NONE) {
            // This is a resizing operation.
            final int deltaX = Math.round(x - mStartDragX);
            final int deltaY = Math.round(y - mStartDragY);
            int left = mWindowOriginalBounds.left;
            int top = mWindowOriginalBounds.top;
            int right = mWindowOriginalBounds.right;
            int bottom = mWindowOriginalBounds.bottom;
            if ((mCtrlType & CTRL_LEFT) != 0) {
                left = Math.min(left + deltaX, right - mMinVisibleWidth);
            }
            if ((mCtrlType & CTRL_TOP) != 0) {
                top = Math.min(top + deltaY, bottom - mMinVisibleHeight);
            }
            if ((mCtrlType & CTRL_RIGHT) != 0) {
                right = Math.max(left + mMinVisibleWidth, right + deltaX);
            }
            if ((mCtrlType & CTRL_BOTTOM) != 0) {
                bottom = Math.max(top + mMinVisibleHeight, bottom + deltaY);
            }
            mWindowDragBounds.set(left, top, right, bottom);
            mTask.setDragResizing(true, DRAG_RESIZE_MODE_FREEFORM);
            return false;
        }

        // ...
        updateWindowDragBounds(nX, nY);
        updateDimLayerVisibility(nX);
        return dragEnded;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35

mWindowDragBounds 控制當前的Task的邊界。

接下來就會不斷的根據mWindowDragBounds的大小來resize我們的Task。

@TaskPointer.java

        @Override
        public void onInputEvent(InputEvent event) {
                    // ...
                    case MotionEvent.ACTION_MOVE: {
                        if (DEBUG_TASK_POSITIONING){
                            Slog.w(TAG, "ACTION_MOVE @ {" + newX + ", " + newY + "}");
                        }
                        synchronized (mService.mWindowMap) {
                            mDragEnded = notifyMoveLocked(newX, newY);
                            mTask.getDimBounds(mTmpRect);
                        }
                        // ...
                            try {
                                mService.mActivityManager.resizeTask(
                                        mTask.mTaskId, mWindowDragBounds, RESIZE_MODE_USER);
                            } catch (RemoteException e) {
                            }
                            Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);
                        }
                    } break;
                    // ...
       }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

mActivityManager.resizeTask的步驟和前面PIP以及分屏裏面講解的類似,不再闡述。

拖動窗口

窗體拖動註冊全雙工輸入管道的方式和窗口縮放是一樣的。唯一的區別是拖動的時候我們的手指是窗口最頂端的裝飾標題視圖之上。 對應DecorCaptionView.java,當我們點擊移動的時候,執行的是:

@DecorCaptionView.java


    @Override
    public boolean onTouch(View v, MotionEvent e) {
           // ...
            case MotionEvent.ACTION_MOVE:
                if (!mDragging && mCheckForDragging && passedSlop(x, y)) {
                    mCheckForDragging = false;
                    mDragging = true;
                    mLeftMouseButtonReleased = false;
                    startMovingTask(e.getRawX(), e.getRawY());
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

接下來到WMS裏面的startPositonLocked,步驟和窗口縮放就一致了。

@WMS.java

    boolean startMovingTask(IWindow window, float startX, float startY) {
        WindowState win = null;
        synchronized (mWindowMap) {
            // ...
            if (!startPositioningLocked(win, false /*resize*/, startX, startY)) {
                return false;
            }
        }
        // ...
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

總結

雖然說三種多窗口的表現形式不一致,但是原理大致類似。

  • 多窗口是在不同的窗口顯示不同Stack ID的Task。把不同的Task根據Stack ID來進行分類,分類主要是爲了方便設置窗口的大小。
  • Stack的大小決定了Stack 裏面的Task 的大小,最終決定了Task裏面的Activity對應的窗口的大小。

後記

本文所有的分析中,最終都是設置了Task的Bounds,那最終這個大小是如何起作用的呢?這個涉及到Window窗口的大小計算原理,內容很多,後續會有文章專門介紹。本文簡單說明,用戶肉眼可以看到的視圖實際上都是Window,每個窗口的大小都是通過WindowState的computeFrameLw計算出來的。在計算的過程中,其中有很重要的一步就是獲取當前窗口對應的task,task.getBounds也就是我們上面設置的大小。mContainingFrame最終會影響到窗口的大小,所以最終Task的邊界變成了窗口對應成了窗口的邊界。

@WindowState.java

    @Override 

    public void computeFrameLw(Rect pf, Rect df, Rect of, Rect cf, Rect vf, Rect dcf, Rect sf, 

            Rect osf) { 

          // ... 

        if (fullscreenTask || layoutInParentFrame()) { 

           // ... 

        } else { 

            task.getBounds(mContainingFrame); 

        } 

    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章