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 的大小。如果制造商启用此功能,设备将同时具有自由窗口模式和分屏模式。

%title插图%num

图 1. 两个应用在分屏模式下左右并排显示。

用户可以通过以下方式切换到多窗口模式:

  • 如果用户打开概览屏幕并长按 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”,您的应用也可能会在可调整大小的窗口中运行。如果这会破坏您的应用,您可以使用过滤条件来限制您的应用在此类设备上的可用性。

在清单的 <activity> 或 <application> 元素中设置此属性,以启用或停用多窗口显示:

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

如果将此属性设置为 true,则·Activity 能以分屏和自由窗口模式启动。如果将此属性设置为 false,则 Activity 不支持多窗口模式。如果此值为 false,并且用户尝试在多窗口模式下启动 Activity,则 Activity 会全屏显示。

如果您的应用以 API 级别 24 为目标平台,但您未指定此属性的值,则其值默认设为 true。

在清单的 <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:minHeightandroid: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 旁边。为此,请使用 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 或更低级别为目标平台,那么当用户尝试在多窗口模式下使用应用时,系统将强制调整应用大小(除非应用进行定向声明)。

如果您的应用未进行定向声明,您应在搭载 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去做
————————————————

Android允许以画中画 (PIP) 模式启动 Activity

Android 8.0(API 级别 26)允许以画中画 (PIP) 模式启动 Activity。画中画是一种特殊类型的多窗口模式,*常用于视频播放。使用该模式,用户可以通过固定到屏幕一角的小窗口观看视频,同时在应用之间进行导航或浏览主屏幕上的内容。

%title插图%num

画中画利用 Android 7.0 中的多窗口模式 API 来提供固定的视频叠加窗口。如要将画中画添加到您的应用中,您需要注册支持画中画的 Activity,根据需要将 Activity 切换为画中画模式,并确保当 Activity 处于画中画模式时,界面元素处于隐藏状态且视频能够继续播放。

画中画窗口会显示在屏幕的*上层,位于系统选择的一角。您可以将画中画窗口拖动到其他位置。当您点按该窗口时,会看到两个特殊的控件:全屏切换开关(位于窗口的中心)和关闭按钮(右上角的“X”)。

您的应用会控制当前 Activity 在何时进入画中画模式。下面是一些示例:

  • 一个 Activity 可以在用户点按主屏幕或*近使用的应用按钮来选择其他应用时,进入画中画模式。(Google 地图就是通过这种方式,在用户同时运行其他 Activity 时继续显示路线。)
  • 您的应用可以在用户从某个视频返回以浏览其他内容时,将该视频切换到画中画模式。
  • 您的应用可以在用户观看到某集内容结束时将视频切换到画中画模式。主屏幕会显示有关这部电视剧下一集的宣传信息或剧情摘要信息。
  • 您的应用可以提供一种方式,让用户可以在观看视频时将其他内容加入播放队列。当主屏幕显示内容选择 Activity 时,视频会继续以画中画模式播放。

默认情况下,系统不会自动为应用提供画中画支持。如果您想在应用中支持画中画,可以通过将 android:supportsPictureInPicture 设置为 true,在清单中注册视频 Activity。此外,指定您的 Activity 处理布局配置更改,这样一来,在画中画模式转换期间发生布局更改时,您的 Activity 就不会重新启动。

<activity android:name="VideoActivity"
    android:supportsPictureInPicture="true"
    android:configChanges=
        "screenSize|smallestScreenSize|screenLayout|orientation"
    ...

如要进入画中画模式,Activity 必须调用 enterPictureInPictureMode()。例如,以下代码会在用户点击应用界面中的专用按钮时,将 Activity 切换到画中画模式:

KotlinJava
@Override
public void onActionClicked(Action action) {
    if (action.getId() == R.id.lb_control_picture_in_picture) {
        getActivity().enterPictureInPictureMode();
        return;
    }
    ...
}

您可能需要添加将 Activity 切换到画中画模式(而不是进入后台)的逻辑。例如,如果用户在 Google 地图正在导航时按下主屏幕或*近使用的应用按钮,则该应用会切换到画中画模式。您可以通过替换 onUserLeaveHint() 来实现这一目的:

KotlinJava
@Override
public void onUserLeaveHint () {
    if (iWantToBeInPipModeNow()) {
        enterPictureInPictureMode();
    }
}

当 Activity 进入或退出画中画模式时,系统会调用 Activity.onPictureInPictureModeChanged() 或 Fragment.onPictureInPictureModeChanged()

您应替换这些回调以重新绘制 Activity 的界面元素。请注意,在画中画模式下,您的 Activity 会在一个小窗口中显示。在画中画模式下,用户无法与界面元素互动,并且可能很难看清小界面元素的详细信息。界面*简的视频播放 Activity 可提供*佳的用户体验。Activity 应仅显示视频播放控件。在 Activity 进入画中画模式之前移除其他界面元素,并在 Activity 再次变为全屏时恢复这些元素:

KotlinJava
@Override
public void onPictureInPictureModeChanged (boolean isInPictureInPictureMode, Configuration newConfig) {
    if (isInPictureInPictureMode) {
        // Hide the full-screen UI (controls, etc.) while in picture-in-picture mode.
    } else {
        // Restore the full-screen UI.
        ...
    }
}

画中画窗口可以在用户打开窗口菜单(通过点按移动设备上的窗口或使用电视遥控器选择菜单)时显示控件。

如果应用有处于活跃状态的媒体会话,则窗口会显示播放、暂停、下一个和上一个控件。

您还可以通过在进入画中画模式之前构建 PictureInPictureParams(使用 PictureInPictureParams.Builder.setActions())来明确指定自定义操作,并使用 enterPictureInPictureMode(android.app.PictureInPictureParams) 或 setPictureInPictureParams(android.app.PictureInPictureParams) 在进入画中画模式时传递这些参数。请注意,如果您尝试添加的控件数量超过 getMaxNumPictureInPictureActions(),则系统只会添加上限数量的控件。

当您的 Activity 切换到画中画模式时,系统会将该 Activity 置于暂停状态并调用 Activity 的 onPause() 方法。当 Activity 在画中画模式下暂停时,视频播放不得暂停,而应继续播放。

在 Android 7.0 及更高版本中,当系统调用 Activity 的 onStop() 时,您应暂停视频播放;当系统调用 Activity 的 onStart() 时,您应恢复视频播放。这样一来,您就无需在 onPause() 中检查应用是否处于画中画模式,只需继续播放视频即可。

