本文參考了部分 Android 7.0中的多窗口-分屏-實現(xiàn)解析的內容滔吠。
從Android N(7.0)版本開始阻星,系統(tǒng)支持了多窗口功能盅惜。在有了多窗口支持之后糙捺,用戶可以同時打開和看到多個應用的界面伐割。并且系統(tǒng)還支持在多個應用之間進行拖拽候味。在大屏幕設備上,這一功能非常實用隔心。
本文將詳細探究Android系統(tǒng)中多窗口功能的實現(xiàn)白群。
三種多窗口模式
Android N上的多窗口功能有三種模式:
- 分屏模式
這種模式可以在手機上使用。該模式將屏幕一分為二硬霍,同時顯示兩個應用的界面川抡。 - 畫中畫模式
這種模式主要在TV上使用,在該模式下視頻播放的窗口可以一直在最頂層顯示须尚。 - Freeform模式
這種模式類似于我們常見的桌面操作系統(tǒng)崖堤,應用界面的窗口可以自由拖動和修改大小。
新增屬性
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模式下的最小高度和寬度
分屏模式探究
** 如何啟動分屏模式怜森? **
在Nexus 6P手機上速挑,分屏模式的啟動和退出是長按多任務虛擬按鍵。(打開相應的應用副硅,再長按多任務鍵)
** android:resizeableActivity 如何使用姥宝?**
我們來寫一個小例子來測試 android:resizeableActivity 這個屬性。我們定義兩個 activity恐疲,HelloActivity腊满、MultipleWindowActivity, 定義如下:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.android.helloactivity">
<application android:label="Hello, Activity!"
android:resizeableActivity="true"
>
<activity android:name="HelloActivity"
android:resizeableActivity="false"
>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity android:name="MultipleWindowActivity"
android:resizeableActivity="true"
android:supportsPictureInPicture="true"/>
</application>
</manifest>
我們在Android 7.0 平臺上進行編譯,下面通過表格整理下在application及activity標簽下定義android:resizeableActivity與能否分屏的關系培己。
| NO | Application resizeableActivity | HelloActivity resizeableActivity |HelloActivity Can split window |
| -----| ----- |:-------|: --------|
| 1 | / | / | yes |
| 2 | false | / | no |
| 3 | true | / | yes |
| 4 | false | true | yes |
| 5 | true | true | yes |
| 6 | false | false | no |
| 7 | true | false | no |
| NO | HelloActivity resizeableActivity | MultipleWindowActivity resizeableActivity |MultipleWindow Activity Can split window |
|:-------|:-------|: --------|:--------|:
| 6 | true | false | yes |
| 7 | true | true | yes |
| 8 | false | true | no |
| 9 | false | false | no |
| 10| / | true | yes |
| 11 | / | false | yes |
注1: 結果是 no 的嘗試切到分屏模式會提示“App doesn't support split screen”碳蛋,后面我們將進行分析;
注2:情況6,7雖然能進入分屏模式漱凝,但是會提示“App may not work with split-screen”, 后面我們將進行分析疮蹦;
從表格可以得出下面的結論:
1 沒設置Application resizeableActivity 和 activity resizeableActivity 時诸迟,模式能分屏(跟平臺有關茸炒,7.0默認可以);
2 只設置了Application resizeableActivity 時阵苇,能否分屏受Application resizeableActivity影響壁公;
3 同時設置了Application resizeableActivity 和 activity resizeableActivity 時,能否分屏受activity resizeableActivity影響
4 設置非 main activity 的 resizeableActivity 沒有效果绅项,非 main activity 能否分屏受 main activity 能否分屏影響紊册;
** 初探分屏模式的實現(xiàn) **
我們從不能進入分屏模式時的提示“App doesn't support split screen” 著手,粗略看看分屏模式的實現(xiàn)快耿。搜索字符串囊陡,發(fā)現(xiàn)彈出提示的地方在 SystemUI 模塊的 Recents#dockTopTask 方法中,
如下
409 @Override
410 public boolean dockTopTask(int dragMode, int stackCreateMode, Rect initialBounds,
411 int metricsDockAction) {
....
433 if (runningTask.isDockable) {
....
458 } else {
459 Log.d(TAG, "dockTopTask," + Log.getStackTraceString(new Throwable()));
460 EventBus.getDefault().send(new ShowUserToastEvent(
461 R.string.recents_incompatible_app_message, Toast.LENGTH_SHORT));
462 return false;
463 }
第 459 行是我們加的 log掀亥,打印調用關系撞反,結果如下:
at com.android.systemui.recents.Recents.dockTopTask(Recents.java:459)
at com.android.systemui.statusbar.phone.PhoneStatusBar.toggleSplitScreenMode(PhoneStatusBar.java:1652)
at com.android.systemui.statusbar.BaseStatusBar.toggleSplitScreen(BaseStatusBar.java:1322)
at com.android.systemui.statusbar.CommandQueue$H.handleMessage(CommandQueue.java:519)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:154)
at android.app.ActivityThread.main(ActivityThread.java:6124)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:926)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:788)
大致可以看出參與觸發(fā)分屏模式的有 PhoneStatusBar、Recents等類搪花。而真正執(zhí)行分屏模式的代碼在 runningTask.isDockable 為 true 的代碼塊遏片,后面將進行講解。
畫中畫模式探究
** 如何進入畫中畫模式撮竿?**
當應用程序調用Activity#enterPictureInPictureMode便進入了畫中畫模式吮便。來做個實驗,我們在MultipleWindowActivity里添加一個按鈕幢踏,點擊后調用 enterPictureInPictureMode 方法:
51 public void onClick(View v) {
52 switch (v.getId()) {
53 case R.id.btn:
54 Log.d(TAG, "onclick");
55 enterPictureInPictureMode();
56 break;
57 }
58 }
運行后髓需,出現(xiàn)頁面閃退,log 如下:
E ActivityManager: Activity Manager Crash
E ActivityManager: java.lang.IllegalStateException: enterPictureInPictureMode: Device doesn't support picture-in-picture mode.
E ActivityManager: at com.android.server.am.ActivityManagerService.enterPictureInPictureMode(ActivityManagerService.java:8082)
E ActivityManager: at android.app.ActivityManagerNative.onTransact(ActivityManagerNative.java:2924)
E ActivityManager: at com.android.server.am.ActivityManagerService.onTransact(ActivityManagerService.java:3045)
E ActivityManager: at android.os.Binder.execTransact(Binder.java:565)
E AndroidRuntime: FATAL EXCEPTION: main
E AndroidRuntime: Process: com.example.android.helloactivity, PID: 9965
E AndroidRuntime: java.lang.IllegalStateException: enterPictureInPictureMode: Device doesn't support picture-in-picture mode.
E AndroidRuntime: at android.os.Parcel.readException(Parcel.java:1692)
E AndroidRuntime: at android.os.Parcel.readException(Parcel.java:1637)
E AndroidRuntime: at android.app.ActivityManagerProxy.enterPictureInPictureMode(ActivityManagerNative.java:6986)
E AndroidRuntime: at android.app.Activity.enterPictureInPictureMode(Activity.java:2042)
E AndroidRuntime: at com.example.android.helloactivity.MultipleWindowActivity.onClick(MultipleWindowActivity.java:54)
E AndroidRuntime: at android.view.View.performClick(View.java:5646)
E AndroidRuntime: at android.view.View$PerformClick.run(View.java:22554)
E AndroidRuntime: at android.os.Handler.handleCallback(Handler.java:751)
E AndroidRuntime: at android.os.Handler.dispatchMessage(Handler.java:95)
E AndroidRuntime: at android.os.Looper.loop(Looper.java:154)
E AndroidRuntime: at android.app.ActivityThread.main(ActivityThread.java:6124)
E AndroidRuntime: at java.lang.reflect.Method.invoke(Native Method)
E AndroidRuntime: at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:926)
E AndroidRuntime: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:788)
異常在 ActivityManagerService.java 中被拋出房蝉,代碼如下:
8081 if (!mSupportsPictureInPicture) {
8082 throw new IllegalStateException("enterPictureInPictureMode: "
8083 + "Device doesn't support picture-in-picture mode.");
8084 }
來看看 mSupportsPictureInPicture 的初始化:
13795 final boolean supportsPictureInPicture =
13796 mContext.getPackageManager().hasSystemFeature(FEATURE_PICTURE_IN_PICTURE);
13798 final boolean supportsMultiWindow = ActivityManager.supportsMultiWindow();
13806 final boolean forceResizable = Settings.Global.getInt(
13807 resolver, DEVELOPMENT_FORCE_RESIZABLE_ACTIVITIES, 0) != 0;
13821 synchronized (this) {
......
13829 if (supportsMultiWindow || forceResizable) {
13830 mSupportsMultiWindow = true;
13831 mSupportsFreeformWindowManagement = freeformWindowManagement || forceResizable;
13832 mSupportsPictureInPicture = supportsPictureInPicture || forceResizable;
13833 } else {
13834 mSupportsMultiWindow = false;
13835 mSupportsFreeformWindowManagement = false;
13836 mSupportsPictureInPicture = false;
13837 }
......
}
mSupportsPictureInPicture 由三個變量決定:
** 1 supportsMultiWindow **
/**
* Returns true if the system supports at least one form of multi-window.
* E.g. freeform, split-screen, picture-in-picture.
* @hide
*/
ActivityManager.supportsMultiWindow()
系統(tǒng)至少支持一種多窗口形式時返回 true授账;顯然這里是 true枯跑;
2 forceResizable
Settings.Global.getInt(
resolver, DEVELOPMENT_FORCE_RESIZABLE_ACTIVITIES, 0) != 0;
這是一個 Setting 項,通過搜索發(fā)現(xiàn)它與“開發(fā)者選項”的“Force activities to be resizable”對應白热。
3 supportsPictureInPicture
/**
* Check whether the given feature name is one of the available features as
* returned by {@link #getSystemAvailableFeatures()}. This tests for the
* presence of <em>any</em> version of the given feature name; use
* {@link #hasSystemFeature(String, int)} to check for a minimum version.
*
* @return Returns true if the devices supports the feature, else false.
*/
mContext.getPackageManager().hasSystemFeature(FEATURE_PICTURE_IN_PICTURE);
這是一個系統(tǒng)特性敛助,我們可以通過下面的方法打印系統(tǒng)支持的特性:
FeatureInfo[] features = getPackageManager().getSystemAvailableFeatures();
for (FeatureInfo info : features) {
Log.d(TAG, "Name=" + info.name);
}
可以推測出 supportsMultiWindow 為 true, forceResizable 和 supportsPictureInPicture 都為 false屋确; 顯然我們不太容易控制 supportsPictureInPicture 這個系統(tǒng)特性纳击,但是 forceResizable 可以控制。我們打開“開發(fā)者選項”的“Force activities to be resizable”選項試一下攻臀,打開后發(fā)現(xiàn)還是崩潰焕数,但是 DEVELOPMENT_FORCE_RESIZABLE_ACTIVITIES 值確實變化了,為什么不行呢?通過分析代碼刨啸,我們發(fā)現(xiàn) mSupportsPictureInPicture 的初始化時機比較早堡赔,雖然我們改變了 DEVELOPMENT_FORCE_RESIZABLE_ACTIVITIES 的值,但是并沒有走到改變 mSupportsPictureInPicture 的邏輯设联,所以善已,把 DEVELOPMENT_FORCE_RESIZABLE_ACTIVITIES 開關打開后,我們重啟一下手機試試离例。果然重啟后不會發(fā)生崩潰了换团。
我們來看看畫中畫模式的效果。進入時頁面會縮小到屏幕左上角宫蛆,變成一個小黑塊艘包,這顯然不是我們想要的。我們需要能控制縮小后的大小耀盗,以及監(jiān)聽縮小的動作以做邏輯切換想虎。如何實現(xiàn)呢?這個問題我們后面再去研究叛拷。
Freeform 模式探究
[TODO]