如何在 Leanback 中实现详情页的沉浸式播放体验?

解读

国内电视盒子与智能电视的 Launcher 大多基于 Leanback 架构,详情页(DetailsFragment)是用户决定是否播放的关键节点。所谓“沉浸式播放”包含两层诉求:

  1. 视觉沉浸:从详情页到播放器无闪屏、无 Launcher 导航栏残留,16:9 视频流能瞬间铺满屏幕,焦点逻辑无缝衔接。
  2. 交互沉浸:用户按“确定/OK”后 300 ms 内出第一帧;按“返回”可秒级回到详情页原焦点位;期间系统栏、虚拟按键、广告弹窗均被屏蔽,符合广电总局对电视 UI 的“一键退出”合规要求。

面试时,面试官想确认候选人是否真正在国产芯片(Amlogic、Rockchip、HiSilicon)平台上做过 Leanback 定制,是否了解国产 ROM 对 WindowType、InputEvent 的魔改,以及是否能把“沉浸式”拆成“窗口层+解码层+焦点层”逐层闭环。

知识点

  1. Leanback 的 Fragment 栈调度:DetailsFragment → PlaybackFragment 的切换不是 replace,而是使用 BackgroundManager 的 fade 机制,保持共享元素(海报、标题)在 GPU 层不动,避免 SurfaceFlinger 重排。
  2. 国产芯片的 SurfaceView 与 TextureView 差异:电视 SoC 的硬件合成器(HWC)对 SurfaceView 的 z-ordering 支持更好,但 TextureView 能响应动画缩放;沉浸式场景优先用 SurfaceView+OverlayView 组合,把 Surface 置于系统栏之下。
  3. 系统栏屏蔽:国内 ROM 把 STATUS_BAR 与 NAVIGATION_BAR 合并为“系统遮罩”,需同时设置 WindowManager.LayoutParams.privateFlags |= 0x00000040(华为)(或 0x00000020 小米)才能全沉浸;此外要调用 TvInputManager.hideInputOverlay() 屏蔽运营商广告条。
  4. 焦点防丢:电视遥控器事件走 InputDispatcher → ViewRootImpl,PlaybackFragment onCreate 时主动 requestFocus() 并注册 OnKeyInterceptListener,防止国产 Launcher 把返回键截去做“一键清理”。
  5. 解码零帧延迟:MediaCodec 在电视平台必须配置为“隧道模式(TUNNEL)”,把 OMX.google.android.exoplayer.tunnel 置 true,让解码器直接输出到 HDMI,绕开 SurfaceFlinger,达到 200 ms 内首帧;同时用 AudioManager.requestAudioFocus(..., AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK) 应对语音助手抢焦点。
  6. 合规退出:广电总局要求任何全屏场景必须 1 s 内响应“返回”键回到可退出状态;因此要在 PlaybackFragment 的 onKey() 里优先消费 KEYCODE_BACK,并调用 finishAffinity() 而非 moveTaskToBack(),防止国产 ROM 把应用压后台后弹出“猜你喜欢”插屏。

答案

步骤拆解(可直接在面试白板手写):

  1. 布局层 在 res/layout-v26/fragment_details.xml 中,把播放器占位 View 设为 SurfaceView@+id/surface,宽高 match_parent,z-order 置顶;同层放 DetailsOverviewRow 的共享元素,用 android:transitionName="shared_poster" 标记。

  2. 窗口层 在 DetailsFragment 的 onCreate() 里:

    getActivity().getWindow().getDecorView().setSystemUiVisibility(
        View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
        | View.SYSTEM_UI_FLAG_FULLSCREEN
        | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
        | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
        | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
    WindowManager.LayoutParams lp = getActivity().getWindow().getAttributes();
    lp.privateFlags |= 0x00000040; // 华为/荣耀隐藏系统遮罩
    getActivity().getWindow().setAttributes(lp);
    BackgroundManager.getInstance(getActivity()).setDrawable(null); // 把 Leanback 背景置空,防止闪白
    
  3. 切换动画 使用 DetailsFragmentBackgroundControllersetSolidColor(android.R.color.black) 把共享海报快速淡入黑色,时长 150 ms;同时调用 PlaybackFragment.setHostCallback(mHostCallback),在 onCreate() 中通过 setEnterTransition(new Fade()) 把 Fragment 切换时间降到 100 ms 以内。

  4. 解码层 在 PlaybackFragment 创建 ExoPlayer 时:

    DefaultRenderersFactory factory =
        new DefaultRenderersFactory(getContext())
            .setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER)
            .setMediaCodecSelector(new TvMediaCodecSelector()); // 自选隧道解码器
    player = new ExoPlayer.Builder(getContext(), factory)
        .setAudioAttributes(new AudioAttributes.Builder()
            .setUsage(C.USAGE_MEDIA)
            .setContentType(C.CONTENT_TYPE_MOVIE)
            .build(), true)
        .build();
    player.setVideoSurfaceView(mSurfaceView);
    

    并在 onPlayWhenReadyChanged() 里记录首帧时间戳,确保 <300 ms。

  5. 焦点与按键 重写 PlaybackFragment.onKey()

    if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_UP) {
        // 合规退出
        getActivity().finishAffinity();
        return true;
    }
    

    同时在 onResume()mSurfaceView.requestFocus(),防止国产遥控器把焦点移到广告视图。

  6. 内存与待机 在 onPause()player.setPlayWhenReady(false) 并释放 AudioFocus;在 onDestroy() 中把 SurfaceView 的 Surface 置 null,避免某些国产固件在待机唤醒后重复创建 Surface 导致黑屏。

通过以上 6 步,即可在 Leanback 详情页实现国产电视合规、无闪屏、200 ms 首帧的沉浸式播放体验。

拓展思考

  1. 折叠屏与投影仪场景:如果未来 Leanback 扩展到折叠屏或智能投影仪,Surface 的宽高比会动态变化,需监听 DisplayManager.DisplayListener 并在 onDisplayChanged() 中重新计算 AspectRatioFrameLayout 的 resize 模式,防止视频被裁。
  2. 广告贴片合规:国内 OTT 牌照方要求“先审后播”,沉浸式播放前必须插入 5 秒牌照广告;可把 ExoPlayer 的 AdsMediaSourceTvInputManagerTvAdService 打通,利用隧道模式把广告与正片同层输出,减少一次 Surface 切换。
  3. 语音搜片打断:遥控器语音键会触发系统 VoiceInteractionSession,此时应通过 AudioManager.registerAudioRecordingCallback() 监听录音状态,一旦检测到语音交互,立即 player.setPlayWhenReady(false) 并保存当前位置,交互结束后无缝续播,保持沉浸感不被打断。