如何在不离开App的前提下实现部分界面PiP画中画模式
App内画中画(类Google Meet前台小窗)实现方案
你要实现的App处于前台时的小窗悬浮效果,和系统级退后台触发的PiP不是同一套技术方案,不要直接调用系统PiP API实现前台效果——系统PiP会强制将整个Activity压缩为系统级悬浮窗,无法做到App内同时展示全屏内容+小窗摄像头画面的效果。Google Meet的前台小窗是纯应用内自定义View实现的,完全适配你当前单Fragment通过显隐切换全屏的现有架构,具体实现逻辑如下:
核心实现步骤
- 调整根布局层级
将当前Fragment的根布局替换为FrameLayout作为最底层容器,内部叠放两层子布局:- 第一层:全屏内容容器,默认隐藏,用来承载你要切换的独立全屏视图
- 第二层:悬浮小窗容器,默认隐藏,固定在布局最上层(设置更高的
elevation值避免被遮挡),用来承载摄像头渲染画面
初始状态下摄像头渲染View放在普通通话界面的对应位置即可,不需要改动原有通话逻辑。
- 全屏切换时动态转移摄像头渲染View
当用户触发进入全屏的操作时,不要销毁、重建摄像头采集流,仅做View的父容器转移:- 将摄像头渲染View从原通话布局的位置移除,挂载到上层悬浮小窗容器中,设置小窗固定尺寸(常见尺寸为120dp*160dp),默认摆放在屏幕右上角预留安全边距
- 展示底层全屏容器,加载对应的全屏内容
参考实现代码:
注意:全程不要中断摄像头采集、不要重新绑定渲染Surface,仅转移View父容器不会造成画面黑屏、中断,性能损耗极低。// 转移摄像头渲染View到小窗容器 val cameraRenderView = findViewById<View>(R.id.camera_render_view) (cameraRenderView.parent as ViewGroup).removeView(cameraRenderView) val pipContainer = findViewById<FrameLayout>(R.id.pip_container) pipContainer.addView(cameraRenderView, FrameLayout.LayoutParams( 120.dpToPx(), 160.dpToPx() ).apply { gravity = Gravity.END or Gravity.TOP marginEnd = 16.dpToPx() topMargin = 48.dpToPx() }) // 展示全屏内容 findViewById<View>(R.id.fullscreen_container).visibility = View.VISIBLE pipContainer.visibility = View.VISIBLE - 给小窗添加基础交互
给悬浮小窗容器添加触摸事件处理,实现两个核心交互即可:- 支持手指拖拽移动小窗位置,松手后自动吸附到屏幕左右边缘
- 点击小窗时退出全屏模式:将摄像头渲染View重新挂载回原通话布局位置,隐藏小窗容器和全屏内容容器,回到初始通话界面
- 对接系统PiP做退后台兜底
如果用户在小窗+全屏展示状态下按Home键退到后台,此时再调用系统PiP API,将通话流、摄像头画面绑定到系统PiP窗口,实现前后台场景的体验无缝衔接。
常见踩坑点
- 摄像头渲染优先选用
TextureView/SurfaceView,这两类系统渲染View支持动态切换父容器不中断渲染;如果使用自定义View做渲染,需要提前确认切换父容器时不会触发资源销毁逻辑。 - 不要在App前台状态下调用系统
enterPictureInPictureMode方法,该方法会强制将整个应用退到系统PiP窗口,和你要的前台同屏展示效果完全冲突。 - 小窗容器要设置足够高的
elevation值,避免被全屏内容内的其他控件遮挡。

内容的提问来源于stack exchange,提问作者Ivan Šimović
相关产品推荐
相关产品推荐

