Android Anatomy:Android 7.0中的多窗口實現解析

多窗口功能介紹

概述

在以往的Android系統上,全部Activity都是全屏的,若是不設置透明效果,一次只能看到一個Activity界面。
可是從Android N(7.0)版本開始,系統支持了多窗口功能。
在有了多窗口支持以後,用戶能夠同時打開和看到多個應用的界面。
而且系統還支持在多個應用之間進行拖拽。在大屏幕設備上,這一功能很是實用。
本文將詳細講解Android系統中多窗口功能的實現。html

關於Android N的新特性,請參見這裏:Android 7.0 for Developers
關於多窗口的詳細說明,請參見這裏:Multi-Window Supportjava

Android N上的多窗口功能有三種模式android

  • 分屏模式
    這種模式能夠在手機上使用。該模式將屏幕一分爲二,同時顯示兩個應用的界面。以下圖所示:git

splitscreen

  • 畫中畫模式
    這種模式主要在TV上使用,在該模式下視頻播放的窗口能夠一直在最頂層顯示。以下圖所示:github

pip-active

  • Freeform模式
    這種模式相似於咱們常見的桌面操做系統,應用界面的窗口能夠自由拖動和修改大小。以下圖所示:shell

freeform_mode

生命週期

多窗口不影響和改變原先Activity的生命週期。windows

在多窗口模式,多個Activity能夠同時可見,但只有一個Activity是最頂層的,即:獲取焦點的Activity。數據結構

全部其餘Activity都會處於Paused狀態(儘管它們是可見的)。
在如下三種場景下,系統會通知應用有狀態變化,應用能夠進行處理:架構

  • 當用戶以多窗口的模式啓動的應用app

  • 當用戶改變了Activity的窗口大小

  • 當用戶將應用窗口從多窗口模式改成全屏模式

關於應用如何進行狀態變化的處理,請參見這裏:Handling Runtime Changes,這裏再也不贅述。

開發者相關

Android從API Level 24開始,提供瞭如下一些機制來配合多窗口功能的使用。

  • Manifest新增屬性

    • android:resizeableActivity=["true" | "false"]

    這個屬性能夠用在<activity>或者<application> 上。置爲true,表示能夠以分屏或者Freeform模式啓動。false表示不支持多窗口模式。對於API目標Level爲24的應用來講,這個值默認是true。

    • android:supportsPictureInPicture=["true" | "false"]

    這個屬性用在<activity>上,表示是否支持畫中畫模式。若是android:resizeableActivity爲false,這個屬性值將被忽略。

  • Layout新增屬性

    • android:defaultWidth,android:defaultHeight Freeform模式下的默認寬度和高度

    • android:gravity Freeform模式下的初始Gravity

    • android:minWidth, android:minHeight 分屏和Freeform模式下的最小高度和寬度

這裏是一段代碼示例:

<activity android:name=".MyActivity">
    <layout android:defaultHeight="500dp"
          android:defaultWidth="600dp"
          android:gravity="top|end"
          android:minHeight="450dp"
          android:minWidth="300dp" />
</activity>
  • 新增API

    • Activity.isInMultiWindowMode() 查詢是否處於多窗口模式

    • Activity.isInPictureInPictureMode() 查詢是否處於畫中畫模式

    • Activity.onMultiWindowModeChanged() 多窗口模式變化時進行通知(進入或退出多窗口)

    • Activity.onPictureInPictureModeChanged() 畫中畫模式變化時進行通知(進入或退出畫中畫模式)

    • Activity.enterPictureInPictureMode() 調用這個接口進入畫中畫模式,若是系統不支持,這個調用無效

    • ActivityOptions.setLaunchBounds() 在系統已經處於Freeform模式時,能夠經過這個參數來控制新啓動的Activity大小,若是系統不支持,這個調用無效

  • 拖拽相關
    Android N以前,系統只容許在一個Activity內部進行拖拽。但從Android N開始,系統支持在多個Activity之間進行拖拽,下面是一些相關的API。具體說明請參見官方文檔。

- `DragAndDropPermissions`
- `View.startDragAndDrop()`
- `View.cancelDragAndDrop()`
- `View.updateDragShadow()`
- `Activity.requestDragAndDropPermissions()`

相關模塊和主要類