如果您必须在 onPause() 实现中暂停播放,请通过调用 isInPictureInPictureMode() 检查是否处于画中画模式并相应地处理播放情况,例如:

KotlinJava
@Override
public void onPause() {
    // If called while in PIP mode, do not pause playback
    if (isInPictureInPictureMode()) {
        // Continue playback
        ...
    } else {
        // Use existing playback logic for paused Activity behavior.
        ...
    }
}

当您的 Activity 从画中画模式切换回全屏模式时,系统会恢复您的 Activity 并调用 onResume() 方法。

在您的应用中,可能会出现以下情况:有一个视频播放 Activity 正处于画中画模式,用户在主屏幕上浏览内容时选择了新的视频。应以全屏模式在现有的播放 Activity 中播放新的视频,而不是启动可能会令用户感到困惑的新 Activity。

如要确保将单个 Activity 用于视频播放请求并根据需要进入或退出画中画模式,请在清单中将 Activity 的 android:launchMode 设置为 singleTask

<activity android:name="VideoActivity"
    ...
    android:supportsPictureInPicture="true"
    android:launchMode="singleTask"
    ...

在您的 Activity 中,替换 onNewIntent() 并处理新的视频,从而根据需要停止任何现有的视频播放。

低内存设备可能无法使用画中画模式。在应用使用画中画之前,请务必通过调用 hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) 进行检查以确保可以使用画中画。

画中画旨在用于播放全屏视频的 Activity。将 Activity 切换到画中画模式时,请避免显示视频内容以外的任何内容。跟踪您的 Activity 何时进入画中画模式并隐藏界面元素,如处理画中画模式下的界面元素中所述。

由于画中画窗口在屏幕的一角显示为浮动窗口,因此您应避免在主屏幕中可能被画中画窗口遮盖的任何区域内显示重要信息。

当 Activity 进入画中画模式后,默认不会获得输入焦点。要在画中画模式下接收输入事件,请使用 MediaSession.setCallback()。如需详细了解如何使用 setCallback(),请参阅显示“正在播放”卡片。

当您的应用处于画中画模式时,画中画窗口中的视频播放可能会对其他应用(例如,音乐播放器应用或语音搜索应用)造成音频干扰。为避免出现此问题,请在开始播放视频时请求音频焦点,并处理音频焦点更改通知,如管理音频焦点中所述。如果您在处于画中画模式时收到音频焦点丢失通知,请暂停或停止视频播放。

当您的应用即将进入画中画模式时,请注意,只有顶层 Activity 才会进入画中画模式。在某些情况下(例如在多窗口设备上),此时系统可能会显示下层 Activity,在画中画 Activity 旁边,您可能会再次看到下层 Activity。您应根据情况相应地处理这一问题,包括下层 Activity 获取 onResume()或 onPause() 回调。用户也有可能与该 Activity 进行交互。例如,如果您的视频列表 Activity 正在显示,视频播放 Activity 处于画中画模式,用户可能会从列表中选择新视频,画中画 Activity 应相应地进行更新。

289. 生命游戏(JS实现)

289. 生命游戏(JS实现)
1 题目
根据 百度百科 ,生命游戏,简称为生命,是英国数学家约翰·何顿·康威在 1970 年发明的细胞自动机。
给定一个包含 m × n 个格子的面板,每一个格子都可以看成是一个细胞。每个细胞都具有一个初始状态:1 即为活细胞(live),或 0 即为死细胞(dead)。每个细胞与其八个相邻位置(水平,垂直,对角线)的细胞都遵循以下四条生存定律:
如果活细胞周围八个位置的活细胞数少于两个,则该位置活细胞死亡;
如果活细胞周围八个位置有两个或三个活细胞,则该位置活细胞仍然存活;
如果活细胞周围八个位置有超过三个活细胞,则该位置活细胞死亡;
如果死细胞周围正好有三个活细胞,则该位置死细胞复活;
根据当前状态,写一个函数来计算面板上所有细胞的下一个(一次更新后的)状态。下一个状态是通过将上述规则同时应用于当前状态下的每个细胞所形成的,其中细胞的出生和死亡是同时发生的。
示例:
输入:
[
[0,1,0],
[0,0,1],
[1,1,1],
[0,0,0]
]
输出:
[
[0,0,0],
[1,0,1],
[0,1,1],
[0,1,0]
]
进阶:
你可以使用原地算法解决本题吗?请注意,面板上所有格子需要同时被更新:你不能先更新某些格子,然后使用它们的更新后的值再更新其他格子。
本题中,我们使用二维数组来表示面板。原则上,面板是无限的,但当活细胞侵占了面板边界时会造成问题。你将如何解决这些问题?
链接:https://leetcode-cn.com/problems/game-of-life
2 思路
首先我们遍历整个矩阵,如果某细胞是活的,那么给周围8个位置细胞都加上影响因子10,然后我们第二次遍历矩阵,根据每个细胞的影响因子数来更新细胞的状态
3代码
/**
 * @param {number[][]} board
 * @return {void} Do not return anything, modify board in-place instead.
 */
var gameOfLife = function(board) {
    let m = board.length;
    let n = board[0].length;
    function affect(i,j) {
        if (board[i][j] % 10 === 0) return;
        for (let x=i-1; x<=i+1; x++) {
            for (let y=j-1; y<=j+1; y++) {
                if (x === i && y === j || x < 0 || y < 0 || x >= m || y >= n) continue;
                board[x][y] += 10;
            }
        }
    }
    function caculte(i,j) {
        let num = board[i][j];
        let k = Math.floor(num / 10);
        if (num % 10 === 0) {
            if ( k === 3) {
                board[i][j] = 1;
            } else {
                board[i][j] = 0;
            }
        } else {
            if (k < 2 || k > 3) {
                board[i][j] = 0;
            } else {
                board[i][j] = 1;
            }
        }
    }
    for (let i=0; i<m; i++) {
        for (let j=0; j<n; j++) {
            affect(i,j);
        }
    }
    for (let i=0; i<m; i++) {
        for (let j=0; j<n; j++) {
            caculte(i,j);
        }
    }
};

总结系列-Android画中画模式

*近做做播放器,有个浮窗播放的需求,两种实现方式,一种是申请浮窗权限,创建浮窗参考 flowWindow,一种是采用画中画模式(8.0以上)

