多窗口功能介紹
概述
Android 從 Android N(7.0)版本開始引入了多窗口的功能。
關(guān)于Android N的新特性,請參見這里:Android 7.0 for Developers
關(guān)于多窗口的詳細說明,請參見這里:Multi-Window Support
Android N上的多窗口功能有三種模式:
1.?分屏模式
這種模式可以在手機上使用。該模式將屏幕一分為二,同時顯示兩個應用的界面。如下圖所示:
2.?畫中畫模式
這種模式主要在TV上使用,在該模式下視頻播放的窗口可以一直在最頂層顯示。如下圖所示:
3.?Freeform模式
這種模式類似于我們常見的桌面操作系統(tǒng),應用界面的窗口可以自由拖動和修改大小。如下圖所示:
生命周期
多窗口不影響和改變原先Activity的生命周期。
在多窗口模式,多個Activity可以同時可見,但只有一個Activity是最頂層的,即:獲取焦點的Activity。
所有其他Activity都會處于Paused狀態(tài)(盡管它們是可見的)。
在以下三種場景下,系統(tǒng)會通知應用有狀態(tài)變化,應用可以進行處理:
當用戶以多窗口的模式啟動的應用
當用戶改變了Activity的窗口大小
當用戶將應用窗口從多窗口模式改為全屏模式
關(guān)于應用如何進行狀態(tài)變化的處理,請參見這里:Handling Runtime Changes,這里不再贅述。
開發(fā)者相關(guān)
Android從API Level 24開始,提供了以下一些機制來配合多窗口功能的使用。
Manifest新增屬性
android:resizeableActivity=["true" | "false"]
這個屬性可以用在或者 上。置為true,表示可以以分屏或者Freeform模式啟動。false表示不支持多窗口模式。對于API目標Level為24的應用來說,這個值默認是true。
-android:supportsPictureInPicture=["true" | "false"]這個屬性用在上,表示是否支持畫中畫模式。如果android:resizeableActivity為false,這個屬性值將被忽略。
Layout新增屬性
android:defaultWidth,android:defaultHeightFreeform模式下的默認寬度和高度
android:gravityFreeform模式下的初始Gravity
android:minWidth, android:minHeight分屏和Freeform模式下的最小高度和寬度
這里是一段代碼示例:
新增API
Activity.isInMultiWindowMode()查詢是否處于多窗口模式
Activity.isInPictureInPictureMode()查詢是否處于畫中畫模式
Activity.onMultiWindowModeChanged()多窗口模式變化時進行通知(進入或退出多窗口)
Activity.onPictureInPictureModeChanged()畫中畫模式變化時進行通知(進入或退出畫中畫模式)
Activity.enterPictureInPictureMode()調(diào)用這個接口進入畫中畫模式,如果系統(tǒng)不支持,這個調(diào)用無效
ActivityOptions.setLaunchBounds()在系統(tǒng)已經(jīng)處于Freeform模式時,可以通過這個參數(shù)來控制新啟動的Activity大小,如果系統(tǒng)不支持,這個調(diào)用無效
拖拽相關(guān)?Android N之前,系統(tǒng)只允許在一個Activity內(nèi)部進行拖拽。但從Android N開始,系統(tǒng)支持在多個Activity之間進行拖拽,下面是一些相關(guān)的API。具體說明請參見官方文檔。
DragAndDropPermissions
View.startDragAndDrop()
View.cancelDragAndDrop()
View.updateDragShadow()
Activity.requestDragAndDropPermissions()
相關(guān)模塊和主要類
本文,我們主要關(guān)注多窗口的功能實現(xiàn)。這里列出了多窗口功能實現(xiàn)的主要類和模塊。
這里的代碼路徑是指AOSP的源碼路徑,關(guān)于如何獲取AOSP源碼請參見這里:Downloading the Source。
ActivityManager
代碼路徑:/frameworks/base/services/core/java/com/android/server/am
ActivityManagerService?負責運行時管理的系統(tǒng)服務,這個類掌管了Android系統(tǒng)的四大組件(Activity,Service,BroadcastReceiver,ContentProvider),應用進程的啟動退出,進程優(yōu)先級的控制。說它是Framework中最重要的系統(tǒng)服務都不為過。
TaskRecord,ActivityStack?管理Activity的容器,多窗口的實現(xiàn)強烈依賴于ActivityStack,下文會詳細講解。
ActivityStackSupervisor?顧名思義,專門負責管理ActivityStack。
ActivityStarter?Android N新增類。掌控Activity的啟動。
WindowManager
代碼路徑:/frameworks/base/services/core/java/com/android/server/wm
WindowManagerService?負責窗口管理的系統(tǒng)服務。
Task,TaskStack?管理窗口對象的容器,與TaskRecord和ActivityStack對應。
WindowLayersController?Android N新增類,專門負責Z-Order的計算。Z-Order決定了窗口的上下關(guān)系。
Framework API
代碼路徑:frameworks/base/core/java/
ActivityManager?提供了管理Activity的接口和常量。
ActivityOptions?提供了啟動Activity的參數(shù)選項,例如,在Freefrom模式下,設(shè)置窗口大小。
SystemUI
代碼路徑:/frameworks/base/packages/SystemUI/
顧名思義:系統(tǒng)UI,這里包括:NavigationBar,StatusBar,Keyguard等。
PhoneStatusBar?SystemUI中非常重要的一個類,負責了很多組件的初始化和控制。
為了便于說明,下文將直接使用這里提到的類。如果你想查看這些類的源碼,請參閱這里的路徑。
多窗口的功能實現(xiàn)
多窗口功能的實現(xiàn)主要依賴于ActivityManagerService與WindowManagerService這兩個系統(tǒng)服務,它們都位于system_server進程中。該進程是Android系統(tǒng)中一個非常重要的系統(tǒng)進程。Framework中的很多服務都位于這個進程中。
整個Android的架構(gòu)是CS的模型,應用程序是Client,而system_server進程就是對應的Server。
應用程序調(diào)用的很多API都會發(fā)送到system_server進程中對應的系統(tǒng)服務上進行處理,例如startActivity這個API,最終就是由ActivityManagerService進行處理。
而由于應用程序和system_server在各自獨立的進程中運行,因此對于系統(tǒng)服務的請求需要通過Binder進行進程間通訊(IPC)來完成調(diào)用,以及調(diào)用結(jié)果的返回。
兩個系統(tǒng)服務簡介
ActivityManagerService負責Activity管理。
對于應用中創(chuàng)建的每一個Activity,在ActivityManagerService中都會有一個與之對應的ActivityRecord,這個ActivityRecord記錄了應用程序中的Activity的狀態(tài)。ActivityManagerService會利用這個ActivityRecord作為標識,對應用程序中的Activity進程調(diào)度,例如生命周期的管理。
實際上,ActivityManagerService的職責遠超出的它的名稱,ActivityManagerService負責了所有四大組件(Activity,Service,BroadcastReceiver,ContentProvider)的管理,以及應用程序的進程管理。
WindowManagerService負責Window管理。包括:
窗口的創(chuàng)建和銷毀
窗口的顯示與隱藏
窗口的布局
窗口的Z-Order管理
焦點的管理
輸入法和壁紙管理
等等 每一個Activity都會有一個自己的窗口,在WindowManagerService中便會有一個與之對應的WindowState。WindowManagerService以此標示應用程序中的窗口,并用這個WindowState來存儲,查詢和控制窗口的狀態(tài)。
ActivityManagerService與WindowManagerService需要緊密配合在一起工作,因為無論是創(chuàng)建還是銷毀Activity都牽涉到Actiivty對象和窗口對象的創(chuàng)建和銷毀。這兩者是既相互獨立,又緊密關(guān)聯(lián)在一起的。
Activity啟動過程
Activity的啟動過程主要包含以下幾個步驟:
Intent的解析(Intent可能是隱式的:關(guān)于Intents and Intent Filters)
Activity的匹配(符合Intent的Activity可能會有多個)
應用進程的創(chuàng)建
Task,Stack的獲取或者創(chuàng)建
Activity窗口的創(chuàng)建
Activity生命周期的調(diào)度(onCreate,onResume等)
本文不打算講解Activity啟動的詳細過程,對于這部分內(nèi)容有興趣的讀者請參閱其他資料,比如這篇Android中Activity啟動過程探究
Task和Stack
Android系統(tǒng)中的每一個Activity都位于一個Task中。一個Task可以包含多個Activity,同一個Activity也可能有多個實例。 在AndroidManifest.xml中,我們可以通過android:launchMode來控制Activity在Task中的實例。
另外,在startActivity的時候,我們也可以通過setFlag來控制啟動的Activity在Task中的實例。
Task管理的意義還在于近期任務列表以及Back棧。 當你通過多任務鍵(有些設(shè)備上是長按Home鍵,有些設(shè)備上是專門提供的多任務鍵)調(diào)出多任務時,其實就是從ActivityManagerService獲取了最近啟動的Task列表。
Back棧管理了當你在Activity上點擊Back鍵,當前Activity銷毀后應該跳轉(zhuǎn)到哪一個Activity的邏輯。關(guān)于Task和Back棧,請參見這里:Tasks and Back Stack。
其實在ActivityManagerService與WindowManagerService內(nèi)部管理中,在Task之外,還有一層容器,這個容器應用開發(fā)者和用戶可能都不會感覺到或者用到,但它卻非常重要,那就是Stack。 下文中,我們將看到,Android系統(tǒng)中的多窗口管理,就是建立在Stack的數(shù)據(jù)結(jié)構(gòu)上的。 一個Stack中包含了多個Task,一個Task中包含了多個Activity(Window),下圖描述了它們的關(guān)系:
另外還有一點需要注意的是,ActivityManagerService和WindowManagerService中的Task和Stack結(jié)構(gòu)是一一對應的,對應關(guān)系對于如下:
ActivityStack <–> TaskStack
TaskRecord <–> Task
即,ActivityManagerService中的每一個ActivityStack或者TaskRecord在WindowManagerService中都有對應的TaskStack和Task,這兩類對象都有唯一的id(id是int類型),它們通過id進行關(guān)聯(lián)。
多窗口與Stack
用過macOS或者Ubuntu的人應該都會用過虛擬桌面的功能,如下圖所示:
這里創(chuàng)建了多個“虛擬桌面”,并在最上面一排列出了出來。 每個虛擬桌面里面都可以放置一個或多個應用窗口,虛擬桌面可以作為一個整體進行切換。
Android為了支持多窗口,在運行時創(chuàng)建了多個Stack,Stack就是類似這里虛擬桌面的作用。
每個Stack會有一個唯一的Id,在ActivityManager.java中定義了這些Stack的Id:
/** First static stack ID. */publicstaticfinalintFIRST_STATIC_STACK_ID=0;/** Home activity stack ID. */publicstaticfinalintHOME_STACK_ID=FIRST_STATIC_STACK_ID;/** ID of stack where fullscreen activities are normally launched into. */publicstaticfinalintFULLSCREEN_WORKSPACE_STACK_ID=1;/** ID of stack where freeform/resized activities are normally launched into. */publicstaticfinalintFREEFORM_WORKSPACE_STACK_ID=FULLSCREEN_WORKSPACE_STACK_ID+1;/** ID of stack that occupies a dedicated region of the screen. */publicstaticfinalintDOCKED_STACK_ID=FREEFORM_WORKSPACE_STACK_ID+1;/** ID of stack that always on top (always visible) when it exist. */publicstaticfinalintPINNED_STACK_ID=DOCKED_STACK_ID+1;
由此我們可以知道,系統(tǒng)中可能會包含這么幾個Stack:
【Id:0】Home Stack,這個是Launcher所在的Stack。?其實還有一些系統(tǒng)界面也運行在這個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并不是系統(tǒng)一啟動就全部創(chuàng)建好的。而是在需要用到的時候才會創(chuàng)建。上文已經(jīng)提到過,ActivityStackSupervisor負責ActivityStack的管理。
有了以上這些背景知識之后,我們再來具體講解一下Android系統(tǒng)中的三種多窗口模式。
分屏模式
在Nexus 6P手機上,分屏模式的啟動和退出是長按多任務虛擬按鍵。?下圖是在Nexus 6P上啟動分屏模式的樣子:
在啟動分屏模式的之后,系統(tǒng)會將屏幕一分為二。當前打開的應用移到屏幕上方(如果是橫屏那就是左邊),其他所有打開的應用,在下方(如果是橫屏那就是右邊)以多任務形式列出。
之后用戶在操作的時候,下方的半屏保持了原先的使用方式:可以啟動或退出應用,可以啟動多任務后進行切換,而上方的應用保持不變。 前面我們已經(jīng)提到過,其實這里處于上半屏固定不變的應用就是處在Docked的Stack中(Id為3),下半屏是之前全屏的Stack進行了Resize(Id為1)。
下面,我們就順著長按多任務按鈕為線索,來調(diào)查一下分屏模式是如何啟動的: 其實無論是NavigationBar(屏幕最下方的三個虛擬按鍵)還是StatusBar(屏幕最上方的狀態(tài)欄)都是在SystemUI中。我們可以以此為入口來調(diào)查。
PhoneStatusBar#prepareNavigationBarView為NavigationBar初始化了UI。同時也在這里為按鈕設(shè)置了事件監(jiān)聽器。這里包括我們感興趣的近期任務按鈕的長按事件監(jiān)聽器:
privatevoidprepareNavigationBarView(){? mNavigationBarView.reorient();? ButtonDispatcher recentsButton = mNavigationBarView.getRecentsButton();? recentsButton.setOnClickListener(mRecentsClickListener);? recentsButton.setOnTouchListener(mRecentsPreloadOnTouchListener);? recentsButton.setLongClickable(true);? recentsButton.setOnLongClickListener(mRecentsLongClickListener);? ...}
在mRecentsLongClickListener中,主要的邏輯就是調(diào)用toggleSplitScreenMode。
toggleSplitScreenMode這個方法的名稱很明顯的告訴我們,這里是在切換分屏模式
privateView.OnLongClickListener mRecentsLongClickListener =newView.OnLongClickListener() {@OverridepublicbooleanonLongClick(View v){if(mRecents ==null|| !ActivityManager.supportsMultiWindow()? ? ? ? ? ? ? || !getComponent(Divider.class).getView().getSnapAlgorithm()? ? ? ? ? ? ? ? ? ? ? .isSplitScreenFeasible()) {returnfalse;? ? ? }? ? ? toggleSplitScreenMode(MetricsEvent.ACTION_WINDOW_DOCK_LONGPRESS,? ? ? ? ? ? ? MetricsEvent.ACTION_WINDOW_UNDOCK_LONGPRESS);returntrue;? }};
再順著往下看PhoneStatusBar#toggleSplitScreenMode的代碼:
這里我們看到,通過查詢WindowManagerProxy.getInstance().getDockSide();來確定當前是否處于分屏模式,如果沒有則將Top Task移到Docked的Stack上。這里的Top Task就是我們在長按多任務按鍵之前打開的當前應用。
@OverrideprotectedvoidtoggleSplitScreenMode(intmetricsDockAction,intmetricsUndockAction){if(mRecents ==null) {return;? }intdockSide = 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(newUndockingTaskEvent());if(metricsUndockAction != -1) {? ? ? ? ? MetricsLogger.action(mContext, metricsUndockAction);? ? ? }? }}
之后便會調(diào)用到ActivityManagerService#moveTaskToDockedStack中。后面的大部分邏輯在ActivityStackSupervisor#moveTaskToStackLocked中,在這個方法中,會做如下幾件事情:
通過指定的taskId獲取對應的TaskRecord
為當前Activity替換窗口(因為要從FullScreen的Stack切換的Docked Stack上)
調(diào)用mWindowManager.deferSurfaceLayout通知WindowManagerService暫停布局
將當前TaskRecord移動到Docked Stack上
為移動后的Task和Stack設(shè)置Bounds,并且進行resize。這里還會通知ActivityonMultiWindowModeChanged
調(diào)用mWindowManager.continueSurfaceLayout(); 通知WindowManagerService繼續(xù)開始布局
而Resize和布局就完全是WindowManagerService的事情,這里面需要計算兩個Stack各自的大小,然后根據(jù)大小來對Stack中的Task和Activity窗口進行重新布局。
由于篇幅關(guān)系,這里不再貼出更多的代碼。如果有興趣,請自行獲取AOSP的代碼然后查看。
下圖總結(jié)了啟動分屏模式的執(zhí)行邏輯:
這里需要注意的是:
黃色標記的模式是運行在SystemUI的進程中。
藍色標記的模式是運行在system_server進程中。
moveTaskToDockedStack是一個Binder調(diào)用,通過IPC調(diào)用到了ActivityManagerService。
畫中畫模式
當應用程序調(diào)用Activity#enterPictureInPictureMode便進入了畫中畫模式。
Activity#enterPictureInPictureMode會通過Binder調(diào)用到ActivityManagerService中對應的方法,該方法代碼如下:
publicvoidenterPictureInPictureMode(IBinder token){finallongorigId = Binder.clearCallingIdentity();try{synchronized(this) {if(!mSupportsPictureInPicture) {thrownewIllegalStateException("enterPictureInPictureMode: "+"Device doesn't support picture-in-picture mode.");? ? ? ? ? }finalActivityRecord r = ActivityRecord.forTokenLocked(token);if(r ==null) {thrownewIllegalStateException("enterPictureInPictureMode: "+"Can't find activity for token="+ token);? ? ? ? ? }if(!r.supportsPictureInPicture()) {thrownewIllegalArgumentException("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.finalActivityStack pinnedStack = mStackSupervisor.getStack(PINNED_STACK_ID);finalRect bounds = (pinnedStack !=null)? ? ? ? ? ? ? ? ? ? pinnedStack.mBounds : mDefaultPinnedStackBounds;? ? ? ? ? mStackSupervisor.moveActivityToPinnedStackLocked(? ? ? ? ? ? ? ? ? r,"enterPictureInPictureMode", bounds);? ? ? }? }finally{? ? ? Binder.restoreCallingIdentity(origId);? }}
這里的mStackSupervisor.getStack(PINNED_STACK_ID);是在獲取Pinned Stack,當這個Stack不存在時,會將其創(chuàng)建。 IBinder token是調(diào)用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決定了窗口的上下關(guān)系。 Android N中新增了一個類WindowLayersController來專門負責Z-Order的計算。在計算Z-Order的時候,有幾類窗口會進行特殊處理,處于Pinned Stack上的窗口便是其中之一。
下面這段代碼是在統(tǒng)計哪些窗口是需要特殊處理的,這里可以看到,除了Pinned Stack上的窗口,還有分屏模式下的窗口以及輸入法窗口都需要特殊處理:
privatevoidcollectSpecialWindows(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 TaskStackstack= w.getStack();if(stack== null) {return;? }if(stack.mStackId == PINNED_STACK_ID) {? ? ? mPinnedWindows.add(w);? }elseif(stack.mStackId == DOCKED_STACK_ID) {? ? ? mDockedWindows.add(w);? }}
在統(tǒng)計完這些特殊窗口之后,在計算Z-Order時會對它們進行特殊處理:
privatevoidadjustSpecialWindows(){intlayer = 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()) {finalWindowState 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設(shè)備上打開Freeform模式很簡單,只需以下兩個步驟:
執(zhí)行以下命令:adb shell settings put global enable_freeform_support 1
然后重啟手機:adb reboot
重啟之后,在近期任務界面會出現(xiàn)一個按鈕,這個按鈕可以將窗口切換到Freeform模式,如下圖所示:
這個按鈕的作用其實就是將當前應用移到Freeform Stack上,相關(guān)邏輯在:
ActivityStackSupervisor中,代碼如下:
voidfindTaskToMoveToFrontLocked(TaskRecord task,intflags, ActivityOptions options, String reason,booleanforceNonResizeable){? ...if(task.isResizeable() && options !=null) {intstackId = options.getLaunchStackId();if(canUseActivityOptionsLaunchBounds(options, stackId)) {finalRect bounds = TaskRecord.validateBounds(options.getLaunchBounds());? ? ? ? ? task.updateOverrideConfiguration(bounds);if(stackId == INVALID_STACK_ID) {? ? ? ? ? ? ? stackId = task.getLaunchStackId();? ? ? ? ? }if(stackId != task.stack.mStackId) {finalActivityStack stack = moveTaskToStackUncheckedLocked(? ? ? ? ? ? ? ? ? ? ? task, stackId, ON_TOP, !FORCE_FOCUS, reason);? ? ? ? ? ? ? stackId = stack.mStackId;? ? ? ? ? ? ? ...}
這段代碼通過查詢stackId,然后調(diào)用moveTaskToStackUncheckedLocked移動Task。 而這里通過task.getLaunchStackId()獲取到的stackId,其實就是FREEFORM_WORKSPACE_STACK_ID,相關(guān)代碼如下:
TaskRecord#getLaunchStackId代碼如下:
intgetLaunchStackId(){if(!isApplicationTask()) {returnHOME_STACK_ID;? }if(mBounds !=null) {returnFREEFORM_WORKSPACE_STACK_ID;? }returnFULLSCREEN_WORKSPACE_STACK_ID;}
即,如果設(shè)置了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的時候設(shè)置了Activity的Bounds,相關(guān)代碼如下:
publicstaticvoidlaunchPhoneSize(Context context, Intent intent){? DisplayManager dm = (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);? Display display = dm.getDisplay(Display.DEFAULT_DISPLAY);intwidth1 = display.getWidth() /2;intwidth2 = context.getResources().getDimensionPixelSize(R.dimen.phone_size_width) /2;intheight1 = display.getHeight() /2;intheight2 = context.getResources().getDimensionPixelSize(R.dimen.phone_size_height) /2;try{? ? ? context.startActivity(intent, ActivityOptions.makeBasic().setLaunchBounds(newRect(? ? ? ? ? ? ? width1 - width2,? ? ? ? ? ? ? height1 - height2,? ? ? ? ? ? ? width1 + width2,? ? ? ? ? ? ? height1 + height2? ? ? )).toBundle());? }catch(ActivityNotFoundException e) {/* Gracefully fail */}}
而在ActivityStarter#computeStackFocus中會判斷如果新啟動的Activity設(shè)置了Bounds,
則在FULLSCREEN_WORKSPACE_STACK_ID這個Stack上啟動Activity,相關(guān)代碼如下:
finalintstackId = 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);returnstack;
下圖是啟動Activity時創(chuàng)建Stack的調(diào)用過程:
當你在全屏應用以及Freeform模式應用來回切換的時候,系統(tǒng)所做的其實就是在FullScreen Stack和Freeform Stack上來回切換而已,這和前面提到的虛擬桌面的幾乎是一樣的。
至此,三種多窗口模式就都分析完了,回過頭來再看一下,三種多窗口模式的實現(xiàn)其實都是依賴于Stack結(jié)構(gòu),明白其原理,發(fā)現(xiàn)也沒有特別神秘的地方。
參考資料
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/