16、多窗口模式:分屏与画中画模式在TV上的适配
多窗口模式,说白了就是让电视屏幕同时干几件事。
你想想看,用户一边看球赛,一边刷社交媒体,或者视频通话时把画面缩小到角落。这在手机平板上已经很常见了。但到了电视上,情况就完全不一样了。
我个人习惯把电视的多窗口分成两类:分屏模式和画中画模式。这两者在Android TV上的适配思路完全不同,坑也各有各的深。
16.1 分屏模式:电视上的“左右互搏”
分屏模式在Android里叫multi-window模式。电视上触发分屏的方式跟手机不一样——通常是通过系统级的快捷键或者遥控器上的特定按键。
我在项目中遇到过一个问题:用户按了分屏键后,我们的视频播放器直接崩溃了。原因很简单——我们没有处理onMultiWindowModeChanged回调。
核心要点:分屏模式下,Activity的生命周期会重新走一遍。你的布局会被压缩到屏幕的一半甚至更小。
适配分屏模式,你需要做这几件事:
- 在Manifest中声明支持:
android:resizeableActivity="true" - 处理配置变更:屏幕尺寸、方向、可用宽度都会变
- 调整UI布局:大屏下的复杂布局在分屏时可能完全不可用
嗯,这里要注意。电视的分屏跟手机不一样。电视屏幕虽然大,但用户离得远。分屏后每个窗口的实际可用面积可能比手机还小。你想想看,一个55寸电视分成两半,每半也就27.5寸,但用户坐在3米外看——这跟拿着手机在眼前看完全是两码事。
我建议你在分屏模式下做两件事:
- 增大字体和控件尺寸:分屏后窗口变小,但用户的观看距离没变。字体至少要比手机大2-3倍。
- 简化交互层级:分屏模式下不要展示复杂的多级菜单。用户操作起来很痛苦。
我的小技巧:在onMultiWindowModeChanged里判断当前窗口宽度,如果小于某个阈值(比如600dp),就切换到“精简模式”。隐藏次要信息,只保留核心功能。
16.2 画中画模式:小窗口里的大学问
画中画模式,也就是PiP(Picture-in-Picture)。这个在电视上其实比分屏更常见。你看直播时切出去看别的,直播画面缩到角落继续播放——这就是PiP。
Android从8.0开始支持PiP,但电视上的PiP适配跟手机有很大区别。我记得有一次,我们的视频App在手机上PiP跑得好好的,上了电视就黑屏。查了半天,原来是电视的SurfaceView渲染方式跟手机不一样。
踩坑警告:电视上的PiP窗口默认是固定大小的。不同厂商的电视,PiP窗口大小和位置都不一样。不要硬编码尺寸。
适配PiP模式,核心步骤是:
- 在Manifest中声明PiP支持:
android:supportsPictureInPicture="true" - 实现PiP回调:
onPictureInPictureModeChanged - 配置PiP参数:宽高比、交互控件、更新策略
来看一段核心代码:
// 进入画中画模式
private void enterPipMode() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
PictureInPictureParams.Builder builder = new PictureInPictureParams.Builder();
// 设置宽高比,电视上建议16:9
Rational aspectRatio = new Rational(16, 9);
builder.setAspectRatio(aspectRatio);
// 设置交互控件
builder.setActions(createPipActions());
// 进入PiP
enterPictureInPictureMode(builder.build());
}
}
// 处理PiP状态变化
@Override
public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode, Configuration newConfig) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig);
if (isInPictureInPictureMode) {
// 切换到PiP布局,隐藏不必要的控件
showPipLayout();
} else {
// 恢复到全屏布局
showFullLayout();
}
}
这里有个关键点:PiP窗口里的交互控件。电视遥控器操作跟触摸屏完全不同。你不能放一个“关闭按钮”让用户去点——遥控器没法精确点击那么小的区域。
我建议的做法是:
- PiP窗口只保留2-3个核心操作:播放/暂停、关闭、切换回全屏
- 所有操作通过遥控器的方向键和确认键完成,不要依赖触摸
- 控件要足够大,至少48dp以上
16.3 两种模式的对比与选择
分屏和画中画,什么时候用哪个?
我个人经验是这样的:
| 场景 | 推荐模式 | 原因 |
|---|---|---|
| 视频+聊天 | 画中画 | 视频是主内容,聊天是辅助 |
| 两个视频对比 | 分屏 | 两个内容同等重要,需要同时观看 |
| 视频+浏览菜单 | 画中画 | 视频持续播放,用户偶尔操作菜单 |
| 游戏+攻略 | 分屏 | 需要频繁切换视线,分屏更自然 |
你想想看,电视上用户的操作成本比手机高得多。手机点一下就行,电视要按好几下遥控器。所以能画中画就别分屏——减少用户的操作负担。
16.4 生命周期与状态管理
多窗口模式下,Activity的生命周期变得很复杂。我曾经被这个问题坑过——用户从分屏切回全屏,播放器状态全丢了。
核心规则是:
- 进入多窗口模式:Activity会走onPause → onStop → onStart → onResume
- 退出多窗口模式:同样的流程再来一遍
- 用户切换焦点窗口:另一个窗口的Activity会走onPause
记住:不要依赖onCreate/onDestroy来保存播放状态。用onSaveInstanceState和onRestoreInstanceState,或者ViewModel。
我建议的做法是:
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
// 保存播放进度、音量、字幕状态等
outState.putLong("playback_position", mPlayer.getCurrentPosition());
outState.putBoolean("is_playing", mPlayer.isPlaying());
}
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
if (savedInstanceState != null) {
long position = savedInstanceState.getLong("playback_position", 0);
boolean isPlaying = savedInstanceState.getBoolean("is_playing", false);
// 恢复播放状态
restorePlaybackState(position, isPlaying);
}
}
16.5 布局适配实战
多窗口模式下,布局适配是最头疼的。电视的布局通常是为大屏设计的,一旦窗口变小,各种问题就来了。
我总结了一套“三明治适配法”:
- 顶层:用ConstraintLayout,让控件自动适应
- 中间层:用配置限定符(layout-w600dp等),不同窗口宽度用不同布局
- 底层:代码动态调整,监听窗口尺寸变化
举个例子,一个视频播放页面的布局:
<!-- res/layout/activity_player.xml -->
<!-- 全屏模式:视频占大部分,下方是控制栏和推荐列表 -->
<androidx.constraintlayout.widget.ConstraintLayout>
<!-- 视频播放器 -->
<SurfaceView android:id="@+id/player_view"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/controls"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_width="0dp"
android:layout_height="0dp"/>
<!-- 控制栏 -->
<LinearLayout android:id="@+id/controls"
app:layout_constraintBottom_toTopOf="@id/recommendations"
.../>
<!-- 推荐列表 -->
<RecyclerView android:id="@+id/recommendations"
app:layout_constraintBottom_toBottomOf="parent"
.../>
</androidx.constraintlayout.widget.ConstraintLayout>
<!-- res/layout-w600dp/activity_player.xml -->
<!-- 分屏模式:视频占上半部分,控制栏和推荐列表合并到下半部分 -->
<LinearLayout android:orientation="vertical">
<SurfaceView android:id="@+id/player_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
<!-- 控制栏和推荐列表合并 -->
<LinearLayout android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<!-- 控制栏 -->
<LinearLayout android:id="@+id/controls"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"/>
<!-- 推荐列表精简版 -->
<RecyclerView android:id="@+id/recommendations"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"/>
</LinearLayout>
</LinearLayout>
我的经验:不要试图用一个布局适配所有模式。分屏和全屏的交互逻辑完全不同,用不同的布局文件反而更清晰。代码里判断一下当前模式,setContentView不同的布局就行。
16.6 知识体系总览
下面这张图总结了多窗口模式适配的核心知识点:
16.7 避坑指南
最后,分享几个我踩过的坑:
我曾经... 在分屏模式下用了一个全屏弹窗,结果弹窗把另一个窗口的内容也盖住了。后来才知道,多窗口模式下弹窗要设置FLAG_ACTIVITY_NEW_TASK,并且指定显示在哪个窗口。
其他常见问题:
- SurfaceView黑屏:多窗口模式下SurfaceView的渲染会出问题。改用TextureView或者Google的ExoPlayer,它内部处理了这些情况。
- 焦点混乱:两个窗口都有可聚焦控件时,遥控器按方向键可能跳到另一个窗口。用
android:nextFocus*属性明确指定焦点顺序。 - 音频冲突:两个App同时播放音频。你的App要监听音频焦点变化,失去焦点时暂停或降低音量。
- 内存不足:多窗口模式下系统可能杀掉后台进程。做好onSaveInstanceState的保存工作。
嗯,多窗口适配说难不难,说简单也不简单。核心就是一句话:把你的App当成一个随时可能被压缩、被暂停、被恢复的“小强”来设计。做好状态保存和恢复,布局灵活适配,基本上就不会出大问题。