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

Jetpack Compose中ExoPlayer视频源更新失效与点击拦截问题求助

问题解决:Jetpack Compose中ExoPlayer视频不更新与点击事件拦截问题

一、视频源切换后视图不更新的问题

核心原因

  1. ViewModel实例化错误:HomeScreen中直接通过val feedViewModel = FeedViewModel()创建实例,每次Compose重组都会生成新的ViewModel,导致currentSource状态无法在组件间共享,切换后的状态无法传递到VideoPlayer。
  2. AndroidView未响应状态变化:DisposableEffect未将currentSource作为依赖项,currentSource变化时,PlayerView不会更新关联的ExoPlayer实例。
  3. 初始值不一致:VideoPlayer中currentSource初始值用attachment.Video,但ViewModel的_currentSource初始为null,导致首次加载与后续切换的状态逻辑冲突。

解决方案

1. 正确获取ViewModel实例

在Compose中使用viewModel()函数获取ViewModel,确保整个界面共享同一个实例:

// HomeScreen中修改ViewModel获取方式
val feedViewModel: FeedViewModel = viewModel()

2. 让AndroidView响应currentSource变化

将currentSource作为DisposableEffect的依赖key,确保状态变化时更新PlayerView关联的Player:

@Composable
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
fun VideoPlayer(
    screenHeight: Dp, screenWidth: Dp, attachment: MediaAttachment, feedViewModel: FeedViewModel
) {
    val context = LocalContext.current
    val currentSource by feedViewModel.currentSource.observeAsState(initial = attachment.Video)

    // 依赖currentSource创建Player,状态变化时自动重建
    val exoPlayer = remember(currentSource) {
        ExoPlayer.Builder(context).build().apply {
            val defaultDataSourceFactory = DefaultDataSource.Factory(context)
            val dataSourceFactory: DataSource.Factory = DefaultDataSource.Factory(
                context, defaultDataSourceFactory
            )
            val source = ProgressiveMediaSource.Factory(dataSourceFactory)
                .createMediaSource(MediaItem.fromUri("https://senses.team/$currentSource"))

            setMediaSource(source)
            prepare()
            playWhenReady = true
            videoScalingMode = C.VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING
            repeatMode = Player.REPEAT_MODE_ONE
        }
    }

    // 以currentSource和exoPlayer为依赖,确保状态变化时更新PlayerView
    DisposableEffect(currentSource, exoPlayer) {
        val playerView = CustomExoPlayerView(context).apply {
            hideController()
            useController = false
            resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT
            player = exoPlayer
            layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
        }

        AndroidView(
            modifier = Modifier
                .height(screenHeight)
                .width(screenWidth)
                .background(Color.Black),
            factory = { playerView }
        )

        onDispose { exoPlayer.release() }
    }
}

3. 初始化ViewModel状态

在FeedViewModel中为_currentSource设置初始值,避免空状态问题:

class FeedViewModel : ViewModel() {
    // ... 其他代码
    private val _currentSource = MutableLiveData<String>("") // 或根据业务设置默认值
    val currentSource: LiveData<String> = _currentSource
    // ... 其他代码
}

二、PlayerView拦截点击事件,无法添加控制按钮的问题

核心原因

PlayerView默认会消费触摸事件,即使重写onTouchEvent返回false,仍可能阻断父视图或上层控件的事件传递。

解决方案

1. Compose布局叠加控制按钮

将VideoPlayer与控制按钮放在同一Box中,按钮层级高于PlayerView,规避事件拦截:

@Composable
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
fun VideoPlayerWithControls(
    screenHeight: Dp, screenWidth: Dp, attachment: MediaAttachment, feedViewModel: FeedViewModel
) {
    val context = LocalContext.current
    val currentSource by feedViewModel.currentSource.observeAsState(initial = attachment.Video)
    val exoPlayer = remember(currentSource) {
        // 初始化Player代码不变
        ExoPlayer.Builder(context).build().apply {
            val defaultDataSourceFactory = DefaultDataSource.Factory(context)
            val dataSourceFactory: DataSource.Factory = DefaultDataSource.Factory(
                context, defaultDataSourceFactory
            )
            val source = ProgressiveMediaSource.Factory(dataSourceFactory)
                .createMediaSource(MediaItem.fromUri("https://senses.team/$currentSource"))

            setMediaSource(source)
            prepare()
            playWhenReady = true
            videoScalingMode = C.VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING
            repeatMode = Player.REPEAT_MODE_ONE
        }
    }

    Box(modifier = Modifier.size(screenWidth, screenHeight)) {
        // 播放器视图
        DisposableEffect(currentSource, exoPlayer) {
            val playerView = CustomExoPlayerView(context).apply {
                hideController()
                useController = false
                resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT
                player = exoPlayer
                layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
            }

            AndroidView(
                modifier = Modifier.fillMaxSize(),
                factory = { playerView }
            )

            onDispose { exoPlayer.release() }
        }

        // 自定义播放/暂停按钮
        IconButton(
            modifier = Modifier
                .align(Alignment.BottomEnd)
                .padding(16.dp)
                .size(48.dp)
                .background(Color.Black.copy(alpha = 0.7f), CircleShape),
            onClick = {
                exoPlayer.playWhenReady = !exoPlayer.playWhenReady
            }
        ) {
            Icon(
                painter = painterResource(
                    id = if (exoPlayer.playWhenReady) R.drawable.ic_pause else R.drawable.ic_play
                ),
                contentDescription = if (exoPlayer.playWhenReady) "暂停" else "播放",
                tint = Color.White
            )
        }
    }
}

2. 调整PlayerView的事件传递逻辑

修改CustomExoPlayerView的事件拦截逻辑,允许事件向上传递:

@UnstableApi
internal class CustomExoPlayerView(
    context: Context, attributeSet: AttributeSet? = null,
) : PlayerView(context, attributeSet) {

    @SuppressLint("ClickableViewAccessibility")
    override fun onTouchEvent(event: MotionEvent): Boolean {
        when (event.action) {
            MotionEvent.ACTION_DOWN -> showController()
        }
        // 处理自身逻辑后,返回false让事件继续传递
        return false
    }

    override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
        // 不拦截触摸事件,允许父视图接收
        return false
    }
}

额外注意事项

  • 列表项状态提升:PostCard中的currentAttachmentIndex是局部状态,若需全局切换视频源,应将其移至FeedViewModel中管理。
  • 内存泄漏防护:确保在DisposableEffect的onDispose中正确释放ExoPlayer实例。
  • 线程安全:后台线程更新currentSource时,需使用_currentSource.postValue()而非直接赋值value。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.07 19:22:35