关于画中画
Android 8.0 Oreo(API Level 26)允许活动启动画中画 Picture-in-picture(PIP)模式。PIP 是一种特殊类型的多窗口模式,主要用于视频播放。PIP 模式已经可用于 Android TV,而 Android 8.0 则让该功能可进一步用于其他 Android 设备。
画中画利用 Android 7.0 中的多窗口模式 API 来提供固定的视频叠加窗口。要将画中画添加到您的应用中,您需要注册支持画中画的 Activity、根据需要将 Activity 切换为画中画模式,并确保当 Activity 处于画中画模式时,界面元素处于隐藏状态且视频能够继续播放。

%title插图%num

如何使用
声明对画中画的支持
默认情况下,系统不会自动为应用提供画中画支持。要想在应用中支持画中画,您可以通过将 android:supportsPictureInPicture 和 android:resizeableActivity 设置为 true ,在清单中注册视频 Activity。此外,指定您的 Activity 会处理布局配置更改,这样一来,在画中画模式转换期间发生布局更改时,您的 Activity 不会重新启动。

<activity android:name=”VideoActivity”
android:resizeableActivity=”true”
android:supportsPictureInPicture=”true”
android:configChanges=
“screenSize|smallestScreenSize|screenLayout|orientation”

低内存设备可能无法使用画中画模式。在应用使用画中画之前,请务必通过调用 hasSystemFeature(PackageManager. FEATURE_PICTURE_IN_PICTURE) 进行检查以确保可以使用画中画。
isSupportPipMode = getPackageManager().hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N;
if(videoPlayer!= null) {
videoPlayer.setSupportPipMode(isSupportPipMode);
}
对单个播放 Activity 使用画中画模式
要确保将单个 Activity 用于视频播放请求并根据需要进入或退出画中画模式,请在清单中将 Activity 的 android:launchMode 设置为 singleTask:
<activity android:name=”VideoActivity”

android:supportsPictureInPicture=”true”
android:launchMode=”singleTask”

在您的 Activity 中,替换 onNewIntent() 并处理新的视频,从而根据需要停止任何现有的视频播放。

将您的 Activity 切换到画中画模式
要进入画中画模式,Activity 必须调用 enterPictureInPictureMode()。例如,以下代码会在用户点击应用界面中的专用按钮时,将 Activity 切换到画中画模式:
/**
* 进入画中画模式
*/
private PictureInPictureParams.Builder mPictureInPictureParamsBuilder;
private void enterPiPMode() {
if (videoPlayer == null) {
return;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
videoPlayer.setIsInPictureInPictureMode(true);
if (mPictureInPictureParamsBuilder == null) {
mPictureInPictureParamsBuilder = new PictureInPictureParams.Builder();
}
// Calculate the aspect ratio of the PiP screen. 计算video的纵横比
mVideoWith = videoPlayer.getCurrentVideoWidth();
mVideoHeight = videoPlayer.getCurrentVideoHeight();
if (mVideoWith != 0 && mVideoHeight != 0) {
//设置param宽高比,根据宽高比例调整初始参数
Rational aspectRatio = new Rational(mVideoWith, mVideoHeight);
mPictureInPictureParamsBuilder.setAspectRatio(aspectRatio);
}
//进入pip模式
enterPictureInPictureMode(mPictureInPictureParamsBuilder.build());
}
}
进入 PIP 模式的*常见流程如下:
1. 从按钮触发
* onClicked (View),onOptionsItemSelected (MenuItem) 等等。
2. 有意的离开您的应用程序触发
* onUserLeaveHint ( )
3. 从返回触发
* onBackPressed ( )

在画中画期间处理界面
当 Activity 进入或退出画中画模式时,系统会调用 Activity. onPictureInPictureModeChanged() 或 Fragment. onPictureInPictureModeChanged()。
您应替换这些回调以重新绘制 Activity 的界面元素。请注意,在画中画模式下,您的 Activity 会在一个小窗口中显示。在画中画模式下,用户可能看不清小界面元素的详细信息,因此不会与这些界面元素互动。界面*简的视频播放 Activity 可提供出色的用户体验。Activity 应仅显示视频播放控件。在 Activity 进入画中画模式之前移除其他界面元素,并在 Activity 再次变为全屏时恢复这些元素:
@Override
public void onPictureInPictureModeChanged (boolean isInPictureInPictureMode, Configuration newConfig) {
if (isInPictureInPictureMode) {
// Hide the full-screen UI (controls, etc.) while in picture-in-picture mode.
} else {
// Restore the full-screen UI.

}
}
在画中画模式下继续播放视频
当您的 Activity 切换到画中画模式时,系统会将该 Activity 置于暂停状态并调用 Activity 的 onPause() 方法。如果该 Activity 在画中画模式下暂停,则视频播放不得暂停,而应继续播放。
在 Android 7.0 及更高版本中,当系统调用 Activity 的 onStop() 时,您应暂停视频播放;当系统调用 Activity 的 onStart() 时,您应恢复视频播放。这样一来,您就无需在 onPause() 中检查应用是否处于画中画模式,只需继续播放视频即可。如果您必须在 onPause() 实现中暂停播放,请通过调用 isInPictureInPictureMode() 检查画中画模式并相应地处理播放情况,例如:
@Override
public void onPause() {
// If called while in PIP mode, do not pause playback
if (isInPictureInPictureMode()) {
// Continue playback

} else {
// Use existing playback logic for paused Activity behavior.

}
}
切换视频/播放下一个时动态调画中画整宽高比例
/**
* 视频尺寸变化(上一个下一个时),动态调整PIP 宽高比
*
* @param with video宽度(非界面宽度)
* @param height video高度(非界面高度)
*/
private void videoSizeChange(int with, int height) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && (height != mVideoHeight || mVideoWith != with)) {
mVideoWith = with;
mVideoHeight = height;
if (mPictureInPictureParamsBuilder != null && mVideoWith != 0 && mVideoHeight != 0) {
//设置param宽高比,根据快高比例调整初始参数
Rational aspectRatio = new Rational(mVideoWith, mVideoHeight);
mPictureInPictureParamsBuilder.setAspectRatio(aspectRatio);

//设置更新PictureInPictureParams
setPictureInPictureParams(mPictureInPictureParamsBuilder.build());
}
}
}

进阶使用
添加自定义按钮:
方式一: 通过MediaSession达到如下图效果
(此处有关videoPlayer相关代码根据自己播放器灵活代入,仅供参考)

