如何在 Leanback 中实现详情页的沉浸式播放体验?
解读
国内电视盒子与智能电视的 Launcher 大多基于 Leanback 架构,详情页(DetailsFragment)是用户决定是否播放的关键节点。所谓“沉浸式播放”包含两层诉求:
- 视觉沉浸:从详情页到播放器无闪屏、无 Launcher 导航栏残留,16:9 视频流能瞬间铺满屏幕,焦点逻辑无缝衔接。
- 交互沉浸:用户按“确定/OK”后 300 ms 内出第一帧;按“返回”可秒级回到详情页原焦点位;期间系统栏、虚拟按键、广告弹窗均被屏蔽,符合广电总局对电视 UI 的“一键退出”合规要求。
面试时,面试官想确认候选人是否真正在国产芯片(Amlogic、Rockchip、HiSilicon)平台上做过 Leanback 定制,是否了解国产 ROM 对 WindowType、InputEvent 的魔改,以及是否能把“沉浸式”拆成“窗口层+解码层+焦点层”逐层闭环。
知识点
- Leanback 的 Fragment 栈调度:DetailsFragment → PlaybackFragment 的切换不是 replace,而是使用 BackgroundManager 的 fade 机制,保持共享元素(海报、标题)在 GPU 层不动,避免 SurfaceFlinger 重排。
- 国产芯片的 SurfaceView 与 TextureView 差异:电视 SoC 的硬件合成器(HWC)对 SurfaceView 的 z-ordering 支持更好,但 TextureView 能响应动画缩放;沉浸式场景优先用 SurfaceView+OverlayView 组合,把 Surface 置于系统栏之下。
- 系统栏屏蔽:国内 ROM 把 STATUS_BAR 与 NAVIGATION_BAR 合并为“系统遮罩”,需同时设置 WindowManager.LayoutParams.privateFlags |= 0x00000040(华为)(或 0x00000020 小米)才能全沉浸;此外要调用 TvInputManager.hideInputOverlay() 屏蔽运营商广告条。
- 焦点防丢:电视遥控器事件走 InputDispatcher → ViewRootImpl,PlaybackFragment onCreate 时主动 requestFocus() 并注册 OnKeyInterceptListener,防止国产 Launcher 把返回键截去做“一键清理”。
- 解码零帧延迟:MediaCodec 在电视平台必须配置为“隧道模式(TUNNEL)”,把 OMX.google.android.exoplayer.tunnel 置 true,让解码器直接输出到 HDMI,绕开 SurfaceFlinger,达到 200 ms 内首帧;同时用 AudioManager.requestAudioFocus(..., AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK) 应对语音助手抢焦点。
- 合规退出:广电总局要求任何全屏场景必须 1 s 内响应“返回”键回到可退出状态;因此要在 PlaybackFragment 的 onKey() 里优先消费 KEYCODE_BACK,并调用 finishAffinity() 而非 moveTaskToBack(),防止国产 ROM 把应用压后台后弹出“猜你喜欢”插屏。
答案
步骤拆解(可直接在面试白板手写):
-
布局层 在 res/layout-v26/fragment_details.xml 中,把播放器占位 View 设为
SurfaceView@+id/surface,宽高 match_parent,z-order 置顶;同层放DetailsOverviewRow的共享元素,用android:transitionName="shared_poster"标记。 -
窗口层 在 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 背景置空,防止闪白 -
切换动画 使用
DetailsFragmentBackgroundController的setSolidColor(android.R.color.black)把共享海报快速淡入黑色,时长 150 ms;同时调用PlaybackFragment.setHostCallback(mHostCallback),在onCreate()中通过setEnterTransition(new Fade())把 Fragment 切换时间降到 100 ms 以内。 -
解码层 在 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。 -
焦点与按键 重写
PlaybackFragment.onKey():if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_UP) { // 合规退出 getActivity().finishAffinity(); return true; }同时在
onResume()里mSurfaceView.requestFocus(),防止国产遥控器把焦点移到广告视图。 -
内存与待机 在
onPause()中player.setPlayWhenReady(false)并释放 AudioFocus;在onDestroy()中把 SurfaceView 的 Surface 置 null,避免某些国产固件在待机唤醒后重复创建 Surface 导致黑屏。
通过以上 6 步,即可在 Leanback 详情页实现国产电视合规、无闪屏、200 ms 首帧的沉浸式播放体验。
拓展思考
- 折叠屏与投影仪场景:如果未来 Leanback 扩展到折叠屏或智能投影仪,Surface 的宽高比会动态变化,需监听
DisplayManager.DisplayListener并在onDisplayChanged()中重新计算AspectRatioFrameLayout的 resize 模式,防止视频被裁。 - 广告贴片合规:国内 OTT 牌照方要求“先审后播”,沉浸式播放前必须插入 5 秒牌照广告;可把 ExoPlayer 的
AdsMediaSource与TvInputManager的TvAdService打通,利用隧道模式把广告与正片同层输出,减少一次 Surface 切换。 - 语音搜片打断:遥控器语音键会触发系统
VoiceInteractionSession,此时应通过AudioManager.registerAudioRecordingCallback()监听录音状态,一旦检测到语音交互,立即player.setPlayWhenReady(false)并保存当前位置,交互结束后无缝续播,保持沉浸感不被打断。