本文,咱們主要關注多窗口的功能實現。這裏列出了多窗口功能實現的主要類和模塊。

這裏的代碼路徑是指AOSP的源碼路徑,關於如何獲取AOSP源碼請參見這裏:Downloading the Source

ActivityManager

代碼路徑:/frameworks/base/services/core/java/com/android/server/am

  • ActivityManagerService 負責運行時管理的系統服務,這個類掌管了Android系統的四大組件(Activity,Service,BroadcastReceiver,ContentProvider),應用進程的啓動退出,進程優先級的控制。說它是Framework中最重要的系統服務都不爲過。

  • TaskRecord,ActivityStack 管理Activity的容器,多窗口的實現強烈依賴於ActivityStack,下文會詳細講解。

  • ActivityStackSupervisor 顧名思義,專門負責管理ActivityStack。

  • ActivityStarter Android N新增類。掌控Activity的啓動。

WindowManager

代碼路徑:/frameworks/base/services/core/java/com/android/server/wm

  • WindowManagerService 負責窗口管理的系統服務。

  • Task,TaskStack 管理窗口對象的容器,與TaskRecord和ActivityStack對應。

  • WindowLayersController Android N新增類,專門負責Z-Order的計算。Z-Order決定了窗口的上下關係。

Framework API

代碼路徑:frameworks/base/core/java/

  • ActivityManager 提供了管理Activity的接口和常量。

  • ActivityOptions 提供了啓動Activity的參數選項,例如,在Freefrom模式下,設置窗口大小。

SystemUI

代碼路徑:/frameworks/base/packages/SystemUI/

顧名思義:系統UI,這裏包括:NavigationBar,StatusBar,Keyguard等。

  • PhoneStatusBar SystemUI中很是重要的一個類,負責了不少組件的初始化和控制。

爲了便於說明,下文將直接使用這裏提到的類。若是你想查看這些類的源碼,請參閱這裏的路徑。

多窗口的功能實現

多窗口功能的實現主要依賴於ActivityManagerService與WindowManagerService這兩個系統服務,它們都位於system_server進程中。該進程是Android系統中一個很是重要的系統進程。Framework中的不少服務都位於這個進程中。

整個Android的架構是CS的模型,應用程序是Client,而system_server進程就是對應的Server。

應用程序調用的不少API都會發送到system_server進程中對應的系統服務上進行處理,例如startActivity這個API,最終就是由ActivityManagerService進行處理。

而因爲應用程序和system_server在各自獨立的進程中運行,所以對於系統服務的請求須要經過Binder進行進程間通信(IPC)來完成調用,以及調用結果的返回。

兩個系統服務簡介

ActivityManagerService負責Activity管理。

對於應用中建立的每個Activity,在ActivityManagerService中都會有一個與之對應的ActivityRecord,這個ActivityRecord記錄了應用程序中的Activity的狀態。ActivityManagerService會利用這個ActivityRecord做爲標識,對應用程序中的Activity進程調度,例如生命週期的管理。

實際上,ActivityManagerService的職責遠超出的它的名稱,ActivityManagerService負責了全部四大組件(Activity,Service,BroadcastReceiver,ContentProvider)的管理,以及應用程序的進程管理。

WindowManagerService負責Window管理。包括:

  • 窗口的建立和銷燬

  • 窗口的顯示與隱藏

  • 窗口的佈局

  • 窗口的Z-Order管理

  • 焦點的管理

  • 輸入法和壁紙管理

等等
每個Activity都會有一個本身的窗口,在WindowManagerService中便會有一個與之對應的WindowState。WindowManagerService以此標示應用程序中的窗口,並用這個WindowState來存儲,查詢和控制窗口的狀態。

ActivityManagerService與WindowManagerService須要緊密配合在一塊兒工做,由於不管是建立仍是銷燬Activity都牽涉到Actiivty對象和窗口對象的建立和銷燬。這二者是既相互獨立,又緊密關聯在一塊兒的。

Activity啓動過程