%title插图%num

当 Activity 进入画中画模式后,它默认没有获得输入焦点。要在画中画模式下接收输入事件,请使用 MediaSession.setCallback() 。如需详细了解如何使用 setCallback(),请参阅显示“ 正在播放 ”卡片。
首先在进入小窗前初始化MediaSessionCompat
private MediaSessionCompat mSession;
public static final long MEDIA_ACTIONS_PLAY_PAUSE = PlaybackStateCompat.ACTION_PLAY | PlaybackStateCompat.ACTION_PAUSE | PlaybackStateCompat.ACTION_PLAY_PAUSE;
public static final long MEDIA_ACTIONS_ALL = MEDIA_ACTIONS_PLAY_PAUSE | PlaybackStateCompat.ACTION_SKIP_TO_NEXT | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS;

private void initializeMediaSession() {
mSession = new MediaSessionCompat(this, TAG);
mSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
mSession.setActive(true);
MediaControllerCompat.setMediaController(this, mSession.getController());

MediaMetadataCompat metadata = new MediaMetadataCompat.Builder().build();
mSession.setMetadata(metadata);

MediaSessionCallback mMediaSessionCallback = new MediaSessionCallback(videoPlayer);
mSession.setCallback(mMediaSessionCallback);

int state = videoPlayer.getCurrentState() == GSYVideoView.CURRENT_STATE_PLAYING ? PlaybackStateCompat.STATE_PLAYING : PlaybackStateCompat.STATE_PAUSED;
updatePlaybackState(state, MEDIA_ACTIONS_ALL, 0, 0);
}
在MediaSessionCompat.Callback中设置自己的播放器逻辑响应

private class MediaSessionCallback extends MediaSessionCompat.Callback {

private LocalListVideoPlayer movieView;
private int indexInPlaylist;

public MediaSessionCallback(LocalListVideoPlayer movieView) {
this.movieView = movieView;
indexInPlaylist = 1;
}

@Override
public void onPlay() {
super.onPlay();
movieView.getGSYVideoManager().start();
movieView.setIsInPictureInPictureMode(true);
movieView.setCurrentState(GSYVideoView.CURRENT_STATE_PLAYING);
updatePlaybackState(PlaybackStateCompat.STATE_PLAYING, 0, 0);
}

@Override
public void onPause() {
super.onPause();
movieView.getGSYVideoManager().pause();
movieView.setIsInPictureInPictureMode(true);
movieView.setCurrentState(GSYVideoView.CURRENT_STATE_PAUSE);
updatePlaybackState(PlaybackStateCompat.STATE_PAUSED, 0, 0);
}

@Override
public void onSkipToNext() {
super.onSkipToNext();
movieView.playNext();
}

@Override
public void onSkipToPrevious() {
super.onSkipToPrevious();
movieView.playLast();
}
}

//更新按钮操作
private void updatePlaybackState(@PlaybackStateCompat.State int state, int position, int mediaId) {
if (mSession.getController().getPlaybackState() != null) {
long actions = mSession.getController().getPlaybackState().getActions();
updatePlaybackState(state, actions, position, mediaId);
}
}

//初始化setPlaybackState
private void updatePlaybackState(@PlaybackStateCompat.State int state, long playbackActions, int position, int mediaId) {
if (mSession != null) {
PlaybackStateCompat.Builder builder = new PlaybackStateCompat.Builder()
.setActions(playbackActions)
.setActiveQueueItemId(mediaId)
.setState(state, position, 1.0f);
mSession.setPlaybackState(builder.build());
}
}
在自己播放器状态更新时更新界面元素

@Override
public void onVideoStart() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
if(isInPictureInPictureMode()) {
updatePlaybackState(PlaybackStateCompat.STATE_PLAYING, 0, 0);
}
}
}

@Override
public void onVideoPause() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
if(isInPictureInPictureMode()) {
updatePlaybackState(PlaybackStateCompat.STATE_PAUSED, 0, 0);
}
}
}
方式2: 自定义按钮 (推荐)
(注意,按钮不超过三个,位置不可调节)
您还可以通过在进入画中画模式之前构建 PictureInPictureParams(使用 PictureInPictureParams.Builder. setActions())来明确指定自定义操作,并使用 enterPictureInPictureMode(android.app.PictureInPictureParams) 或 setPictureInPictureParams(android.app.PictureInPictureParams) 在进入画中画模式时传递这些参数。

首先自定义按钮初始化或刷新
private BroadcastReceiver mReceiver;
private static final String ACTION_MEDIA_CONTROL = “media_control”;
private static final String EXTRA_CONTROL_TYPE = “control_type”;
private static final int CONTROL_TYPE_PLAY = 1;
private static final int CONTROL_TYPE_PAUSE = 2;
private static final int CONTROL_TYPE_LAST = 3;
private static final int CONTROL_TYPE_NEXT = 4;
private static final int REQUEST_TYPE_PLAY = 1;
private static final int REQUEST_TYPE_PAUSE = 2;
private static final int REQUEST_TYPE_LAST = 3;
private static final int REQUEST_TYPE_NEXT = 4;

//进入画中画前判断状态,调用initPictureInPictureActions
private void initPictureInPictureActions() {
//int state = videoPlayer.getCurrentState() == GSYVideoView.CURRENT_STATE_PLAYING ? PlaybackStateCompat.STATE_PLAYING : PlaybackStateCompat.STATE_PAUSED;
//STATE_PLAYING = 3 ; STATE_PAUSED = 2
if (videoPlayer.getCurrentState() == GSYVideoView.CURRENT_STATE_PLAYING) {
updatePictureInPictureActions(R.drawable.gsy_play_video_icon_pause, “”, CONTROL_TYPE_PLAY, REQUEST_TYPE_PLAY);
} else {
updatePictureInPictureActions(R.drawable.gsy_play_video_icon_play, “”, CONTROL_TYPE_PAUSE, REQUEST_TYPE_PAUSE);
}
}
/**
* 刷新自定义按钮 (若是初始化,注意区分进入画中画前onpause状态)
*
* @param iconId
* @param title
* @param controlType
* @param requestCode 注意!! 每个intent的requestCode必须不一样
*/
void updatePictureInPictureActions(@DrawableRes int iconId, String title, int controlType, int requestCode) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (mPictureInPictureParamsBuilder == null) {
mPictureInPictureParamsBuilder = new PictureInPictureParams.Builder();
}
final ArrayList<RemoteAction> actions = new ArrayList<>();
// This is the PendingIntent that is invoked when a user clicks on the action item. You need to use distinct request codes for play and pause, or the PendingIntent won’t be updated.

