本文参考了部分 Android 7.0中的多窗口-分屏-实现解析的内容。
从Android N(7.0)版本开始,系统支持了多窗口功能。在有了多窗口支持之后,用户可以同时打开和看到多个应用的界面。并且系统还支持在多个应用之间进行拖拽。在大屏幕设备上,这一功能非常实用。
本文将详细探究Android系统中多窗口功能的实现。
三种多窗口模式
Android N上的多窗口功能有三种模式:
- 分屏模式
这种模式可以在手机上使用。该模式将屏幕一分为二,同时显示两个应用的界面。 - 画中画模式
这种模式主要在TV上使用,在该模式下视频播放的窗口可以一直在最顶层显示。 - Freeform模式
这种模式类似于我们常见的桌面操作系统,应用界面的窗口可以自由拖动和修改大小。
新增属性
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 能否分屏影响;
** 初探分屏模式的实现 **
我们从不能进入分屏模式时的提示“App doesn't support split screen” 着手,粗略看看分屏模式的实现。搜索字符串,发现弹出提示的地方在 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)
大致可以看出参与触发分屏模式的有 PhoneStatusBar、Recents等类。而真正执行分屏模式的代码在 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 }
运行后,出现页面闪退,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()
系统至少支持一种多窗口形式时返回 true;显然这里是 true;
2 forceResizable
Settings.Global.getInt(
resolver, DEVELOPMENT_FORCE_RESIZABLE_ACTIVITIES, 0) != 0;
这是一个 Setting 项,通过搜索发现它与“开发者选项”的“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);
这是一个系统特性,我们可以通过下面的方法打印系统支持的特性:
FeatureInfo[] features = getPackageManager().getSystemAvailableFeatures();
for (FeatureInfo info : features) {
Log.d(TAG, "Name=" + info.name);
}
可以推测出 supportsMultiWindow 为 true, forceResizable 和 supportsPictureInPicture 都为 false; 显然我们不太容易控制 supportsPictureInPicture 这个系统特性,但是 forceResizable 可以控制。我们打开“开发者选项”的“Force activities to be resizable”选项试一下,打开后发现还是崩溃,但是 DEVELOPMENT_FORCE_RESIZABLE_ACTIVITIES 值确实变化了,为什么不行呢?通过分析代码,我们发现 mSupportsPictureInPicture 的初始化时机比较早,虽然我们改变了 DEVELOPMENT_FORCE_RESIZABLE_ACTIVITIES 的值,但是并没有走到改变 mSupportsPictureInPicture 的逻辑,所以,把 DEVELOPMENT_FORCE_RESIZABLE_ACTIVITIES 开关打开后,我们重启一下手机试试。果然重启后不会发生崩溃了。
我们来看看画中画模式的效果。进入时页面会缩小到屏幕左上角,变成一个小黑块,这显然不是我们想要的。我们需要能控制缩小后的大小,以及监听缩小的动作以做逻辑切换。如何实现呢?这个问题我们后面再去研究。
Freeform 模式探究
[TODO]