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

Jetpack Compose中ExoPlayer Media3视频播放器视图重叠与显示异常

HorizontalPager中VideoPlayerView问题修复

问题1:切换页面后播放器显示错误视频

原因

当前代码里remember{ createExoPlayer(context, uri) }未将uri作为记忆依赖键,导致不同页面的VideoPlayerView复用同一个ExoPlayer实例,媒体源没有随uri更新;同时AndroidView也未根据uri变化调整播放器内容。

修复方案

  1. 给remember添加uri作为依赖键,确保不同uri对应独立的ExoPlayer实例:
val exoPlayer = remember(uri) { createExoPlayer(context, uri) }
  1. 给AndroidView设置key = uri,保证uri变化时重建播放器视图;同时在update块中新增媒体源校验逻辑,避免复用错误内容:
AndroidView(
    factory = { context ->
        // 原有PlayerView创建代码
    },
    update = { playerView ->
        // 新增:校验当前媒体源,不匹配则更新
        val currentMediaItem = playerView.player?.currentMediaItem
        if (currentMediaItem?.mediaId != uri) {
            val source = ProgressiveMediaSource.Factory(DefaultDataSource.Factory(context))
                .createMediaSource(MediaItem.fromUri(uri))
            exoPlayer.setMediaSource(source)
            exoPlayer.prepare()
        }
        // 原有生命周期处理代码
    },
    key = uri, // 新增:uri变化时重建视图
    modifier = modifier.fillMaxSize()
)
  1. 移除setKeepContentOnPlayerReset(true),避免播放器重置后残留旧视频画面。

问题2:视频缩放裁剪后溢出Pager页面

原因

视频启用裁剪缩放模式后,超出了Pager页面的边界,且播放器容器未设置裁剪限制,导致内容溢出。

修复方案

  1. 给播放器容器Box添加clip(RectangleShape)修饰符,强制裁剪超出容器范围的内容:
Box(
    modifier = Modifier
        .clip(RectangleShape) // 新增:裁剪溢出内容
        .onGloballyPositioned { /* 原有定位逻辑 */ },
    contentAlignment = Alignment.Center
)
  1. 调整PlayerView的resizeMode为RESIZE_MODE_FILL,配合ExoPlayer的裁剪缩放模式,确保视频填充容器同时不溢出:
val playerView = PlayerView(context).apply {
    // 原有设置
    resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FILL
    // 原有设置
}
  1. 确保VideoPlayerView的宽度被限制为Pager页面宽度,调用时直接使用fillMaxWidth()即可(HorizontalPager默认每个页面占满屏幕宽度)。

修改后的完整VideoPlayerView代码

@OptIn(UnstableApi::class) @Composable
fun VideoPlayerView(
    uri: String,
    modifier: Modifier = Modifier
) {
    val context = LocalContext.current
    var isReady by remember { mutableStateOf(false) }

    val model = ImageRequest.Builder(context)
        .data(uri)
        .videoFrameMillis(10000)
        .decoderFactory { result, options, _ ->
            VideoFrameDecoder(
                result.source,
                options
            )
        }
        .build()

    // 修复问题1:添加uri作为remember依赖键
    val exoPlayer = remember(uri) { createExoPlayer(context, uri) }

    exoPlayer.videoScalingMode = C.VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING
    exoPlayer.repeatMode = Player.REPEAT_MODE_ONE

    exoPlayer.addListener(object : Player.Listener {
        override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {
            isReady = playWhenReady
            super.onPlayWhenReadyChanged(playWhenReady, reason)
        }
    })

    var lifecycle by remember { mutableStateOf(Lifecycle.Event.ON_CREATE) }
    val lifecycleOwner = LocalLifecycleOwner.current
    DisposableEffect(lifecycleOwner) {
        val observer = LifecycleEventObserver { _, event ->
            lifecycle = event
        }
        lifecycleOwner.lifecycle.addObserver(observer)

        onDispose {
            lifecycleOwner.lifecycle.removeObserver(observer)
            exoPlayer.release()
        }
    }

    Box(
        modifier = modifier
            // 修复问题2:添加裁剪修饰符
            .clip(RectangleShape)
            .onGloballyPositioned { coordinates ->
                val parentRect = coordinates.boundsInParent()
                val isFullyVisible = parentRect.contains(coordinates.positionInWindow())

                if (isFullyVisible) {
                    exoPlayer.play()
                } else {
                    exoPlayer.pause()
                }
            },
        contentAlignment = Alignment.Center
    ) {
        if (exoPlayer.isLoading) {
            AsyncImage(
                modifier = Modifier.fillMaxSize(),
                model = model,
                contentDescription = "video thumbnail",
                contentScale = ContentScale.Crop
            )
        } else {
            AndroidView(
                factory = { context ->
                    val playerView = PlayerView(context).apply {
                        hideController()
                        useController = false
                        player = exoPlayer
                        // 修复问题2:修改resizeMode为FILL
                        resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FILL
                        layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
                        // 移除旧画面保留设置
                    }
                    playerView
                },
                update = { playerView ->
                    // 修复问题1:校验并更新媒体源
                    val currentMediaItem = playerView.player?.currentMediaItem
                    if (currentMediaItem?.mediaId != uri) {
                        val source = ProgressiveMediaSource.Factory(DefaultDataSource.Factory(context))
                            .createMediaSource(MediaItem.fromUri(uri))
                        exoPlayer.setMediaSource(source)
                        exoPlayer.prepare()
                    }

                    when (lifecycle) {
                        Lifecycle.Event.ON_PAUSE -> {
                            playerView.onPause()
                            playerView.player?.pause()
                        }
                        Lifecycle.Event.ON_RESUME -> {
                            playerView.onResume()
                        }
                        else -> Unit
                    }
                },
                // 修复问题1:添加uri作为AndroidView的键
                key = uri,
                modifier = Modifier.fillMaxSize()
            )
        }
    }
}

@OptIn(UnstableApi::class)
private fun createExoPlayer(context: Context, uri: String): ExoPlayer {
    return ExoPlayer.Builder(context)
        .build()
        .apply {
            val dataSourceFactory: DataSource.Factory = DefaultDataSource.Factory(context)
            val source = ProgressiveMediaSource.Factory(dataSourceFactory)
                .createMediaSource(MediaItem.fromUri(uri))

            setMediaSource(source)
            prepare()
        }
}

内容的提问来源于stack exchange,提问作者Jeevan Rupacha

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.03 16:44:52