Android Kotlin中ExoPlayer进度监听及时段贴纸显示实现方案
在ExoPlayer中实现定时显示的视频贴纸/图片
针对你需要在视频预览层上方添加指定时段显示的贴纸需求,PlayerMessage确实不适合——它仅能在特定播放位置触发单次操作,无法处理持续的区间检测和状态同步。以下是几种更合适的实现方案:
方案一:自定义Overlay + 播放进度监听(推荐,适合大多数场景)
这是最直接且易维护的方案,核心思路是将贴纸View放在ExoPlayer的渲染层上方,通过实时监听播放进度来切换贴纸可见性。
实现步骤:
- 布局层级:在XML中将贴纸View(如
ImageView)放在ExoPlayer的SurfaceView/TextureView同级且上方,初始设为GONE。 - 监听播放进度:通过ExoPlayer的
AnalyticsListener或Player.Listener实时获取当前播放位置。 - 区间判断与状态切换:每次进度更新时,判断当前位置是否在贴纸的显示区间内,动态调整View的可见性。
代码示例(Kotlin):
// 初始化贴纸View val stickerView = findViewById<ImageView>(R.id.sticker_view) stickerView.visibility = View.GONE // 定义贴纸的时间区间(单位:毫秒) val stickerStartMs = 2000L val stickerEndMs = 6000L // 监听播放进度,实时更新贴纸状态 player.addAnalyticsListener(object : AnalyticsListener { override fun onPositionChanged( eventTime: AnalyticsListener.EventTime, positionMs: Long, bufferedPositionMs: Long, totalDurationMs: Long ) { val isInDisplayRange = positionMs in stickerStartMs..stickerEndMs stickerView.visibility = if (isInDisplayRange) View.VISIBLE else View.GONE } })
扩展:多贴纸场景
如果需要添加多个贴纸,可将贴纸信息封装为数据类,遍历所有贴纸进行区间判断:
data class Sticker( val view: View, val startMs: Long, val endMs: Long ) val stickers = listOf( Sticker(findViewById(R.id.sticker1), 2000L, 6000L), Sticker(findViewById(R.id.sticker2), 4000L, 8000L) ) // 在进度监听中遍历更新 stickers.forEach { sticker -> val isInRange = positionMs in sticker.startMs..sticker.endMs sticker.view.visibility = if (isInRange) View.VISIBLE else View.GONE }
方案二:视频帧同步绘制(适合高精度需求)
如果需要贴纸与视频帧精确同步(如随视频缩放、旋转,或添加动画贴纸),可通过ExoPlayer的VideoListener在视频帧渲染时直接绘制贴纸到Surface上。
代码示例(Kotlin):
// 自定义贴纸数据类,包含位置、时间区间和位图 data class FrameSticker( val bitmap: Bitmap, val startMs: Long, val endMs: Long, val x: Float, val y: Float ) val stickerList = listOf( FrameSticker( BitmapFactory.decodeResource(resources, R.drawable.sticker), 2000L, 6000L, 100f, 100f ) ) // 监听视频帧渲染事件,直接绘制贴纸 player.addVideoListener(object : VideoListener { override fun onVideoFrameRendered( player: Player, timestampMs: Long, frame: Bitmap? ) { val surfaceHolder = (player.videoSurfaceView as SurfaceView).holder val canvas = surfaceHolder.lockCanvas() canvas?.let { // 遍历贴纸,在对应时间区间绘制 stickerList.forEach { sticker -> if (timestampMs in sticker.startMs..sticker.endMs) { it.drawBitmap(sticker.bitmap, sticker.x, sticker.y, null) } } surfaceHolder.unlockCanvasAndPost(it) } } })
注意事项:
- 避免在主线程执行复杂绘制逻辑,可通过子线程处理位图预处理。
- 开启硬件加速提升绘制性能,避免卡顿。
方案三:PlayerMessage结合进度拖动监听(仅适合简单单次场景)
如果坚持使用PlayerMessage,可通过两次消息分别触发显示和隐藏,同时添加进度拖动监听处理用户手动跳转的情况:
// 触发显示贴纸 val showMsg = PlayerMessage( player, Runnable { stickerView.visibility = View.VISIBLE }, 2000L ) showMsg.send() // 触发隐藏贴纸 val hideMsg = PlayerMessage( player, Runnable { stickerView.visibility = View.GONE }, 6000L ) hideMsg.send() // 处理进度拖动后的状态同步 player.addListener(object : Player.Listener { override fun onPlaybackStateChanged(playbackState: Int) { if (playbackState == Player.STATE_READY) { val currentPos = player.currentPosition stickerView.visibility = if (currentPos in 2000L..6000L) View.VISIBLE else View.GONE } } })
方案选型建议:
- 普通贴纸需求:优先选方案一,实现成本低、稳定性高。
- 高精度/动画贴纸需求:选方案二,保证帧级同步。
- 单次触发类需求:可考虑方案三,但需额外处理进度拖动的边界情况。
内容的提问来源于stack exchange,提问作者Dhina17
相关产品推荐
相关产品推荐