//上一个
final PendingIntent intentLast = PendingIntent.getBroadcast(this, REQUEST_TYPE_NEXT, new Intent(ACTION_MEDIA_CONTROL).putExtra(EXTRA_CONTROL_TYPE, CONTROL_TYPE_LAST), 0);
actions.add(new RemoteAction(Icon.createWithResource(this, R.drawable.gsy_play_video_icon_last), “”, “”, intentLast));
//暂停/播放
final PendingIntent intentPause = PendingIntent.getBroadcast(this, requestCode, new Intent(ACTION_MEDIA_CONTROL).putExtra(EXTRA_CONTROL_TYPE, controlType), 0);
actions.add(new RemoteAction(Icon.createWithResource(this, iconId), title, title, intentPause));
//下一个
final PendingIntent intentNext = PendingIntent.getBroadcast(this, REQUEST_TYPE_LAST, new Intent(ACTION_MEDIA_CONTROL).putExtra(EXTRA_CONTROL_TYPE, CONTROL_TYPE_NEXT), 0);
actions.add(new RemoteAction(Icon.createWithResource(this, R.drawable.gsy_play_video_icon_next), “”, “”, intentNext));

mPictureInPictureParamsBuilder.setActions(actions);

// This is how you can update action items (or aspect ratio) for Picture-in-Picture mode. Note this call can happen even when the app is not in PiP mode.
setPictureInPictureParams(mPictureInPictureParamsBuilder.build());
}
}
响应按钮发出的intent
@Override
public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode, Configuration newConfig) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig);
if (videoPlayer != null) {
isInPIPMode = isInPictureInPictureMode;
videoPlayer.setIsInPictureInPictureMode(isInPIPMode);
}
//自定义action形式
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
final int controlType = intent.getIntExtra(EXTRA_CONTROL_TYPE, 0);
try {
switch (controlType) {
case CONTROL_TYPE_PLAY:
videoPlayer.getGSYVideoManager().start();
videoPlayer.setIsInPictureInPictureMode(true);
videoPlayer.setCurrentState(GSYVideoView.CURRENT_STATE_PLAYING);
break;
case CONTROL_TYPE_PAUSE:
videoPlayer.getGSYVideoManager().pause();
videoPlayer.setIsInPictureInPictureMode(true);
videoPlayer.setCurrentState(GSYVideoView.CURRENT_STATE_PAUSE);
break;
case CONTROL_TYPE_LAST:
videoPlayer.playLast();
break;
case CONTROL_TYPE_NEXT:
videoPlayer.playNext();
break;
}
} catch (Exception e) {
e.printStackTrace();
}
}
};
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;
}
}
当播放状态改变时更新按钮功能

videoPlayer.setLocalPlayerCallback(new LocalListVideoPlayer.LocalPlayerCallback() {
@Override
public void clickPIPMode() {
enterPiPMode();
}
@Override
public void OnPrepareVideoSizeChanged(int with, int height) {
videoSizeChange(with, height);
}
@Override
public void surfaceDestroyed() {
handleSurfaceDestroyed();
}
@Override
public void onVideoStart() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
//自定义action刷新-开始播放-按钮替换为暂停
updatePictureInPictureActions(R.drawable.gsy_play_video_icon_pause, “”, CONTROL_TYPE_PLAY, REQUEST_TYPE_PLAY);
}
}
@Override
public void onVideoPause() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
//自定义action刷新-暂停播放,按钮替换为开始
updatePictureInPictureActions(R.drawable.gsy_play_video_icon_play, “”, CONTROL_TYPE_PAUSE, REQUEST_TYPE_PAUSE);
}
关于浮窗关闭后仍有声音,无法获取浮窗关闭通知
方式一:监听SurfaceView-surfaceDestroyed()
在官方demo中,采用mediaSession方式, 以surfaceview的 surfaceDestroyed()回调关闭播放器
我采用的gsy播放器(同bilibili播放器),无法监听画中画浮窗关闭,采用如下的方法
在播放界面底层创建一个空的emptySurfaceView,通过callback获知浮窗被手动关闭 (此方法有个缺陷:在锁屏时也会回调此方法)
private SurfaceView emptySurfaceView;
….
emptySurfaceView = findViewById(R.id.emtpy_surface);
emptySurfaceView
.getHolder()
.addCallback(
new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder holder) {
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
if(mLocalPlayerCallback != null) {
mLocalPlayerCallback.surfaceDestroyed();
}
}
});
方式2:通过进入/退出/关闭画中画VideoActivity的生命周期判断(推荐)
操作画中画时VideoActivity相关生命周期梳理:

进入画中画–onPause

画中画返回全屏–OnResume

关闭画中画–onStop

全屏播放状态下下锁屏/解锁 onPause ,onStop / onStart,onResume

画中画状态下下锁屏/解锁 onStop / onStart

//是否支持pip画中画小窗模式(自行判断赋值时机)
protected boolean isSupportPipMode = false;
//是否已经在画中画模式(自行判断赋值时机)
public boolean isInPIPMode = false;
//是否点击进入过画中画模式–用于判断程序在后台时,由画中画返回全屏后退出,是否启动首页activity,以及onstop配合判断是否点击进入过画中画且在画中画模式
public boolean isEnteredPIPMode = false;

@Override
protected void onResume() {
super.onResume();
//画中画返回全屏会执行onresume
isEnteredPIPMode = false;
}

@Override
protected void onStop() {
super.onStop();
//备注: 在画中画模式下,onStop执行时, 若是关闭画中画,isInPictureInPictureMode()=false ; 若是锁屏,isInPictureInPictureMode()=true ; 判断锁屏isLockPage()一直为false
boolean inPictureInPictureMode = false;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
inPictureInPictureMode = isInPictureInPictureMode();
}
if (BuildConfig.DEBUG) {
Log.i(TAG, “onStop — inPictureInPictureMode=” + inPictureInPictureMode + ” ,isEnteredPIPMode=” + isEnteredPIPMode + ” ,isInPIPMode=” + isInPIPMode);
}
if (!inPictureInPictureMode && isInPIPMode && isEnteredPIPMode) {
//满足此条件下认为是关闭了画中画界面
if (BuildConfig.DEBUG) {
Log.w(TAG, “onStop — 判断为PIP下关闭画中画”);
}
handleSurfaceDestroyed();
return;
}
if (inPictureInPictureMode && isInPIPMode && isEnteredPIPMode && videoPlayer != null) {
//满足此条件下认为是画中画模式下锁屏
videoPlayer.onVideoPause();
isPause = true;
if (BuildConfig.DEBUG) {
Log.w(TAG, “onStop — 判断为PIP下锁屏”);
}
}
}
关于开启浮窗关闭后显示在*近任务列表
manifest添加 android:excludeFromRecents=”true”

