You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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范围内:

  1. 创建自定义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
    }
}
  1. 在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.28 23:31:06