You need to enable JavaScript to run this app.
导航
画中画
最近更新时间:2025.07.04 20:18:26首次发布时间:2025.07.04 20:18:26
我的收藏
有用
有用
无用
无用

画中画(Picture-in-Picture, PiP)功能(也称“悬浮窗播放”)允许用户在屏幕一角的悬浮小窗中观看视频,同时可以在主屏幕上继续查看其它内容或与其他应用进行交互。这对于需要多任务处理的场景(如一边观看视频教程一边操作)非常有用。本文为您详细介绍使用 Android 播放器 SDK 时如何实现画中画功能。

Demo 体验

Demo 中短剧详情页、短/中/长各场景均实现了画中画功能,在设置中开启通用设置 - 开启小窗开关后即可体验。前往下载体验 Demo

前提条件

在开始集成前,请确保 Android 版本为 Android 6.0 (API level 23) 或更高版本。Android 6.0 以上提供了标准的悬浮窗权限申请 API,低于此版本则需要兼容各厂商的私有 API。

实现流程说明

实现画中画功能的核心流程如下:

  1. 完成悬浮窗权限的申请与配置。
  2. 创建一个可复用的悬浮窗组件 PipWindowView,封装窗口的创建、显示和隐藏、手势拖动、边缘吸附等通用逻辑。
  3. 在通用悬浮窗组件内添加视频渲染 View、播控 View 和播放器。
  4. 实现一个控制器来控制在主播放界面和画中画窗口之间切换的逻辑。核心是复用同一个播放器实例 videoEngine,仅切换视频流渲染的 Surface ,从而实现无缝播放体验。

本文仅为您介绍实现画中画功能的核心步骤。如需了解实现细节,可点击以下表格中的链接前往 GitHub 查看源码。

模块

关键类

说明

vod-demo

PipVideoPermission

悬浮窗权限申请。旨在简化悬浮窗权限申请流程,内部使用的权限申请 API 要求系统版本需大于等于 Android 6.0,若不满足此条件则返回申请失败。

PipVideoPermissionActivity

悬浮窗权限申请代理 Activity。用于监听 onActivityResult 回调,简化流程。

PipVideoController

悬浮窗控制器。控制在主播放界面和画中画窗口之间切换的逻辑。

vod-scenekit

PipVideoScene

画中画视频场景。实现播放列表播放。

PipWindowView

通用悬浮窗控件。

实现步骤

步骤 1:检查并申请悬浮窗权限

在应用启动或进入画中画功能前,需要检查并申请悬浮窗权限。以下为您介绍如何使用系统权限申请。

注意

系统权限申请依赖 onActivityResult 回调授权结果,对业务逻辑的侵入性较强,建议使用 Demo 封装的 PipVideoPermission 进行权限申请。

查询是否授权

// 查询用户是否授权
boolean isPermissionGranted = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && Settings.canDrawOverlays(mContext);

动态申请权限

如果用户未授权,则需要引导用户到系统设置页面手动授权。

  1. AndroidManifest.xml 中声明 SYSTEM_ALERT_WINDOW 权限:

    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
    
  2. 在需要触发画中画时调用权限申请逻辑:

    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();
        }
    }
    

步骤 2:创建并配置通用悬浮窗

将 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();

步骤 3:实现主播放界面与悬浮小窗的切换

切换的核心是复用播放器 videoEngine 实例,只改变其渲染的 Surface。以下代码示例假设您已在您的 Activity 或 Fragment 中初始化了播放器实例 videoEngine 及用于主播放界面的mainTextureView

进入画中画模式

当用户触发进入画中画模式时,执行以下操作从主播放界面切换至悬浮小窗:

  1. PipWindowView 中创建一个新的 TextureView 用于在小窗中渲染视频。
  2. 将播放器 videoEngine 的渲染目标从主界面的 Surface 切换到小窗的 Surface 。
  3. 显示悬浮窗。
// 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();

退出画中画模式

当用户从画中画窗口返回应用时,执行相反的操作从悬浮小窗切换至主播放界面:

  1. 隐藏并销毁悬浮窗。
  2. 将播放器 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));
    }
});