Activity的啓動過程主要包含如下幾個步驟:

  • Intent的解析(Intent多是隱式的:關於Intents and Intent Filters

  • Activity的匹配(符合Intent的Activity可能會有多個)

  • 應用進程的建立

  • Task,Stack的獲取或者建立

  • Activity窗口的建立

  • Activity生命週期的調度(onCreate,onResume等)

本文不打算講解Activity啓動的詳細過程,對於這部份內容有興趣的讀者請參閱其餘資料。

Task和Stack

Android系統中的每個Activity都位於一個Task中。一個Task能夠包含多個Activity,同一個Activity也可能有多個實例。
在AndroidManifest.xml中,咱們能夠經過android:launchMode來控制Activity在Task中的實例。

另外,在startActivity的時候,咱們也能夠經過setFlag) 來控制啓動的Activity在Task中的實例。

Task管理的意義還在於近期任務列表以及Back棧。
當你經過多任務鍵(有些設備上是長按Home鍵,有些設備上是專門提供的多任務鍵)調出多任務時,其實就是從ActivityManagerService獲取了最近啓動的Task列表。

Back棧管理了當你在Activity上點擊Back鍵,當前Activity銷燬後應該跳轉到哪個Activity的邏輯。關於Task和Back棧,請參見這裏:Tasks and Back Stack

其實在ActivityManagerService與WindowManagerService內部管理中,在Task以外,還有一層容器,這個容器應用開發者和用戶可能都不會感受到或者用到,但它卻很是重要,那就是Stack。
下文中,咱們將看到,Android系統中的多窗口管理,就是創建在Stack的數據結構上的
一個Stack中包含了多個Task,一個Task中包含了多個Activity(Window),下圖描述了它們的關係:
Task_Stack

另外還有一點須要注意的是,ActivityManagerService和WindowManagerService中的Task和Stack結構是一一對應的,對應關係對於以下:

  • ActivityStack <--> TaskStack

  • TaskRecord <--> Task

即,ActivityManagerService中的每個ActivityStack或者TaskRecord在WindowManagerService中都有對應的TaskStack和Task,這兩類對象都有惟一的id(id是int類型),它們經過id進行關聯。

多窗口與Stack

用過macOS或者Ubuntu的人應該都會用過虛擬桌面的功能,以下圖所示:
mac_multi_desktop
這裏建立了多個「虛擬桌面」,並在最上面一排列出了出來。
每一個虛擬桌面裏面均可以放置一個或多個應用窗口,虛擬桌面能夠做爲一個總體進行切換。

Android爲了支持多窗口,在運行時建立了多個Stack,Stack就是相似這裏虛擬桌面的做用。

每一個Stack會有一個惟一的Id,在ActivityManager.java中定義了這些Stack的Id:

