Android 视频播放器封装(三):Compose 与 RecyclerView 的播放状态管理
播放器真正复杂的地方不在“播放一个地址”,而在页面有多个播放器时如何管理状态。
这一篇完成两件事:
•Compose 场景下,用状态驱动播放器 UI。
•RecyclerView 场景下,保证同一时间只播放一个 item,并保存滚动或切换时的播放进度。
1. Compose 通过 AndroidView 复用渲染层
核心播放器返回的是渲染 View,Compose 用 AndroidView 直接承载:
AndroidView(
factory = { player.renderView() as View },
modifier = Modifier.fillMaxSize(),
)
播放状态用 collectAsState() 订阅:
val playbackState by player.state.collectAsState()
val isLoading = playbackState is VideoPlayerState.Loading
val isPlaying = playbackState is VideoPlayerState.Playing &&player.isPlaying
这样 loading、中心播放按钮和控制条都由 Compose 状态控制,而不是手工调用 View 的显示和隐藏方法。
2. 进度条要区分播放进度和拖动进度
如果用户正在拖动进度条,播放器轮询更新的位置不能覆盖手指位置。因此需要两个值:
•currentProgress:播放器当前真实进度。
•seekProgress:用户拖动中的临时进度。
al currentProgress = if (durationMs > 0L) {
positionMs.toFloat() / durationMs
} else {
0f
}
val displayedProgress = if (isSeeking) seekProgress else currentProgress
拖动结束时再真正调用 seek:
onProgressFinished = {
player.seekTo((seekProgress * durationMs).toLong())
isSeeking = false
}
3. RecyclerView 中只允许一个 item 播放
列表里如果每个 ViewHolder 都各自管理播放,很容易出现多个视频同时出声、离屏播放器未停止、滚动回来进度丢失等问题。
解决方式是将“当前播放位置”提升到 Activity 或 Adapter:
private fun playAt(position: Int) {
if (activePosition != RecyclerView.NO_POSITION && activePosition != position) {
findViewHolder(activePosition)?.saveAndStop(playbackCache)
}
activePosition = position
findViewHolder(position)?.play(videoUrl, playbackCache[position])
}
快照数据只需要保存当前位置和总时长:
private data class PlaybackSnapshot(
val positionMs: Long,
val durationMs: Long,
)
恢复播放时,先设置地址,再 seek 到缓存位置,最后播放。
4. 未播放 item 也要有稳定的时长展示
因为延迟 prepare,未播放 item 还拿不到真实媒体时长。为了避免控制条显示 0:00,列表可设置一个业务默认值
private const val DEFAULT_DURATION_MS = 596_000L // 9:56
playerView.setPlaybackCache(
snapshot?.positionMs ?: 0L,
snapshot?.durationMs?.takeIf { it > 0L } ?: DEFAULT_DURATION_MS,
)
视频真正播放后,真实 duration 会覆盖默认值。
5. 全屏不要重新创建播放器
全屏时如果重新创建 ExoPlayer,容易造成播放位置丢失、重新缓冲,甚至 Surface 切换闪黑。
更稳妥的做法是移动现有 VideoPlayerView:
1.记录它在列表中的父容器和索引。
2.从原父容器移除。
3.添加到 Activity 根布局,铺满屏幕。
4.退出全屏时按原索引放回。
重点是移动 View,不移动播放状态,也不重建 ExoPlayer。
6. 生命周期释放
页面结束时必须释放所有持有的播放器:
override fun onDestroy() {
adapter.releaseAllPlayers()
super.onDestroy()
}
RecyclerView 回收时也要保存进度并停止:
override fun onViewRecycled(holder: VideoViewHolder) {
holder.saveAndStop(playbackCache)
super.onViewRecycled(holder)
}
小结
经过三层拆分后,播放器职责清晰:
•video-player 负责播放内核与状态。
•video-player-xml 负责传统 View 的控制层、首帧海报和全屏入口。
•video-player-compose 负责 Compose 状态和手势交互。
•app 负责列表单播、进度缓存和业务页面。
这套结构后续可以继续扩展清晰度选择、倍速、投屏、广告、埋点和离线缓存,而不会把所有能力堆到一个 Activity 中。