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

Android RecyclerView视频滑块:解决视频切换后重复加载问题

解决RecyclerView视频切换后重新加载的问题

核心问题分析

RecyclerView默认会回收超出屏幕范围的ViewHolder,导致视频项滑出后再返回时,onBindViewHolder会重新执行,现有代码每次绑定都会重新设置视频URI,触发重新加载。同时VideoView本身没有内置状态持久化机制,需要手动保存和恢复播放进度与状态。

解决方案步骤

1. 调整RecyclerView缓存策略,避免ViewHolder被回收

修改Fragment中RecyclerView的配置,让它保留所有列表项的ViewHolder,防止回收:

// Setup StoryBoardAdapter
val storyboardAdapter = StoryBoardAdapter(requireContext(), DataRepository.storyBoardList)
val layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)
binding.recyclerViewStoryBoard.apply {
    this.layoutManager = layoutManager
    this.adapter = storyboardAdapter
    // 固定列表大小,优化缓存逻辑
    setHasFixedSize(true)
    // 设置缓存大小为列表总数量,确保所有ViewHolder都被保留
    itemViewCacheSize = DataRepository.storyBoardList.size
}

// 设置预加载数量为列表总数量,提前加载所有项
layoutManager.initialPrefetchItemCount = DataRepository.storyBoardList.size

// Attach PagerSnapHelper to show one item at a time
val pagerSnapHelper = PagerSnapHelper()
pagerSnapHelper.attachToRecyclerView(binding.recyclerViewStoryBoard)

2. 在Adapter中添加视频播放状态管理

定义存储播放状态的数据类,并在Adapter中维护每个视频的状态,实现状态的保存与恢复:

class StoryBoardAdapter(
    private val context: Context,
    private val storyBoardList: List<StoryBoard>
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {

    companion object {
        private const val TYPE_IMAGE = 0
        private const val TYPE_VIDEO = 1
        private const val BASE_URL = "Link"
    }

    // 存储每个视频的播放状态:key为视频完整URL,value为状态对象
    private data class VideoPlaybackState(
        var isPrepared: Boolean = false,
        var currentPosition: Int = 0,
        var isPlaying: Boolean = false
    )
    private val videoPlaybackStates = mutableMapOf<String, VideoPlaybackState>()

    private fun isVideo(url: String): Boolean {
        val lowerUrl = url.lowercase()
        return lowerUrl.endsWith(".mp4") || lowerUrl.endsWith(".webm")
    }

    private fun getFullUrl(relativeUrl: String): String {
        return if (relativeUrl.startsWith("http")) relativeUrl else "$BASE_URL$relativeUrl"
    }

    override fun getItemViewType(position: Int): Int {
        return if (isVideo(storyBoardList[position].imageEn)) TYPE_VIDEO else TYPE_IMAGE
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        return if (viewType == TYPE_IMAGE) {
            val view = LayoutInflater.from(parent.context)
                .inflate(R.layout.item_story_board_image, parent, false)
            ImageViewHolder(view)
        } else {
            val view = LayoutInflater.from(parent.context)
                .inflate(R.layout.item_story_board_video, parent, false)
            VideoViewHolder(view)
        }
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        val item = storyBoardList[position]
        val currentLanguage = context.resources.configuration.locales[0].language
        val imageUrl = if (currentLanguage == "ar") item.imageAr else item.imageEn
        val fullUrl = getFullUrl(imageUrl)

        if (holder is ImageViewHolder) {
            Glide.with(holder.imageView.context)
                .load(fullUrl)
                .into(holder.imageView)
        } else if (holder is VideoViewHolder) {
            val videoState = videoPlaybackStates.getOrPut(fullUrl) { VideoPlaybackState() }

            if (!videoState.isPrepared) {
                // 首次加载视频
                val videoUri = Uri.parse(fullUrl)
                holder.videoView.tag = fullUrl
                holder.videoView.setVideoURI(videoUri)
                holder.videoView.setOnPreparedListener { mediaPlayer ->
                    mediaPlayer.isLooping = true
                    videoState.isPrepared = true
                    // 恢复之前保存的播放位置
                    if (videoState.currentPosition > 0) {
                        mediaPlayer.seekTo(videoState.currentPosition)
                    }
                    // 恢复播放状态
                    if (videoState.isPlaying) {
                        holder.videoView.start()
                    }
                }
            } else {
                // 视频已加载,直接恢复状态
                holder.videoView.seekTo(videoState.currentPosition)
                if (videoState.isPlaying) {
                    holder.videoView.start()
                } else {
                    holder.videoView.pause()
                }
            }

            // 监听播放状态变化,实时更新状态存储
            holder.videoView.setOnPlaybackStateChangedListener { mp, state ->
                videoState.isPlaying = state == MediaPlayer.STATE_PLAYING
                if (videoState.isPlaying) {
                    videoState.currentPosition = mp.currentPosition
                }
            }
        }
    }

    // 当ViewHolder被回收时,保存当前播放状态
    override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
        super.onViewRecycled(holder)
        if (holder is VideoViewHolder) {
            val videoUrl = holder.videoView.tag as? String ?: return
            val videoState = videoPlaybackStates[videoUrl] ?: return
            // 保存当前位置和播放状态
            videoState.currentPosition = holder.videoView.currentPosition
            videoState.isPlaying = holder.videoView.isPlaying
            // 暂停播放并释放资源
            holder.videoView.pause()
            holder.videoView.stopPlayback()
        }
    }

    override fun getItemCount(): Int = storyBoardList.size

    inner class ImageViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val imageView: ImageView = itemView.findViewById(R.id.imageViewItem)
    }

    inner class VideoViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val videoView: VideoView = itemView.findViewById(R.id.videoViewItem)
    }
}

3. 额外优化建议

  • 如果视频数量较多,不建议一次性加载所有视频(会占用大量内存),可改为预加载当前项前后2-3个视频,平衡内存占用和用户体验。
  • 考虑使用ExoPlayer替代VideoView,ExoPlayer提供了更完善的缓存、状态管理和生命周期控制能力,能更好适配RecyclerView场景。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 23:10:57