16、多窗口模式:分屏与画中画模式在TV上的适配

多窗口模式,说白了就是让电视屏幕同时干几件事。

你想想看,用户一边看球赛,一边刷社交媒体,或者视频通话时把画面缩小到角落。这在手机平板上已经很常见了。但到了电视上,情况就完全不一样了。

我个人习惯把电视的多窗口分成两类:分屏模式画中画模式。这两者在Android TV上的适配思路完全不同,坑也各有各的深。

16.1 分屏模式:电视上的“左右互搏”

分屏模式在Android里叫multi-window模式。电视上触发分屏的方式跟手机不一样——通常是通过系统级的快捷键或者遥控器上的特定按键。

我在项目中遇到过一个问题:用户按了分屏键后,我们的视频播放器直接崩溃了。原因很简单——我们没有处理onMultiWindowModeChanged回调。

核心要点:分屏模式下,Activity的生命周期会重新走一遍。你的布局会被压缩到屏幕的一半甚至更小。

适配分屏模式,你需要做这几件事:

  1. 在Manifest中声明支持android:resizeableActivity="true"
  2. 处理配置变更:屏幕尺寸、方向、可用宽度都会变
  3. 调整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模式,核心步骤是:

  1. 在Manifest中声明PiP支持android:supportsPictureInPicture="true"
  2. 实现PiP回调onPictureInPictureModeChanged
  3. 配置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 布局适配实战

多窗口模式下,布局适配是最头疼的。电视的布局通常是为大屏设计的,一旦窗口变小,各种问题就来了。

我总结了一套“三明治适配法”:

  1. 顶层:用ConstraintLayout,让控件自动适应
  2. 中间层:用配置限定符(layout-w600dp等),不同窗口宽度用不同布局
  3. 底层:代码动态调整,监听窗口尺寸变化

举个例子,一个视频播放页面的布局:

<!-- 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 知识体系总览

下面这张图总结了多窗口模式适配的核心知识点:

多窗口模式适配知识体系 分屏模式 (Multi-Window) 画中画模式 (PiP) resizeableActivity 配置变更处理 布局适配 生命周期管理 supportsPiP声明 宽高比配置 交互控件 状态恢复 核心原则 能画中画就别分屏 · 布局独立适配 · 状态完整保存 实践建议 使用ConstraintLayout + 配置限定符 + 代码动态调整 三明治适配法

16.7 避坑指南

最后,分享几个我踩过的坑:

我曾经... 在分屏模式下用了一个全屏弹窗,结果弹窗把另一个窗口的内容也盖住了。后来才知道,多窗口模式下弹窗要设置FLAG_ACTIVITY_NEW_TASK,并且指定显示在哪个窗口。

其他常见问题:

  • SurfaceView黑屏:多窗口模式下SurfaceView的渲染会出问题。改用TextureView或者Google的ExoPlayer,它内部处理了这些情况。
  • 焦点混乱:两个窗口都有可聚焦控件时,遥控器按方向键可能跳到另一个窗口。用android:nextFocus*属性明确指定焦点顺序。
  • 音频冲突:两个App同时播放音频。你的App要监听音频焦点变化,失去焦点时暂停或降低音量。
  • 内存不足:多窗口模式下系统可能杀掉后台进程。做好onSaveInstanceState的保存工作。

嗯,多窗口适配说难不难,说简单也不简单。核心就是一句话:把你的App当成一个随时可能被压缩、被暂停、被恢复的“小强”来设计。做好状态保存和恢复,布局灵活适配,基本上就不会出大问题。

公众号:蓝海资料掘金营,微信deep3321