Android Jetpack Compose中ExoPlayer非全屏时弹窗超出播放器范围问题
ExoPlayer默认弹窗显示在播放器外部的解决方案
问题描述
在Android Jetpack Compose中集成ExoPlayer并使用默认控件时,横屏模式下如果播放器未占满屏幕宽度,播放速度和音频选择的默认弹窗会显示在播放器外部,需要让弹窗始终显示在播放器内部。
用户提供的播放器代码:
@Composable fun XpPlayer() { val context = LocalContext.current fun buildCustomDrmSessionManager(): DrmSessionManager { val userAgent = "1" val dataSourceFactory = DefaultHttpDataSource.Factory().setUserAgent(userAgent) .setDefaultRequestProperties(mapOf("X-AxDRM-Message" to "")) val drmCallback: MediaDrmCallback = HttpMediaDrmCallback("", dataSourceFactory) val exoMediaDrmProvider: ExoMediaDrm.Provider = FrameworkMediaDrm.DEFAULT_PROVIDER return DefaultDrmSessionManager.Builder() .setUuidAndExoMediaDrmProvider(C.WIDEVINE_UUID, exoMediaDrmProvider).build(drmCallback) } fun buildDrmMediaItemWithDrmConfig(): MediaItem { return MediaItem.fromUri("https://travelxp.s.llnwi.net/watch1/61025c11781ce3c543f5abcd/manifest_v4.mpd") } val customDrmSessionManager = buildCustomDrmSessionManager() val mediaSourceFactory = DefaultMediaSourceFactory(context).setDrmSessionManagerProvider { customDrmSessionManager } val loadControl = DefaultLoadControl.Builder().setBufferDurationsMs( 10000, 20000, 2000, 10000 ).build() val exoPlayer = remember { ExoPlayer.Builder(context, mediaSourceFactory).setLoadControl(loadControl).build().also { it.playWhenReady = true }.apply { setMediaItem(buildDrmMediaItemWithDrmConfig()) prepare() } } DisposableEffect(Unit) { onDispose { exoPlayer.release() } } AndroidView(modifier = Modifier .border(2.dp, Color.Red) .width(800.dp).aspectRatio(16/9f), factory = { PlayerView(context).apply { useController = true resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FILL player = exoPlayer setShowBuffering(PlayerView.SHOW_BUFFERING_WHEN_PLAYING) hideController() layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT) } }) }
解决方案
方案一:自定义PlayerControlView绑定弹窗锚点
通过自定义PlayerControlView,强制将弹窗的锚点绑定到播放器的内部容器,确保弹窗始终显示在PlayerView范围内:
- 创建自定义ControlView类:
class CustomPlayerControlView(context: Context) : PlayerControlView(context) { override fun createPopup(): PopupWindow { val popup = super.createPopup() // 将弹窗锚点设置为当前ControlView的父容器(即PlayerView的核心容器) popup.anchorView = this.parent as View return popup } }
- 在Compose的AndroidView中替换默认ControlView:
修改原代码中PlayerView初始化部分,添加setControlView设置自定义控件:
AndroidView(modifier = Modifier .border(2.dp, Color.Red) .width(800.dp).aspectRatio(16/9f), factory = { context -> PlayerView(context).apply { useController = true // 设置自定义ControlView setControlView(CustomPlayerControlView(context)) resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FILL player = exoPlayer setShowBuffering(PlayerView.SHOW_BUFFERING_WHEN_PLAYING) hideController() layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT) } })
方案二:通过反射修改弹窗窗口属性
如果不想自定义控件,可以通过反射获取默认弹窗并修改其窗口类型,强制弹窗依附于PlayerView:
AndroidView(modifier = Modifier .border(2.dp, Color.Red) .width(800.dp).aspectRatio(16/9f), factory = { context -> PlayerView(context).apply { useController = true resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FILL player = exoPlayer setShowBuffering(PlayerView.SHOW_BUFFERING_WHEN_PLAYING) hideController() layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT) // 反射获取弹窗并修改属性 controlView?.let { controlView -> try { val popupField = PlayerControlView::class.java.getDeclaredField("popupWindow") popupField.isAccessible = true val popupWindow = popupField.get(controlView) as PopupWindow // 设置弹窗为应用子面板类型,确保依附于PlayerView popupWindow.windowLayoutType = WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL // 开启裁剪,限制弹窗在PlayerView范围内 popupWindow.isClippingEnabled = true } catch (e: Exception) { e.printStackTrace() } } } })
注意事项
- 方案一为官方推荐的规范方式,避免反射带来的版本兼容性问题
- 确保PlayerView的
layoutParams设置为MATCH_PARENT,让内部控件能正确计算边界 - 测试时需覆盖横屏、竖屏及不同屏幕尺寸场景,验证弹窗显示位置
内容的提问来源于stack exchange,提问作者siddhartha verma
相关产品推荐
相关产品推荐