参考:关于Android TaskAffinity的那些事儿

From Picture-in-Picture activity to Back-Stack activity not working in android?

关于APP进入后台,播放完成后吊起主页activity,主页activity也进入浮窗模式(部分机型偶现)
主页设置 android:supportsPictureInPicture=”false”无效

方案:采用遍历tasks,task.moveToFront() – task.moveToFront(); 避免采用startActivity方法使应用回到前台

参考:Launching Intent from notification opening in picture-in-picture window

public static void moveLauncherTaskToFront(Context context) {

ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
assert activityManager != null;
final List<ActivityManager.AppTask> appTasks = activityManager.getAppTasks();
for (ActivityManager.AppTask task : appTasks) {
final Intent baseIntent = task.getTaskInfo().baseIntent;
final Set<String> categories = baseIntent.getCategories();
if (categories != null && categories.contains(Intent.CATEGORY_LAUNCHER)) {
task.moveToFront();
return;
}
}
}
判断获取用户是否关闭了应用画中画模式

当您的应用处于画中画模式时,画中画窗口中的视频播放可能会对其他应用(例如,音乐播放器应用或语音搜索应用)造成音频干扰。为避免出现此问题,请在开始播放视频时请求音频焦点,并处理音频焦点更改通知,如管理音频焦点中所述。如果您在处于画中画模式时收到音频焦点丢失通知,请暂停或停止视频播放。

//音频焦点的监听
protected AudioManager mAudioManager;
mAudioManager = (AudioManager) getActivityContext().getApplicationContext().getSystemService(Context.AUDIO_SERVICE);

/**
* 监听是否有外部其他多媒体开始播放
*/
protected AudioManager.OnAudioFocusChangeListener onAudioFocusChangeListener = new AudioManager.OnAudioFocusChangeListener() {
@Override
public void onAudioFocusChange(int focusChange) {
switch (focusChange) {
case AudioManager.AUDIOFOCUS_GAIN:
//获得了Audio Focus
onGankAudio();
break;
case AudioManager.AUDIOFOCUS_LOSS:
//失去了Audio Focus,并将会持续很长的时间-暂停音频
onLossAudio();
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
//暂时失去Audio Focus,并会很快再次获得
onLossTransientAudio();
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
//暂时失去AudioFocus,但是可以继续播放,不过要在降低音量
onLossTransientCanDuck();
break;
}
}
};

问题处理记录:
页面切换进入/退出小窗播放状态处理
小窗关闭监听
10s退出
权限支持判断
自定义按钮状态
吊起主页页面状态错误
创建多个后台

参考:
app: MXplayer: (顶层activity: pipmenuActivity)
————————————————
版权声明:本文为CSDN博主「ZhangQiang-」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/u011200604/article/details/104701266

300. *长上升子序列(JS实现)

300. *长上升子序列(JS实现)
1 题目
给定一个无序的整数数组,找到其中*长上升子序列的长度。
示例:
输入: [10,9,2,5,3,7,101,18]
输出: 4
解释: *长的上升子序列是 [2,3,7,101],它的长度是 4。
说明:
可能会有多种*长上升子序列的组合,你只需要输出对应的长度即可。
你算法的时间复杂度应该为 O(n2) 。
进阶: 你能将算法的时间复杂度降低到 O(n log n) 吗?
链接:https://leetcode-cn.com/problems/longest-increasing-subsequence
2 思路
这道题明显根据题意就知道应该用动态规划来做,注意题意,这里是子序列并不是要求连续的,设d[i]为0…i元素之间*大上升子序列长度,状态转移方程为 d[i+1] = max(d[0…i]) + 1
3代码
/**
 * @param {number[]} nums
 * @return {number}
 */
var lengthOfLIS = function(nums) {
    if (nums.length < 2) return nums.length;
    const d = [1];
    let maxAns = 1;
    for (let i=1; i<nums.length; i++) {
        let len = 0;
        for (let j=0;j<d.length;j++) {
            if (nums[i] > nums[j]) {
                len = Math.max(len, d[j]);
            }
        }
        d[i] = len + 1;
        maxAns = Math.max(maxAns, d[i])
    }
    return maxAns;
};

304. 二维区域和检索 – 矩阵不可变(JS实现)

304. 二维区域和检索 – 矩阵不可变(JS实现)
1 题目
给定一个二维矩阵,计算其子矩形范围内元素的总和,该子矩阵的左上角为 (row1, col1) ,右下角为 (row2, col2)。
上图子矩阵左上角 (row1, col1) = (2, 1) ,右下角(row2, col2) = (4, 3),该子矩形内元素的总和为 8。
示例:
给定 matrix = [
[3, 0, 1, 4, 2],
[5, 6, 3, 2, 1],
[1, 2, 0, 1, 5],
[4, 1, 0, 1, 7],
[1, 0, 3, 0, 5]
]
sumRegion(2, 1, 4, 3) -> 8
sumRegion(1, 1, 2, 2) -> 11
sumRegion(1, 2, 2, 4) -> 12
说明:
你可以假设矩阵不可变。
会多次调用 sumRegion 方法。
你可以假设 row1 ≤ row2 且 col1 ≤ col2。
链接:https://leetcode-cn.com/problems/range-sum-query-2d-immutable
2 思路
这道题可以用动态规划来做,首先sumRegion(2, 1, 4, 3)可以分割为sumRegion(0, 0, 4, 3) – sumRegion(0, 0, 4, 1) – sumRegion(0, 0, 2, 3) + sumRegion(0, 0, 2, 1),因此我只需计算d[i][j],d[i][j]为左上角为num[0][0],右下角为num[i][j]子矩阵的和。状态转移方程为d[i+1][j] = d[i][j] + sumRow(i+1, j), 其中 sumRow(i,j)为第i行0…j的和
3代码
/**
 * @param {number[][]} matrix
 */
