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

RecyclerView中ExoPlayer向下滚动可播放,向上滚动显示空白问题

RecyclerView中ExoPlayer向上滚动返回时显示空白的问题修复

我在首页Fragment中用RecyclerView展示帖子,ViewPager处理图片,ExoPlayer处理视频,通过CommonExoPlayer单实例根据项位置传入URL管理播放。目前的问题是:向下滚动到视频项时首次可以正常播放,但向上滚动返回该视频项时,播放器显示空白视图,无法播放目标视频。

预期行为:ExoPlayer应能播放RecyclerView当前可见的对应视频,不受滚动方向影响。


现有代码

Fragment中的代码

private fun setAdapter() {
    recyclerViewFollowings.apply {
        adapter = homeFollowingPostAdapter
        addOnScrollListener(scrollListener)
    }
}

private val scrollListener = object : RecyclerView.OnScrollListener() {
    override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
        val llm = layoutManager as LinearLayoutManager
        val firstVisibleItem = llm.findFirstVisibleItemPosition()
        // ... 省略其他代码
        if (firstVisibleItem != lastFirstVisiblePosition) {
            homeFollowingPostAdapter.commonExoPlayer.pausePlayer()
            Handler().postDelayed({
                if (firstVisibleItem < onePageDataList!!.size) {
                    val viewHolder = findViewHolderForAdapterPosition(firstVisibleItem)
                    if (viewHolder is HomeFollowingPostAdapter.ViewHolder) {
                        viewHolder.playStopVideo(firstVisibleItem)
                    }
                }
            }, 3000)
         }
        // ... 省略其他代码
    }
}

HomeFollowingPostAdapter中的代码

class HomeFollowingPostAdapter(var context: Context) :
    AdvanceRecycleViewAdapter<BaseHolder<DataItem>, DataItem>() {

    var commonExoPlayer: CommonExoPlayer = CommonExoPlayer(context)
    val player = commonExoPlayer.getPlayer()
    private var storyUrl: String? = null

    override fun createDataHolder(
        parent: ViewGroup,
        viewType: Int
    ): BaseHolder<DataItem> {
        return ViewHolder(parent.toBinding())
    }

    inner class ViewHolder(private val binding: HomeFollowingPostRowBinding) :
        BaseHolder<DataItem>(binding.root) {

        fun onBindView(followingPost: DataItem) = with(binding) {
            textViewUserName.text =
                layoutPosition.toString() + " " + followingPost.type
            if (items?.get(layoutPosition)?.type == "post") {
                val adapter = ImageSliderAdapter() { adapter, position, action -> }
                viewPagerPost.adapter = adapter
                adapter.items = followingPost.file_url as MutableList<FileUrl>?
                viewPagerPost.visibility = View.VISIBLE
                clVideoView.visibility = View.GONE
            } else {
                viewPagerPost.visibility = View.GONE
                clVideoView.visibility = View.VISIBLE
                imageViewPost.visibility = View.VISIBLE
                imageViewPost.setImageWithOptimize(followingPost.file_url?.get(0)?.thumb_url)
            }
        }

        fun playStopVideo(firstVisibleItem: Int) {
            if (firstVisibleItem == itemViewType){
                player?.let {
                    Log.d("MMM player: ", player.toString())
                    binding.videoViewPost.visibility = View.VISIBLE
                    binding.imageViewPost.visibility = View.GONE
                    binding.videoViewPost.player = player
                    if (itemViewType != -1){
                        commonExoPlayer.prepareMedia(items?.get(itemViewType)?.file_url?.get(0)?.post_url.toString())
                        binding.videoViewPost.player = player
                    }
                }
            }
        }
    }

    override fun getItemId(position: Int): Long {
        return position.toLong()
    }

    override fun getItemViewType(position: Int): Int {
        return position
    }

    override fun onBindDataHolder(
        holder: BaseHolder<DataItem>,
        position: Int,
        item: DataItem
    ) {
        when (holder) {
            is ViewHolder -> {
                holder.onBindView(item)
            }
        }
    }
}

CommonExoPlayer中的代码

class CommonExoPlayer(context: Context) {
    private var simplePlayer: ExoPlayer? = null

    fun getPlayer(): ExoPlayer? {
        if (simplePlayer == null) {
            prepareVideoPlayer()
        }
        return simplePlayer
    }

    private fun prepareVideoPlayer() {
        simplePlayer = ExoPlayer.Builder(context).build()
        // ... 省略其他初始化代码
    }

