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

如何在Jetpack Compose中为Media3 ExoPlayer添加自定义UI控制器

在Jetpack Compose中为Media3 ExoPlayer实现自定义控件

核心思路

Compose无法直接复用旧View系统的PlayerControlView,但可以通过自定义Compose布局+监听Player状态实现和旧系统一致的自定义控件逻辑——本质是用Compose组件替代传统View,直接对接ExoPlayer的状态与操作API。

实现步骤

1. 初始化并管理ExoPlayer生命周期

用Compose的remember和DisposableEffect确保Player实例的创建与释放符合生命周期:

@Composable
fun CustomPlayerScreen() {
    val context = LocalContext.current
    val exoPlayer = remember {
        ExoPlayer.Builder(context).build().apply {
            setMediaItem(MediaItem.fromUri("你的视频URI"))
            prepare()
        }
    }

    // 页面销毁时自动释放Player资源
    DisposableEffect(Unit) {
        onDispose {
            exoPlayer.release()
        }
    }

    CustomPlayerLayout(exoPlayer)
}

2. 构建基础播放器布局

用AndroidView承载ExoPlayer的渲染Surface,同时隐藏默认控件,再叠加自定义Compose控件层:

@Composable
fun CustomPlayerLayout(exoPlayer: ExoPlayer) {
    Box(modifier = Modifier.fillMaxSize()) {
        // 视频渲染容器,隐藏默认控件
        AndroidView(
            factory = { context ->
                PlayerView(context).apply {
                    player = exoPlayer
                    useController = false
                    controllerAutoShow = false
                }
            },
            modifier = Modifier.fillMaxSize()
        )

        // 自定义控制控件层
        PlayerControls(exoPlayer)
    }
}

3. 实现自定义控制逻辑

监听ExoPlayer的播放状态、进度等参数,用Compose组件构建交互控件:

@Composable
fun PlayerControls(exoPlayer: ExoPlayer) {
    // 将Player的Flow状态转为Compose可观察状态
    val isPlaying by exoPlayer.isPlaying.collectAsStateWithLifecycle()
    val currentPosition by exoPlayer.currentPosition.collectAsStateWithLifecycle()
    val duration by exoPlayer.duration.collectAsStateWithLifecycle()

    Column(
        modifier = Modifier
            .fillMaxSize()
            .background(Color.Black.copy(alpha = 0.5f))
            .padding(16.dp),
        verticalArrangement = Arrangement.Bottom
    ) {
        // 播放/暂停按钮
        IconButton(
            onClick = { if (isPlaying) exoPlayer.pause() else exoPlayer.play() },
            modifier = Modifier.align(Alignment.CenterHorizontally)
        ) {
            Icon(
                imageVector = if (isPlaying) Icons.Default.Pause else Icons.Default.PlayArrow,
                contentDescription = if (isPlaying) "暂停" else "播放",
                tint = Color.White,
                modifier = Modifier.size(48.dp)
            )
        }

        // 进度条
        Slider(
            value = currentPosition.toFloat(),
            onValueChange = { exoPlayer.seekTo(it.toLong()) },
            valueRange = 0f..duration.toFloat(),
            modifier = Modifier.fillMaxWidth()
        )

        // 时间显示
        Row(
            modifier = Modifier.fillMaxWidth(),
            horizontalArrangement = Arrangement.SpaceBetween
        ) {
            Text(text = formatTime(currentPosition), color = Color.White)
            Text(text = formatTime(duration), color = Color.White)
        }
    }
}

// 辅助函数:将毫秒转为时分秒格式
private fun formatTime(millis: Long): String {
    val seconds = (millis / 1000) % 60
    val minutes = (millis / (1000 * 60)) % 60
    val hours = (millis / (1000 * 60 * 60))
    return if (hours > 0) {
        String.format("%02d:%02d:%02d", hours, minutes, seconds)
    } else {
        String.format("%02d:%02d", minutes, seconds)
    }
}

4. 扩展更多自定义功能

根据需求添加额外控件:

  • 音量调节:监听exoPlayer.volume状态,用滑块控制exoPlayer.setVolume()
  • 全屏切换:修改布局Modifier或调用系统API切换横竖屏
  • 播放速度:添加下拉选择器,调用exoPlayer.setPlaybackSpeed()
  • 字幕切换:通过exoPlayer.currentMediaItem获取字幕轨道,调用exoPlayer.setTrackSelectionParameters()切换

关键注意事项

  • 必须使用collectAsStateWithLifecycle()监听Player的Flow状态,确保状态响应符合Compose生命周期规范
  • 隐藏默认控件时需同时设置useController = false和controllerAutoShow = false,避免默认控件意外显示
  • 所有Player操作(播放、暂停、seek等)默认在主线程执行,无需额外线程切换

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.03 10:31:15