var NumMatrix = function(matrix) {
    this.d = [];
    let rows = matrix.length;
    if (rows === 0) return;
    let cols = matrix[0].length;
    for (let i=0; i<rows; i++) {
        this.d[i] = [];
        let sumRow = 0;
        for (let j=0; j<cols; j++) {
            sumRow += matrix[i][j];
            this.d[i][j] = i > 0 ? this.d[i-1][j] + sumRow : sumRow;
        }
    }
};
/**
 * @param {number} row1
 * @param {number} col1
 * @param {number} row2
 * @param {number} col2
 * @return {number}
 */
NumMatrix.prototype.sumRegion = function(row1, col1, row2, col2) {
    let area = 0;
    if (this.d.length === 0) return area;
    if (col1 > 0) {
        area -= this.d[row2][col1-1];
    }
    if (row1 > 0) {
        area -= this.d[row1-1][col2];
    }
    if ( col1 > 0 && row1 > 0) {
        area += this.d[row1-1][col1-1];
    }
    return this.d[row2][col2] + area;
};
/**
 * Your NumMatrix object will be instantiated and called as such:
 * var obj = new NumMatrix(matrix)
 * var param_1 = obj.sumRegion(row1,col1,row2,col2)
 */
————————————————
版权声明:本文为CSDN博主「PAT-python-zjw」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/zjw_python/article/details/108035266
1 题目
给定一个二维矩阵,计算其子矩形范围内元素的总和,该子矩阵的左上角为 (row1, col1) ,右下角为 (row2, col2)。
上图子矩阵左上角 (row1, col1) = (2, 1) ,右下角(row2, col2) = (4, 3),该子矩形内元素的总和为 8。
示例:
给定 matrix = [
[3, 0, 1, 4, 2],
[5, 6, 3, 2, 1],
[1, 2, 0, 1, 5],
[4, 1, 0, 1, 7],
[1, 0, 3, 0, 5]
]
sumRegion(2, 1, 4, 3) -> 8
sumRegion(1, 1, 2, 2) -> 11
sumRegion(1, 2, 2, 4) -> 12
说明:
你可以假设矩阵不可变。
会多次调用 sumRegion 方法。
你可以假设 row1 ≤ row2 且 col1 ≤ col2。
链接:https://leetcode-cn.com/problems/range-sum-query-2d-immutable
2 思路
这道题可以用动态规划来做,首先sumRegion(2, 1, 4, 3)可以分割为sumRegion(0, 0, 4, 3) – sumRegion(0, 0, 4, 1) – sumRegion(0, 0, 2, 3) + sumRegion(0, 0, 2, 1),因此我只需计算d[i][j],d[i][j]为左上角为num[0][0],右下角为num[i][j]子矩阵的和。状态转移方程为d[i+1][j] = d[i][j] + sumRow(i+1, j), 其中 sumRow(i,j)为第i行0…j的和
3代码
/**
 * @param {number[][]} matrix
 */
var NumMatrix = function(matrix) {
    this.d = [];
    let rows = matrix.length;
    if (rows === 0) return;
    let cols = matrix[0].length;
    for (let i=0; i<rows; i++) {
        this.d[i] = [];
        let sumRow = 0;
        for (let j=0; j<cols; j++) {
            sumRow += matrix[i][j];
            this.d[i][j] = i > 0 ? this.d[i-1][j] + sumRow : sumRow;
        }
    }
};
/**
 * @param {number} row1
 * @param {number} col1
 * @param {number} row2
 * @param {number} col2
 * @return {number}
 */
NumMatrix.prototype.sumRegion = function(row1, col1, row2, col2) {
    let area = 0;
    if (this.d.length === 0) return area;
    if (col1 > 0) {
        area -= this.d[row2][col1-1];
    }
    if (row1 > 0) {
        area -= this.d[row1-1][col2];
    }
    if ( col1 > 0 && row1 > 0) {
        area += this.d[row1-1][col1-1];
    }
    return this.d[row2][col2] + area;
};
/**
 * Your NumMatrix object will be instantiated and called as such:
 * var obj = new NumMatrix(matrix)
 * var param_1 = obj.sumRegion(row1,col1,row2,col2)
 */

306. 累加数(JS实现)

306. 累加数(JS实现)
1 题目
累加数是一个字符串,组成它的数字可以形成累加序列。
一个有效的累加序列必须至少包含 3 个数。除了*开始的两个数以外,字符串中的其他数都等于它之前两个数相加的和。
给定一个只包含数字 ‘0’-‘9’ 的字符串,编写一个算法来判断给定输入是否是累加数。
说明: 累加序列里的数不会以 0 开头,所以不会出现 1, 2, 03 或者 1, 02, 3 的情况。
示例 1:
输入: “112358”
输出: true
解释: 累加序列为: 1, 1, 2, 3, 5, 8 。1 + 1 = 2, 1 + 2 = 3, 2 + 3 = 5, 3 + 5 = 8
示例 2:
输入: “199100199”
输出: true
解释: 累加序列为: 1, 99, 100, 199。1 + 99 = 100, 99 + 100 = 199
链接:https://leetcode-cn.com/problems/additive-number
2 思路
这道题用递归的方法,依次回溯累加即可,比较讨厌的是,要注意处理0的情况,递归函数d(i,j,k,s),其中i为指向当前*个数的开头字符,j指向第二个数的开头字符,k为第三个数的开头字符
3代码
/**
 * @param {string} num
 * @return {boolean}
 */
var isAdditiveNumber = function(num) {
    if (num.length < 3) return false;
    let i=0;
    for (let j=i+1; j<num.length-1;j++) {
        for (let k=j+1; k<num.length; k++) {
            if (d(i,j,k,num)) return true;
        }
    }
    return false;
    function d(i,j,k,s) {
        if (k === s.length) return true;
        if (s[i] === ‘0’ && j > i + 1) return false;  //*个数字不是0,但以0开头
        if (s[j] === ‘0’ && k > j + 1) return false;  //第二个数字不是0,但以0开头
        let num1 = parseInt(s.slice(i,j));
        let num2 = parseInt(s.slice(j,k));
        for (let m=k+1; m<=s.length; m++) {
            if (s[k] === ‘0’ && m > k+1) return false;  //第三个数字不是0,但以0开头
            let num3 = parseInt(s.slice(k,m));
            if (num1 + num2 === num3) {
                return d(j,k,m,s);
            } else if (num1 + num2 < num3) {    //若前两个数已经小于第三个数,则提前结束
                return false;
            }
        }
        return false;
    }
};