/** 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;

由此咱們能夠知道,系統中可能會包含這麼幾個Stack:

  • 【Id:0】Home Stack,這個是Launcher所在的Stack。 其實還有一些系統界面也運行在這個Stack上,例如近期任務

  • 【Id:1】FullScren Stack,全屏的Activity所在的Stack。 但其實在分屏模式下,Id爲1的Stack只佔了半個屏幕。

  • 【Id:2】Freeform模式的Activity所在Stack

  • 【Id:3】Docked Stack 下文中咱們將看到,在分屏模式下,屏幕有一半運行了一個固定的應用,這個就是這裏的Docked Stack

  • 【Id:4】Pinned Stack 這個是畫中畫Activity所在的Stack

須要注意的是,這些Stack並非系統一啓動就所有建立好的。而是在須要用到的時候纔會建立。上文已經提到過,ActivityStackSupervisor負責ActivityStack的管理。

有了以上這些背景知識以後,咱們再來具體講解一下Android系統中的三種多窗口模式。

分屏模式

在Nexus 6P手機上,分屏模式的啓動和退出是長按多任務虛擬按鍵。 下圖是在Nexus 6P上啓動分屏模式的樣子:

splitscreen_mode_on_nexus-w360

在啓動分屏模式的以後,系統會將屏幕一分爲二。當前打開的應用移到屏幕上方(若是是橫屏那就是左邊),其餘全部打開的應用,在下方(若是是橫屏那就是右邊)以多任務形式列出。

以後用戶在操做的時候,下方的半屏保持了原先的使用方式:能夠啓動或退出應用,能夠啓動多任務後進行切換,而上方的應用保持不變。
前面咱們已經提到過,其實這裏處於上半屏固定不變的應用就是處在Docked的Stack中(Id爲3),下半屏是以前全屏的Stack進行了Resize(Id爲1)。

下面,咱們就順着長按多任務按鈕爲線索,來調查一下分屏模式是如何啓動的:
其實不管是NavigationBar(屏幕最下方的三個虛擬按鍵)仍是StatusBar(屏幕最上方的狀態欄)都是在SystemUI中。咱們能夠以此爲入口來調查。

PhoneStatusBar#prepareNavigationBarView 爲NavigationBar初始化了UI。同時也在這裏爲按鈕設置了事件監聽器。這裏包括咱們感興趣的近期任務按鈕的長按事件監聽器:

private void prepareNavigationBarView() {
   mNavigationBarView.reorient();

   ButtonDispatcher recentsButton = mNavigationBarView.getRecentsButton();
   recentsButton.setOnClickListener(mRecentsClickListener);
   recentsButton.setOnTouchListener(mRecentsPreloadOnTouchListener);
   recentsButton.setLongClickable(true);
   recentsButton.setOnLongClickListener(mRecentsLongClickListener);
   ...
}

在mRecentsLongClickListener中,主要的邏輯就是調用toggleSplitScreenMode。

toggleSplitScreenMode這個方法的名稱很明顯的告訴咱們,這裏是在切換分屏模式

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

再順着往下看PhoneStatusBar#toggleSplitScreenMode的代碼:

這裏咱們看到,經過查詢WindowManagerProxy.getInstance().getDockSide(); 來肯定當前是否處於分屏模式,若是沒有則將Top Task移到Docked的Stack上。這裏的Top Task就是咱們在長按多任務按鍵以前打開的當前應用。

@Override
protected void toggleSplitScreenMode(int metricsDockAction, int metricsUndockAction) {
   if (mRecents == null) {
       return;
   }
   int dockSide = WindowManagerProxy.getInstance().getDockSide();
   if (dockSide == WindowManager.DOCKED_INVALID) {
       mRecents.dockTopTask(NavigationBarGestureHelper.DRAG_MODE_NONE,
               ActivityManager.DOCKED_STACK_CREATE_MODE_TOP_OR_LEFT, null, metricsDockAction);
   } else {
       EventBus.getDefault().send(new UndockingTaskEvent());
       if (metricsUndockAction != -1) {
           MetricsLogger.action(mContext, metricsUndockAction);
       }
   }
}

以後便會調用到ActivityManagerService#moveTaskToDockedStack中。後面的大部分邏輯在ActivityStackSupervisor#moveTaskToStackLocked中,在這個方法中,會作以下幾件事情:

  • 經過指定的taskId獲取對應的TaskRecord

  • 爲當前Activity替換窗口(由於要從FullScreen的Stack切換的Docked Stack上)

  • 調用mWindowManager.deferSurfaceLayout通知WindowManagerService暫停佈局

  • 將當前TaskRecord移動到Docked Stack上

  • 爲移動後的Task和Stack設置Bounds,而且進行resize。這裏還會通知Activity onMultiWindowModeChanged

  • 調用mWindowManager.continueSurfaceLayout(); 通知WindowManagerService繼續開始佈局

而Resize和佈局就徹底是WindowManagerService的事情,這裏面須要計算兩個Stack各自的大小,而後根據大小來對Stack中的Task和Activity窗口進行從新佈局。

因爲篇幅關係,這裏再也不貼出更多的代碼。若是有興趣,請自行獲取AOSP的代碼而後查看。

下圖總結了啓動分屏模式的執行邏輯:
sequece_splitscreen_mode_start

這裏須要注意的是:

黃色標記的模式是運行在SystemUI的進程中。

藍色標記的模式是運行在system_server進程中。

moveTaskToDockedStack是一個Binder調用,經過IPC調用到了ActivityManagerService。

畫中畫模式

當應用程序調用Activity#enterPictureInPictureMode便進入了畫中畫模式。

Activity#enterPictureInPictureMode會經過Binder調用到ActivityManagerService中對應的方法,該方法代碼以下:

public void enterPictureInPictureMode(IBinder token) {
   final long origId = Binder.clearCallingIdentity();
   try {
       synchronized(this) {
           if (!mSupportsPictureInPicture) {
               throw new IllegalStateException("enterPictureInPictureMode: "
                       + "Device doesn't support picture-in-picture mode.");
           }

           final ActivityRecord r = ActivityRecord.forTokenLocked(token);

           if (r == null) {
               throw new IllegalStateException("enterPictureInPictureMode: "
                       + "Can't find activity for token=" + token);
           }

           if (!r.supportsPictureInPicture()) {
               throw new IllegalArgumentException("enterPictureInPictureMode: "
                       + "Picture-In-Picture not supported for r=" + r);
           }

           // 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.getStack(PINNED_STACK_ID); 是在獲取Pinned Stack,當這個Stack不存在時,會將其建立。
IBinder token是調用enterPictureInPictureMode的Activity的Binder標示,經過這個標示能夠獲取到Activity對應的ActivityRecord對象,而後就是將這個ActivityRecord移動到Pinned Stack上。

Pinned Stack的默認大小來自於mDefaultPinnedStackBounds,這個值是從Internal的Resource上獲取的:

mDefaultPinnedStackBounds = Rect.unflattenFromString(res.getString(
     com.android.internal.R.string.config_defaultPictureInPictureBounds));

而com.android.internal.R.string.config_defaultPictureInPictureBounds的值是從配置文件:/frameworks/base/core/res/res/values/config.xml 中讀取的。

爲何一旦將Activity移動到Pinned Stack上,該窗口就能一直在最上層顯示呢?這就是由Z-Order控制的,Z-Order決定了窗口的上下關係。
Android N中新增了一個類WindowLayersController來專門負責Z-Order的計算。在計算Z-Order的時候,有幾類窗口會進行特殊處理,處於Pinned Stack上的窗口即是其中之一。

下面這段代碼是在統計哪些窗口是須要特殊處理的,這裏能夠看到,除了Pinned Stack上的窗口,還有分屏模式下的窗口以及輸入法窗口都須要特殊處理:

private void collectSpecialWindows(WindowState w) {
   if (w.mAttrs.type == TYPE_DOCK_DIVIDER) {
       mDockDivider = w;
       return;
   }
   if (w.mWillReplaceWindow) {
       mReplacingWindows.add(w);
   }
   if (w.mIsImWindow) {
       mInputMethodWindows.add(w);
       return;
   }
   final TaskStack stack = w.getStack();
   if (stack == null) {
       return;
   }
   if (stack.mStackId == PINNED_STACK_ID) {
       mPinnedWindows.add(w);
   } else if (stack.mStackId == DOCKED_STACK_ID) {
       mDockedWindows.add(w);
   }
}

在統計完這些特殊窗口以後,在計算Z-Order時會對它們進行特殊處理:

private void adjustSpecialWindows() {
   int layer = mHighestApplicationLayer + WINDOW_LAYER_MULTIPLIER;
   // For pinned and docked stack window, we want to make them above other windows also when
   // these windows are animating.
   while (!mDockedWindows.isEmpty()) {
       layer = assignAndIncreaseLayerIfNeeded(mDockedWindows.remove(), layer);
   }

   layer = assignAndIncreaseLayerIfNeeded(mDockDivider, layer);

   if (mDockDivider != null && mDockDivider.isVisibleLw()) {
       while (!mInputMethodWindows.isEmpty()) {
           final WindowState w = mInputMethodWindows.remove();
           // Only ever move IME windows up, else we brake IME for windows above the divider.
           if (layer > w.mLayer) {
               layer = assignAndIncreaseLayerIfNeeded(w, layer);
           }
       }
   }

   // We know that we will be animating a relaunching window in the near future, which will
   // receive a z-order increase. We want the replaced window to immediately receive the same
   // treatment, e.g. to be above the dock divider.
   while (!mReplacingWindows.isEmpty()) {
       layer = assignAndIncreaseLayerIfNeeded(mReplacingWindows.remove(), layer);
   }

   while (!mPinnedWindows.isEmpty()) {
       layer = assignAndIncreaseLayerIfNeeded(mPinnedWindows.remove(), layer);
   }
}

這段代碼保證了處於Pinned Stack上的窗口(即處於畫中畫模式的窗口)的會在普通的應用窗口之上。

Freeform模式

在Andorid N設備上打開Freeform模式很簡單,只需如下兩個步驟:

  1. 執行如下命令:adb shell settings put global enable_freeform_support 1

  2. 而後重啓手機:adb reboot

重啓以後,在近期任務界面會出現一個按鈕,這個按鈕能夠將窗口切換到Freeform模式,以下圖所示:
startActivity

這個按鈕的做用其實就是將當前應用移到Freeform Stack上,相關邏輯在:

ActivityStackSupervisor中,代碼以下:

void findTaskToMoveToFrontLocked(TaskRecord task, int flags, ActivityOptions options, String reason, boolean forceNonResizeable) {
   ...
   if (task.isResizeable() && options != null) {
       int stackId = options.getLaunchStackId();
       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) {
               final ActivityStack stack = moveTaskToStackUncheckedLocked(
                       task, stackId, ON_TOP, !FORCE_FOCUS, reason);
               stackId = stack.mStackId;
               ...
}

這段代碼經過查詢stackId,而後調用moveTaskToStackUncheckedLocked移動Task。
而這裏經過task.getLaunchStackId() 獲取到的stackId,其實就是FREEFORM_WORKSPACE_STACK_ID,相關代碼以下:

TaskRecord#getLaunchStackId代碼以下:

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

即,若是設置了Bound,便表示該Task會在Freeform Stack上啓動。

PS:這裏將應用切換到Freeform模式,必須先打開應用,而後在近期任務中切換。

若是想要打開應用就直接進入Freeform模式,能夠看一下這篇文章:

Taskbar lets you enable Freeform mode on Android Nougat without root or adb

若是沒有Google Play,能夠到這裏下載上文中提到的Taskbar應用:Taskbar

有興趣的讀者也能夠去GitHub上獲取TaskBar的源碼:farmerbb/Taskbar

其實TaskBar的原理就是在啓動Activity的時候設置了Activity的Bounds,相關代碼以下:

public static void launchPhoneSize(Context context, Intent intent) {
   DisplayManager dm = (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
   Display display = dm.getDisplay(Display.DEFAULT_DISPLAY);

   int width1 = display.getWidth() / 2;
   int width2 = context.getResources().getDimensionPixelSize(R.dimen.phone_size_width) / 2;
   int height1 = display.getHeight() / 2;
   int height2 = context.getResources().getDimensionPixelSize(R.dimen.phone_size_height) / 2;

   try {
       context.startActivity(intent, ActivityOptions.makeBasic().setLaunchBounds(new Rect(
               width1 - width2,
               height1 - height2,
               width1 + width2,
               height1 + height2
       )).toBundle());
   } catch (ActivityNotFoundException e) { /* Gracefully fail */ }
}

