第 3 篇:接入 Compose 和 RecyclerView,处理列表单播、进度恢复与全屏

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 中。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容