307. 区域和检索 – 数组可修改(JS实现)

307. 区域和检索 – 数组可修改(JS实现)
1 题目
给定一个整数数组 nums,求出数组从索引 i 到 j (i ≤ j) 范围内元素的总和,包含 i, j 两点。
update(i, val) 函数可以通过将下标为 i 的数值更新为 val,从而对数列进行修改。
示例:
Given nums = [1, 3, 5]
sumRange(0, 2) -> 9
update(1, 2)
sumRange(0, 2) -> 8
说明:
数组仅可以在 update 函数下进行修改。
你可以假设 update 函数与 sumRange 函数的调用次数是均匀分布的。
链接:https://leetcode-cn.com/problems/range-sum-query-mutable
2 思路
这道题用动态规划的方法来做即可,在更新数组元素值时,也要更新dp数组
3代码
/**
 * @param {number[]} nums
 */
var NumArray = function(nums) {
    this.d = [];
    this.nums = nums;
    let sum = 0;
    for (let i=0; i<nums.length; i++) {
        sum += nums[i];
        this.d[i] = sum;
    }
};
/**
 * @param {number} i
 * @param {number} val
 * @return {void}
 */
NumArray.prototype.update = function(i, val) {
    let diff = val – this.nums[i];
    for (let j=i; j<this.d.length; j++) {
        this.d[j] += diff;
    }
    this.nums[i] = val;
};
/**
 * @param {number} i
 * @param {number} j
 * @return {number}
 */
NumArray.prototype.sumRange = function(i, j) {
    return this.d[j] – (i > 0 ? this.d[i-1] : 0);
};
/**
 * Your NumArray object will be instantiated and called as such:
 * var obj = new NumArray(nums)
 * obj.update(i,val)
 * var param_2 = obj.sumRange(i,j)
 */

310. *小高度树(JS实现)

310. *小高度树(JS实现)
1 题目
对于一个具有树特征的无向图,我们可选择任何一个节点作为根。图因此可以成为树,在所有可能的树中,具有*小高度的树被称为*小高度树。给出这样的一个图,写出一个函数找到所有的*小高度树并返回他们的根节点。
格式
该图包含 n 个节点,标记为 0 到 n – 1。给定数字 n 和一个无向边 edges 列表(每一个边都是一对标签)。
你可以假设没有重复的边会出现在 edges 中。由于所有的边都是无向边, [0, 1]和 [1, 0] 是相同的,因此不会同时出现在 edges 里。
示例 1:
输入: n = 4, edges = [[1, 0], [1, 2], [1, 3]]
0
|
1
/
2 3
输出: [1]
示例 2:
输入: n = 6, edges = [[0, 3], [1, 3], [2, 3], [4, 3], [5, 4]]
0 1 2
\ | /
3
|
4
|
5
输出: [3, 4]
说明:
根据树的定义,树是一个无向图,其中任何两个顶点只通过一条路径连接。 换句话说,一个任何没有简单环路的连通图都是一棵树。
树的高度是指根节点和叶子节点之间*长向下路径上边的数量。
链接:https://leetcode-cn.com/problems/minimum-height-trees
2 思路
这道题考察图的遍历,每次从图的*外层节点开始,删除图外层节点(入度为1的节点),直到图中没有节点,*后一次删除的节点就是*内层的根节点
3代码
/**
 * @param {number} n
 * @param {number[][]} edges
 * @return {number[]}
 */
var findMinHeightTrees = function(n, edges) {
    if (edges.length === 0) {   //对于没有边的情况,每个节点都是根节点
        let res = [];
        for (let i=0; i<n; i++) {
            res.push(i);
        }
        return res;
    }
    const map = {};
    for (let edge of edges) {    //建立图结构
        if (!map[edge[0]]) {
            map[edge[0]] = [];
            map[edge[0]].len = 0;
        }
        if (!map[edge[1]]) {
            map[edge[1]] = [];
            map[edge[1]].len = 0;
        }
        map[edge[0]].push(edge[1]);
        map[edge[1]].push(edge[0]);
        map[edge[0]].len++;
        map[edge[1]].len++;
    }
    let arr = Object.keys(map).filter(key => map[key].len === 1);
    let temp = [];
    let visited = {};
    let res = arr.slice();
    while(arr.length > 0 || temp.length > 0) {
        if (arr.length === 0) {
            arr = temp;
            temp = [];
            res = arr.slice();
        }
        let p = arr.shift();
        if (visited[p]) continue;
        visited[p] = true;
        for (let edge of map[p]) {   //将下一次外层节点推入
            if (–map[edge].len === 1) temp.push(edge);
        }
    }
    return res;
};

309. *佳买卖股票时机含冷冻期(JS实现)

309. *佳买卖股票时机含冷冻期(JS实现)
1 题目
给定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格 。​
设计一个算法计算出*大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
示例:
输入: [1,2,3,0,2]
输出: 3
解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出]
链接:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-with-cooldown
2 思路
这道题是股票系列的变种, 动态规划d[i][j],代表第i天持有状态为j的*大利润,状态转移方程d[i][0] = Math.max(d[i-1][0], d[i-1][1] + prices[i])、d[i][1] = Math.max(d[i-1][1], d[i-2][0] – prices[i]),注意由于股票有冷冻期,因此只能买入第i-2天的股票
3代码
/**
 * @param {number[]} prices
 * @return {number}
 */
var maxProfit = function(prices) {
    if (prices.length === 0) return 0;
    const d = [];
    const min = Number.MIN_SAFE_INTEGER;
    for (let i=0; i<prices.length; i++) {
        if (!d[i]) d[i] = [];
        d[i][0] = 0;
        d[i][1] = 0;
    }
    for (let i=0; i<prices.length; i++) {
        if (i === 0) {   //*天特殊处理
            d[0][0] = 0;
            d[0][1] = -prices[i];
            continue;
        }
        if (i === 1) {  //第二天情况特殊处理
            d[i][0] = Math.max(d[i-1][0], d[i-1][1] + prices[i]);
            d[i][1] = Math.max(d[i-1][1], -prices[i]);
            continue;
        }
        d[i][0] = Math.max(d[i-1][0], d[i-1][1] + prices[i]);
        d[i][1] = Math.max(d[i-1][1], d[i-2][0] – prices[i]);
    }
    return d[prices.length-1][0];
};