Jetpack Compose中ExoPlayer视频源更新失效与点击拦截问题求助
问题解决:Jetpack Compose中ExoPlayer视频不更新与点击事件拦截问题
一、视频源切换后视图不更新的问题
核心原因
- ViewModel实例化错误:
HomeScreen中直接通过val feedViewModel = FeedViewModel()创建实例,每次Compose重组都会生成新的ViewModel,导致currentSource状态无法在组件间共享,切换后的状态无法传递到VideoPlayer。 - AndroidView未响应状态变化:
DisposableEffect未将currentSource作为依赖项,currentSource变化时,PlayerView不会更新关联的ExoPlayer实例。 - 初始值不一致:
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
相关产品推荐
相关产品推荐

