画中画(Picture-in-Picture, PiP)功能(也称“悬浮窗播放”)允许用户在屏幕一角的悬浮小窗中观看视频,同时可以在主屏幕上继续查看其它内容或与其他应用进行交互。这对于需要多任务处理的场景(如一边观看视频教程一边操作)非常有用。本文为您详细介绍使用 Android 播放器 SDK 时如何实现画中画功能。
Demo 中短剧详情页、短/中/长各场景均实现了画中画功能,在设置中开启通用设置 - 开启小窗开关后即可体验。前往下载体验 Demo
在开始集成前,请确保 Android 版本为 Android 6.0 (API level 23) 或更高版本。Android 6.0 以上提供了标准的悬浮窗权限申请 API,低于此版本则需要兼容各厂商的私有 API。
实现画中画功能的核心流程如下:
PipWindowView
,封装窗口的创建、显示和隐藏、手势拖动、边缘吸附等通用逻辑。videoEngine
,仅切换视频流渲染的 Surface ,从而实现无缝播放体验。本文仅为您介绍实现画中画功能的核心步骤。如需了解实现细节,可点击以下表格中的链接前往 GitHub 查看源码。
模块 | 关键类 | 说明 |
---|---|---|
vod-demo | 悬浮窗权限申请。旨在简化悬浮窗权限申请流程,内部使用的权限申请 API 要求系统版本需大于等于 Android 6.0,若不满足此条件则返回申请失败。 | |
悬浮窗权限申请代理 Activity。用于监听 | ||
悬浮窗控制器。控制在主播放界面和画中画窗口之间切换的逻辑。 | ||
vod-scenekit | 画中画视频场景。实现播放列表播放。 | |
通用悬浮窗控件。 |
在应用启动或进入画中画功能前,需要检查并申请悬浮窗权限。以下为您介绍如何使用系统权限申请。
注意
系统权限申请依赖 onActivityResult
回调授权结果,对业务逻辑的侵入性较强,建议使用 Demo 封装的 PipVideoPermission 进行权限申请。
// 查询用户是否授权 boolean isPermissionGranted = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && Settings.canDrawOverlays(mContext);
如果用户未授权,则需要引导用户到系统设置页面手动授权。
在 AndroidManifest.xml
中声明 SYSTEM_ALERT_WINDOW
权限:
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
在需要触发画中画时调用权限申请逻辑:
public static final int REQUEST_CODE = 100; @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == REQUEST_CODE) { if (Settings.canDrawOverlays(mContext)) { // 用户在设置中已授权,处理主播放界面切换悬浮小窗逻辑 } else { // 未授权 Toast.makeText(mContext, "未开启权限,小窗切换失败...", Toast.LENGTH_SHORT).show(); } } } public void requestMainToPip() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { // 6.0 以下不支持权限申请 return; } if (Settings.canDrawOverlays(mContext)) { // 已授权,处理主播放界面切换悬浮小窗逻辑 } else { // 展示权限申请 Dialog new AlertDialog.Builder(mContext) .setMessage("尚未开启系统悬浮窗,请去设置中开启 [显示悬浮窗] 权限") .setPositiveButton("去开启", (dialog, which) -> { // 用户同意开启。前往设置开启应用悬浮窗权限,开启结果在 Activity 的 onActivityResult 回调 Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + mContext.getPackageName())); activity.startActivityForResult(intent, REQUEST_CODE); }) .setNegativeButton("取消", (dialog, which) -> { // 用户未同意 Toast.makeText(mContext, "未开启权限,小窗切换失败...", Toast.LENGTH_SHORT).show(); }) .setCancelable(false) .show(); } }
将 Demo 中提供的 PipWindowView 类拷贝至您自己的项目中,再参考以下示例代码使用 PipWindowView
作为画中画的容器。PipWindowView
内部实现了悬浮窗的显示隐藏、手势拖动、贴边动画等功能,不包含视频相关逻辑,适合任何悬浮窗场景使用。
// 创建悬浮窗控件 PipWindowView PipWindowView pipWindowView = new PipWindowView(context);
// 设置悬浮窗大小,单位:px pipWindowView.setWindowInitWidth(width); pipWindowView.setWindowInitHeight(height); // 设置悬浮窗距离屏幕边缘的 margin,单位:px pipWindowView.setWindowMargin(margin); // 设置悬浮窗显示位置坐标,单位:px pipWindowView.setWindowInitX(x); pipWindowView.setWindowInitY(y); // 设置是否在显示时使用设置的大小、坐标信息 pipWindowView.setInitShow(true); // 设置悬浮窗圆角,单位:px pipWindowView.setRadius(radius); // 设置悬浮窗背景颜色 pipWindowView.setCardBackgroundColor(Color.BLACK); // 设置悬浮窗 Z 轴高度,以及阴影效果,单位:px pipWindowView.setCardElevation(elevation);
// 显示 pipWindowView.show(); // 隐藏 pipWindowView.dismiss(); // 是否显示 boolean isShowing = pipWindowView.isShowing();
切换的核心是复用播放器 videoEngine
实例,只改变其渲染的 Surface。以下代码示例假设您已在您的 Activity 或 Fragment 中初始化了播放器实例 videoEngine
及用于主播放界面的mainTextureView
。
当用户触发进入画中画模式时,执行以下操作从主播放界面切换至悬浮小窗:
PipWindowView
中创建一个新的 TextureView
用于在小窗中渲染视频。videoEngine
的渲染目标从主界面的 Surface 切换到小窗的 Surface 。// 1. 创建小窗 TextureView TextureView pipTextureView = new TextureView(context); pipWindowView.add(pipTextureView, new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)); // 2. 切换到小窗 TextureView 的 Surface pipTextureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() { @Override public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) { // 小窗 Surface 设置给播放器即可 videoEngine.setSurface(new Surface(surfaceTexture)); } }); // 3. 显示小窗 pipWindowView.show();
当用户从画中画窗口返回应用时,执行相反的操作从悬浮小窗切换至主播放界面:
videoEngine
的渲染目标切回主播放界面的 TextureView 所对应的 Surface 。// 1. 隐藏小窗 pipWindowView.dismiss(); // 2. 切换到主播放界面 TextureView 的 Surface SurfaceTexture surfaceTexture = mainTextureView.getSurfaceTexture(); if (surfaceTexture != null && !surfaceTexture.isReleased()) { videoEngine.setSurface(new Surface(surfaceTexture)); } mainTextureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() { @Override public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) { // 主播放界面 Surface 更新后设置给播放器即可 videoEngine.setSurface(new Surface(surfaceTexture)); } });