Android 8.0 PictureInPicture 画中画模式分析与使用
之前的直播业务中有退出直播后显示一个小窗口继续播放视频直播需求,当时是用的windowManger做的windowManger实现连接,今天来了解一下Android8.0后的画中画怎么实现,先看效果图
先看一下谷歌文档的介绍
多窗口支持
Android 7.0 新增了对同时显示多个应用窗口的支持。在手持设备上,两个应用可以在分屏模式下左右并排或上下并排显示。在电视设备上,应用可以使用画中画模式,在用户与另一个应用互动的同时继续播放视频。
如果您的应用以 Android 7.0(API 级别 24)或更高版本为目标平台,则您可以配置应用处理多窗口显示的方式。例如,您可以指定 Activity 允许的*小尺寸。您还可以为自己的应用停用多窗口显示,确保系统仅以全屏模式显示该应用。
概览
Android 允许多个应用同时共享屏幕。例如,用户可以分屏显示应用,在左边查看网页,同时在右边写电子邮件。用户体验取决于 Android 操作系统的版本和设备类型:
- 搭载 Android 7.0 的手持设备支持分屏模式。在此模式下,系统以左右并排或上下并排的方式分屏显示两个应用。用户可以拖动两个应用之间的分界线,放大其中一个应用,同时缩小另一个应用。
- 从 Android 8.0 开始,应用可以将自身置于画中画模式,从而使其能在用户浏览其他应用或与之互动时继续显示内容。
- 更大尺寸设备的制造商可选择启用自由窗口模式,在该模式下,用户可以自由调整各 Activity 的大小。如果制造商启用此功能,设备将同时具有自由窗口模式和分屏模式。
用户可以通过以下方式切换到多窗口模式:
- 如果用户打开概览屏幕并长按 Activity 名称,则可以拖动该 Activity 至屏幕突出显示的区域,使 Activity 进入多窗口模式。
- 如果用户长按“概览”按钮,设备上的当前 Activity 将进入多窗口模式,同时将打开概览屏幕,供用户选择要共享屏幕的另一个 Activity。
当两个 activity 共享屏幕时,用户可在二者之间拖放数据。
多窗口生命周期
多窗口模式不会更改 activity 生命周期。
在多窗口模式下,在任意时间点都只有用户*近一次互动的 Activity 处于活动状态。此 Activity 被视为*顶层的 Activity,而且是唯一处于 RESUMED
状态的 Activity。所有其他可见的 Activity 均处于 STARTED
而非 RESUMED
状态。不过,这些可见但并不处于 RESUMED 状态的 Activity 在系统中享有比不可见 Activity 更高的优先级。如果用户与其中一个可见的 Activity 互动,那么该 Activity 将进入 RESUMED 状态,而之前的*顶层 Activity 将进入 STARTED
状态。
注意:在多窗口模式下,即使应用对用户可见,也可能不处于 RESUMED
状态。尽管不在*顶层,应用可能也需要继续执行其操作。例如,处于此状态的视频播放应用应继续显示其视频。出于此原因,我们建议播放视频的 Activity 不要通过暂停视频播放来响应 ON_PAUSE
生命周期事件。相反,此类 activity 应通过开始播放来响应 ON_START
,通过暂停播放来响应 ON_STOP
。如果您未使用生命周期软件包,而是直接处理生命周期事件,请在您的 onStop()
处理程序中暂停视频播放,在 onStart()
中继续视频播放。
如处理配置更改中所述,当用户将应用置于多窗口模式下时,系统会通知 activity 发生的配置更改。当用户调整应用大小或将应用恢复到全屏模式时,系统也会通知发生了配置更改。该更改与系统通知应用设备已从纵向模式切换到横屏模式时的 Activity 生命周期影响基本相同,区别在于设备不仅仅是切换了方向,还更改了尺寸。如处理配置更改中所述,您的 Activity 可以自行处理配置更改,或允许系统销毁 Activity,并以新的尺寸重新创建该 Activity。
如果用户调整窗口大小并在任意维度放大窗口,那么系统会根据用户操作调整 activity 的大小,同时根据需要发出配置更改通知。如果应用在新公开区域的绘制滞后,系统会使用 windowBackground
属性或默认 windowBackgroundFallback
样式属性指定的颜色临时填充这些区域。
当应用处于多窗口模式时,恢复状态取决于设备的 Android 版本。
- 在 Android 9(API 级别 28)及更低版本中,只有获得焦点的 activity 处于
RESUMED
状态,所有其他 activity 均为PAUSED
状态。如果一个应用进程中有多个 activity,则 Z 轴顺序*高的 activity 为RESUMED
状态,其他 activity 为PAUSED
状态。 - 在 Android 10(API 级别 29)及更高版本中,所有顶层的可聚焦 activity 均处于
RESUMED
状态。
如需了解详情,请参阅多项恢复文档。
针对多窗口模式配置应用
如果您的应用以 API 级别 24 或更高级别为目标平台,那么您可以配置该应用的 activity 是否支持以及如何支持多窗口显示。您可以在清单中设置属性来控制大小和布局。根 Activity 的属性设置会应用于其任务堆栈中的所有 Activity。例如,如果根 Activity 将 android:resizeableActivity
设置为 true,那么任务堆栈中的所有 Activity 均可调整大小。
注意:如果您构建以 API 级别 23 或更低级别为目标平台、支持多个屏幕方向的应用,那么当用户在多窗口模式下使用应用时,系统会强制调整应用大小。系统会显示一个对话框,提醒用户应用可能会出现意外行为。系统不会调整定向应用的大小;如果用户尝试在多窗口模式下打开一个定向应用,该应用将全屏显示。
在 Chromebook 等一些较大的设备上,即使指定了 android:resizeableActivity=”false”
,您的应用也可能会在可调整大小的窗口中运行。如果这会破坏您的应用,您可以使用过滤条件来限制您的应用在此类设备上的可用性。
android:resizeableActivity
在清单的 <activity>
或 <application>
元素中设置此属性,以启用或停用多窗口显示:
android:resizeableActivity=["true" | "false"]
如果将此属性设置为 true,则·Activity 能以分屏和自由窗口模式启动。如果将此属性设置为 false,则 Activity 不支持多窗口模式。如果此值为 false,并且用户尝试在多窗口模式下启动 Activity,则 Activity 会全屏显示。
如果您的应用以 API 级别 24 为目标平台,但您未指定此属性的值,则其值默认设为 true。
android:supportsPictureInPicture
在清单的 <activity>
节点中设置此属性,以指示 Activity 是否支持画中画显示。如果 android:resizeableActivity
为 false,系统将忽略此属性。
android:supportsPictureInPicture=["true" | "false"]
布局属性
在 Android 7.0 中,<layout>
清单元素支持以下几种属性,这些属性会影响 Activity 在多窗口模式下的行为:
android:defaultWidth
- 在自由窗口模式下启动时 Activity 的默认宽度。
android:defaultHeight
- 在自由窗口模式下启动时 Activity 的默认高度。
android:gravity
- 在自由窗口模式下启动时 activity 的初始位置。请参阅
Gravity
参考资料,了解合适的值设置。 android:minHeight
、android:minWidth
- 分屏和自由窗口模式下 Activity 的*小高度和*小宽度。如果用户在分屏模式下移动分界线,使 Activity 尺寸小于指定的*小值,则系统会将 Activity 裁剪为用户请求的尺寸。
例如,以下代码展示了如何指定 Activity 在自由窗口模式下显示时的默认大小、位置和*小尺寸:
<activity android:name=".MyActivity"> <layout android:defaultHeight="500dp" android:defaultWidth="600dp" android:gravity="top|end" android:minHeight="450dp" android:minWidth="300dp" /> </activity>
正确处理配置更改
如果您自行处理多窗口配置更改(例如当用户调整窗口大小时会怎样),请将至少指定以下值的 android:configChanges
属性添加到清单中:
<activity android:name=".MyActivity" android:configChanges="screenSize|smallestScreenSize |screenLayout|orientation" />
添加 android:configChanges
后,您的 Activity 和 Fragment 会收到对 onConfigurationChanged()
的回调,而不是被销毁并重新创建。然后,您可以根据需要手动更新视图、重新加载资源以及执行其他操作。
在多窗口模式下运行应用
从 Android 7.0 开始,系统会为应用提供支持其在多窗口模式下运行的功能。
多窗口模式下被停用的功能
当设备处于多窗口模式时,系统会停用或忽略某些功能,因为对于可能与其他 Activity 或应用共享设备屏幕的 Activity 而言,这些功能并没有任何意义。此类功能包括:
- 某些系统界面自定义选项将被停用;例如,在非全屏模式下,应用无法隐藏状态栏。
- 系统将忽略对
android:screenOrientation
属性所作的更改。
多窗口更改通知和查询
Activity
提供以下方法来支持多窗口显示。
isInMultiWindowMode()
- 调用该方法可确认 activity 是否处于多窗口模式。
isInPictureInPictureMode()
- 调用该方法可确认 Activity 是否处于画中画模式。
注意:画中画模式是多窗口模式的特例。如果
myActivity.isInPictureInPictureMode()
返回 true,那么myActivity.isInMultiWindowMode()
也会返回 true。 onMultiWindowModeChanged()
- 每当 Activity 进入或退出多窗口模式时,系统都会调用此方法。当 Activity 进入多窗口模式时,系统会向该方法传递 true 值,退出多窗口模式时则传递 false 值。
onPictureInPictureModeChanged()
- 每当 Activity 进入或退出画中画模式时,系统都会调用此方法。当 activity 进入画中画模式时,系统会向该方法传递 true 值,退出画中画模式时则传递 false 值。
对于以上的很多方法,Fragment
类公开了多个版本,例如 Fragment.onMultiWindowModeChanged()
。
进入画中画模式
如要将 Activity 置于画中画模式,请调用 Activity.enterPictureInPictureMode()
。如果设备不支持画中画模式,则调用此方法不会产生任何影响。如需了解详细信息,请参阅画中画文档。
在多窗口模式下启动新 activity
启动新 activity 时,您可以指示应尽可能将新 activity 显示在当前 activity 旁边。为此,请使用 Intent 标志 FLAG_ACTIVITY_LAUNCH_ADJACENT
。此标志将告知系统尽量在启动它的 Activity 旁边创建新 Activity,以便两个 Activity 共享屏幕。系统会尽可能这些做,但*终结果无法保证。
如果设备处于自由窗口模式,则在启动新 Activity 时,您可通过调用 ActivityOptions.setLaunchBounds()
来指定新 Activity 的尺寸和屏幕位置。如果设备未处于多窗口模式,则调用该方法不会产生任何影响。
注意:如果您在任务堆栈中启动 Activity,该 Activity 会替换屏幕上的 Activity,并沿用其所有的多窗口模式属性。如果您要在多窗口模式下以单独的窗口启动新 activity,那么必须在新的任务堆栈中启动此 activity。
支持拖放
当两个 activity 共享屏幕时,用户可在二者之间拖放数据。(在 Android 7.0 之前,用户只能在一个 activity 内拖放数据。)如需快速添加对在可编辑的 TextView
微件中接收拖动内容的支持,请参阅 Jetpack 中的 OnReceiveContentListener
。如需添加全面的拖放支持(例如能够从您的应用拖动内容),请参阅拖放主题。
DragAndDropPermissions
- 令牌对象,负责指定向接收拖放数据的应用授予的权限。
View.startDragAndDrop()
- 等同于
View.startDrag()
。如要启用跨 activity 拖放,请传递DRAG_FLAG_GLOBAL
标志。如需向接收拖放数据的 activity 授予 URI 权限,请根据情况传递DRAG_FLAG_GLOBAL_URI_READ
或DRAG_FLAG_GLOBAL_URI_WRITE
标志。 View.cancelDragAndDrop()
- 取消当前正在进行的拖动操作。只能由发起拖动操作的应用调用。
View.updateDragShadow()
- 替换当前正在进行的拖动操作的拖动阴影。只能由发起拖动操作的应用调用。
Activity.requestDragAndDropPermissions()
- 请求权限,以获取通过
DragEvent
中所含ClipData
传递的内容 URI。 DragAndDropPermissions.release()
- 释放访问
ClipData
中提供的内容 URI 上的数据所需的权限。 如果您不调用此方法,则在系统销毁包含的 activity 时会自动释放权限。
多实例
每个根 activity 都有自己的任务,该任务在单独的进程中运行,并显示在其自己的窗口中。如需在单独的窗口中启动应用的新实例,可使用 FLAG_ACTIVITY_NEW_TASK
标志启动新 activity。您可以将此标志与某些多窗口模式属性结合使用,以请求用于新窗口的特定位置。例如,购物应用可以显示多个窗口来比较商品。
请不要将多实例与多面板布局混淆,例如使用 SlidingPaneLayout
的列表/详情布局。它在单个窗口内运行。
请注意,当多个实例在可折叠设备上的单独窗口中运行时,如果折叠状态发生更改,则一个或多个实例可能会发送到后台。例如,假设一台设备已展开,并且有两个应用实例在折叠任一侧的两个窗口中运行。如果设备处于折叠状态,系统可能会终止其中的一个实例,而不是在较小的屏幕上尝试使窗口适应两个实例。
测试应用的多窗口支持
无论您的应用是否以 API 级别 24 或更高级别为目标平台,您都应验证应用在多窗口模式下的行为,以防用户尝试在搭载 Android 7.0 或更高版本的设备上以多窗口模式启动应用。
配置测试设备
如果设备搭载 Android 7.0 或更高版本,将自动支持分屏模式。
如果您的应用以 API 级别 23 或更低级别为目标平台
如果您的应用以 API 级别 23 或更低级别为目标平台,那么当用户尝试在多窗口模式下使用应用时,系统将强制调整应用大小(除非应用进行定向声明)。
如果您的应用未进行定向声明,您应在搭载 Android 7.0 或更高版本的设备上启动应用,并尝试将应用置于分屏模式。确认应用被强制调整大小后,能提供可接受的用户体验。
如果您的应用进行定向声明,您应尝试将应用置于多窗口模式。确认执行此操作后,应用仍保持全屏模式。
如果支持多窗口模式
如果您的应用以 API 级别 24 或更高级别为目标平台,并且未停用多窗口支持,请在分屏模式和自由窗口模式下验证以下行为。
- 在全屏模式下启动应用,然后通过长按“概览”按钮切换到多窗口模式。确认应用能正常切换。
- 直接在多窗口模式下启动应用,确认应用能正常启动。您可以按一下“概览”按钮,然后长按应用的名称栏,并将其拖动到屏幕上任一突出显示的区域,从而在多窗口模式下启动应用。
- 拖动分界线,调整应用在分屏模式下的大小。确认应用正常调整大小且未崩溃,并且必要的界面元素仍然可见。
- 如果您已指定应用的*小尺寸,请尝试将应用尺寸调整到*小值以下。确认无法将应用尺寸调整到指定*小值以下。
- 完成所有测试后,确认应用性能可以接受。例如,确认调整应用大小后,更新界面没有长时间的滞后。
测试核对清单
如要验证应用在多窗口模式下的性能,请执行以下操作。除非另有说明,否则应分别在分屏模式和多窗口模式下执行以下操作。
- 进入和退出多窗口模式。
- 从您的应用切换至另一个应用,确认应用在非活跃但可见的状态下正常运行。例如,如果您的应用正在播放视频,请确认当用户与另一个应用互动时,视频仍在继续播放。
- 在分屏模式下,尝试移动分界线,放大和缩小您的应用。在左右并排和上下并排配置下,都要尝试放大和缩小操作。确认应用不会崩溃,主要功能可见,且调整大小的操作无需过长时间。
- 快速连续地执行几次调整大小的操作。确认应用不会崩溃或出现内存泄露。如需了解如何查看应用的内存用量,请使用 Android Studio 的内存性能分析器。
- 在多种不同的窗口配置下正常使用应用,确认应用能正常运行。确认文本可读,且界面元素大小正常,不影响互动。
如果已停用多窗口支持
如果您已通过设置 android:resizeableActivity="false"
停用多窗口支持,则应在运行 Android 7.0 或更高版本的设备上启动应用,并尝试将应用置于自由窗口模式和分屏模式。确认执行此操作后,应用仍保持全屏模式。
1,在清单文件AndroidManifest中声名允许开启画中画模式
android:resizeableActivity=”true” android:supportsPictureInPicture=”true”
<activity android:name=”.TestActivity”
android:configChanges=”screenSize|smallestScreenSize|screenLayout|orientation”
android:resizeableActivity=”true”
android:hardwareAccelerated=”true”
android:supportsPictureInPicture=”true”>
<intent-filter>
<action android:name=”android.intent.action.MAIN” />
<category android:name=”android.intent.category.LAUNCHER” />
</intent-filter>
</activity>
2.在activity中调用enterPictureInPictureMode()方法
/** Enters Picture-in-Picture mode. 进入画中画模式*/
void minimize() {
if (mMovieView == null) {
return;
}
// Hide the controls in picture-in-picture mode. 在画中画模式中 隐藏控制条
mMovieView.hideControls();
// Calculate the aspect ratio of the PiP screen. 计算屏幕的纵横比
Rational aspectRatio = new Rational(mMovieView.getWidth(), mMovieView.getHeight());
mPictureInPictureParamsBuilder.setAspectRatio(aspectRatio).build();
enterPictureInPictureMode(mPictureInPictureParamsBuilder.build());
}
该方法需要传PictureInPictureParams 参数,主要用于确定我们activity需要作为画中画的宽高比,我们可以将宽高设置为videoView的宽高,这样宽高比就与视频画面一致,进入画中画前隐藏其他UI控件就行了
3,在onPictureInPictureModeChanged方法中处理画中画切换的回调
@Override
public void onPictureInPictureModeChanged(
boolean isInPictureInPictureMode, Configuration configuration) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode, configuration);
if (isInPictureInPictureMode) {
// Starts receiving events from action items in PiP mode.
//开始接收画中画模式的操作
mReceiver =
new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (intent == null
|| !ACTION_MEDIA_CONTROL.equals(intent.getAction())) {
return;
}
// This is where we are called back from Picture-in-Picture action items.
//这就是我们从画中画模式的操作回调的地方
final int controlType = intent.getIntExtra(EXTRA_CONTROL_TYPE, 0);
switch (controlType) {
case CONTROL_TYPE_PLAY:
mMovieView.play();
break;
case CONTROL_TYPE_PAUSE:
mMovieView.pause();
break;
}
}
};
registerReceiver(mReceiver, new IntentFilter(ACTION_MEDIA_CONTROL));
} else {
// We are out of PiP mode. We can stop receiving events from it.
// 当我们不在画中画模式时,停止接收广播
unregisterReceiver(mReceiver);
mReceiver = null;
// Show the video controls if the video is not playing
//当视频不在播放时,显示控制条
if (mMovieView != null && !mMovieView.isPlaying()) {
mMovieView.showControls();
}
}
}
然后手势移动,关闭画中画,画中画切换回原页面等操作谷歌已经替我们做好了
需要注意画中画模式只有在Android8.0及以后才有,低版本实现画中画还是需要利用windowManger 通过addview去做
————————————————