    fun prepareMedia(linkUrl: String) {
        pausePlayer()
        // ... 省略DataSourceFactory初始化代码
        val mediaSource = ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(MediaItem.fromUri(linkUrl))
        simplePlayer!!.setMediaSource(mediaSource)
        simplePlayer?.prepare()
        // ... 省略其他代码
    }

    // ... 省略pausePlayer等其他方法
}

问题根源分析

  1. ScrollListener延迟过长:3秒延迟会导致滚动过程中视图已被复用,findViewHolderForAdapterPosition无法拿到正确的目标ViewHolder。
  2. 位置判断逻辑错误:用itemViewType作为位置标识,但getItemViewType返回的是position,ViewHolder复用后该值不会更新,向上滚动时会加载错误的视频URL。
  3. 视图复用未重置状态:视频项被回收复用后,PlayerView未解绑旧Player,也未重置封面图状态。
  4. 播放状态未触发:prepareMedia仅调用prepare(),未设置自动播放,导致视频加载后无响应。

修复步骤

1. 优化ScrollListener,精准定位可见项

改用findFirstCompletelyVisibleItemPosition获取完全可见项,缩短延迟并避免重复触发:

private var currentPlayingPosition = -1
private val scrollListener = object : RecyclerView.OnScrollListener() {
    override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
        super.onScrolled(recyclerView, dx, dy)
        val llm = layoutManager as LinearLayoutManager
        val firstCompletelyVisibleItem = llm.findFirstCompletelyVisibleItemPosition()
        
        if (firstCompletelyVisibleItem != currentPlayingPosition && firstCompletelyVisibleItem != -1) {
            homeFollowingPostAdapter.commonExoPlayer.pausePlayer()
            currentPlayingPosition = firstCompletelyVisibleItem
            
            Handler(Looper.getMainLooper()).removeCallbacksAndMessages(null)
            Handler(Looper.getMainLooper()).postDelayed({
                val viewHolder = recyclerView.findViewHolderForAdapterPosition(currentPlayingPosition)
                if (viewHolder is HomeFollowingPostAdapter.ViewHolder) {
                    viewHolder.playStopVideo(currentPlayingPosition)
                }
            }, 500)
        }
    }
}

2. 修改ViewHolder位置判断逻辑,使用真实数据位置

用adapterPosition替代itemViewType,确保拿到当前ViewHolder对应的真实数据项:

inner class ViewHolder(private val binding: HomeFollowingPostRowBinding) :
    BaseHolder<DataItem>(binding.root) {

    // ... 保留onBindView方法

    fun playStopVideo(targetPosition: Int) {
        if (adapterPosition == targetPosition) {
            val currentItem = items?.get(adapterPosition) ?: return
            if (currentItem.type != "post") {
                player?.let { exoPlayer ->
                    binding.imageViewPost.visibility = View.GONE
                    binding.videoViewPost.visibility = View.VISIBLE
                    binding.videoViewPost.player = exoPlayer
                    
                    val videoUrl = currentItem.file_url?.get(0)?.post_url ?: return
                    commonExoPlayer.prepareMedia(videoUrl)
                    exoPlayer.playWhenReady = true
                }
            }
        } else {
            binding.videoViewPost.player = null
            binding.imageViewPost.visibility = View.VISIBLE
            val currentItem = items?.get(adapterPosition) ?: return
            binding.imageViewPost.setImageWithOptimize(currentItem.file_url?.get(0)?.thumb_url)
        }
    }
}

3. 修正getItemViewType逻辑,按数据类型返回

让RecyclerView按内容类型复用视图,而非position:

override fun getItemViewType(position: Int): Int {
    return if (items?.get(position)?.type == "post") 0 else 1 // 0=图片帖,1=视频帖
}

4. 完善CommonExoPlayer的prepareMedia方法

添加自动播放逻辑,清除旧媒体源:

fun prepareMedia(linkUrl: String) {
    if (linkUrl.isEmpty() || simplePlayer == null) return
    
    pausePlayer()
    simplePlayer?.clearMediaItems()
    val mediaSource = ProgressiveMediaSource.Factory(dataSourceFactory)
        .createMediaSource(MediaItem.fromUri(linkUrl))
    simplePlayer?.setMediaSource(mediaSource)
    simplePlayer?.prepare()
    simplePlayer?.playWhenReady = true
}

5. 处理ViewHolder回收时的资源释放

重写onViewRecycled解绑Player,避免内存泄漏:

override fun onViewRecycled(holder: BaseHolder<DataItem>) {
    super.onViewRecycled(holder)
    if (holder is ViewHolder) {
        holder.binding.videoViewPost.player = null
        holder.binding.imageViewPost.visibility = View.VISIBLE
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.03 21:00:27