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等其他方法 }
问题根源分析
- ScrollListener延迟过长:3秒延迟会导致滚动过程中视图已被复用,
findViewHolderForAdapterPosition无法拿到正确的目标ViewHolder。 - 位置判断逻辑错误:用
itemViewType作为位置标识,但getItemViewType返回的是position,ViewHolder复用后该值不会更新,向上滚动时会加载错误的视频URL。 - 视图复用未重置状态:视频项被回收复用后,PlayerView未解绑旧Player,也未重置封面图状态。
- 播放状态未触发:
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
相关产品推荐
相关产品推荐

