Android实现画中画

本文介绍如何在Android端实现画中画(悬浮窗)。

功能介绍

“画中画”(Picture-in-Picture,简称 PiP)是一种让视频“悬浮”在屏幕上的功能。开启后,视频会以一个小窗口的形式显示在屏幕一角,用户可以一边看视频,一边正常使用手机或电脑的其他应用,比如回消息、浏览网页或操作其他App,互不干扰。

示例代码

ARTC 提供了开源示例代码供您参考:Android实现画中画

前提条件

要实现画中画功能,需要满足如下条件:

  • Android 版本不能低于 Android 8.0(API 级别 26)。

功能实现

1. 配置相关属性

画中画的 API 维度为 Activity,因此需要在 AndroidManifest.xml 中为 Activity 声明下列属性

  • android:supportsPictureInPicture="true"

  • android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"

<activity android:name=".PictureInPicture.PictureInPictureAcitivity"
  android:label="@string/picture_in_picture"
  android:exported="false"
  android:supportsPictureInPicture="true"
  android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
  android:resizeableActivity="true"
  android:launchMode="singleTask"
  tools:targetApi="24" />

2. 进入画中画模式

  • 部分设备可能无法使用画中画模式,因此进入画中画模式前需要调用hasSystemFeature接口进行检查。

  • 调用系统 API enterPictureInPictureMode进入画中画模式。

private void enterPIPMode(){
    if(!hasJoined) {
        ToastHelper.showToast(this, "请先加入频道", Toast.LENGTH_SHORT);
        return;
    }
    if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        if(getPackageManager().hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)) {
            Rational aspectRatio = new Rational(9, 16); // 推荐视频比例
            PictureInPictureParams params = new PictureInPictureParams.Builder()
            .setAspectRatio(aspectRatio)
            .build();
            enterPictureInPictureMode(params);
        } else {
            ToastHelper.showToast(this, "设备不支持画中画", Toast.LENGTH_SHORT);
        }
    } else {
        ToastHelper.showToast(this, "Android 8.0 以上才支持画中画", Toast.LENGTH_SHORT);
    }
}

3. 响应画中画变化

可以重写系统onPictureInPictureModeChanged回调并根据业务场景控制 UI 变化。

@Override
public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode, Configuration newConfig) {
    super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig);
    if (isInPictureInPictureMode) {
        // 进入 PIP:隐藏 全屏UI,只保留视频
        findViewById(R.id.ll_channel_layout).setVisibility(View.GONE);
        findViewById(R.id.scroll_remote_container).setVisibility(View.GONE);

        // 可选:隐藏状态栏
        getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE);

        // 可选:创建专用 PIP 视频容器
        showPipVideoContainer();
    } else {
        // 退出 PIP:恢复 全屏UI
        findViewById(R.id.ll_channel_layout).setVisibility(View.VISIBLE);
        findViewById(R.id.scroll_remote_container).setVisibility(View.VISIBLE);
        getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE);

        // 重新绑定本地预览视图
        if (mAliRtcEngine != null && mLocalVideoCanvas != null) {
            mAliRtcEngine.setLocalViewConfig(mLocalVideoCanvas, AliRtcVideoTrackCamera);
        }

        // 移除 PIP 容器
        removePipVideoContainer();
    }
}

/**
 * 显示 PIP 模式下的主视频容器(例如本地预览)
 */
private void showPipVideoContainer() {
    if (mPipVideoContainer == null) {
        mPipVideoContainer = new FrameLayout(this);
        mPipVideoContainer.setBackgroundColor(Color.BLACK);
        addContentView(mPipVideoContainer, new ViewGroup.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.MATCH_PARENT));

        // 重新创建本地预览视图(不能移动 SurfaceView,只能重建)
        SurfaceView surfaceView = mAliRtcEngine.createRenderSurfaceView(this);
        surfaceView.setZOrderOnTop(true);
        surfaceView.setZOrderMediaOverlay(true);

        mPipVideoContainer.addView(surfaceView, new FrameLayout.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.MATCH_PARENT));

        if(mAliRtcEngine != null) {
            AliRtcEngine.AliRtcVideoCanvas canvas = new AliRtcEngine.AliRtcVideoCanvas();
            canvas.view = surfaceView;
            mAliRtcEngine.setLocalViewConfig(canvas, AliRtcVideoTrackCamera);
        }
    }
}

/**
 * 移除 PIP 视频容器
 */
private void removePipVideoContainer() {
    if (mPipVideoContainer != null && mPipVideoContainer.getParent() != null) {
        ((ViewGroup) mPipVideoContainer.getParent()).removeView(mPipVideoContainer);
        mPipVideoContainer = null;
    }
}