而在ActivityStarter#computeStackFocus中會判斷若是新啓動的Activity設置了Bounds,

則在FULLSCREEN_WORKSPACE_STACK_ID這個Stack上啓動Activity,相關代碼以下:

final int stackId = task != null ? task.getLaunchStackId() :
      bounds != null ? FREEFORM_WORKSPACE_STACK_ID :
              FULLSCREEN_WORKSPACE_STACK_ID;
stack = mSupervisor.getStack(stackId, CREATE_IF_NEEDED, ON_TOP);
if (DEBUG_FOCUS || DEBUG_STACK) Slog.d(TAG_FOCUS, "computeStackFocus: New stack r="
      + r + " stackId=" + stack.mStackId);
return stack;

下圖是啓動Activity時建立Stack的調用過程:
startActivity

當你在全屏應用以及Freeform模式應用來回切換的時候,系統所作的其實就是在FullScreen Stack和Freeform Stack上來回切換而已,這和前面提到的虛擬桌面的幾乎是同樣的。

至此,三種多窗口模式就都分析完了,回過頭來再看一下,三種多窗口模式的實現其實都是依賴於Stack結構,明白其原理,發現也沒有特別神祕的地方。

--以上--

參考資料

https://developer.android.com/guide/topics/ui/multi-window.html

https://developer.android.com/guide/components/tasks-and-back-stack.html

http://arstechnica.com/gadgets/2016/03/this-is-android-ns-freeform-window-mode/

http://www.androidpolice.com/2016/08/27/taskbar-lets-enable-freeform-mode-android-nougat-without-root-adb/

相關文章
相關標籤/搜索