RecyclerView集成ExoPlayer显示黑屏问题的解决方法
问题描述
在Android应用的RecyclerView中集成ExoPlayer展示视频缩略图,运行后视频区域仅显示黑屏,无法正常展示视频或缩略图。相关代码如下:
RecyclerView子项布局XML
<com.google.android.exoplayer2.ui.PlayerView android:id="@+id/blueVideoPlayer" android:layout_width="@dimen/_150mdp" android:layout_height="@dimen/_150mdp" android:visibility="gone" app:use_artwork="true" app:resize_mode="zoom" app:use_controller="false" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" />
RecyclerView Adapter Kotlin代码
class ChatAdapter constructor(val context: Context, val items: MutableList<Result>) : RecyclerView.Adapter<ChatAdapter.ViewHolder>() { private lateinit var binding: ItemChatBinding private val userToken by lazy { GoodPrefs.getInstance().getString(USER_TOKEN, "") } private var player: ExoPlayer = ExoPlayer.Builder(context).build() override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { binding = ItemChatBinding.inflate(LayoutInflater.from(parent.context), parent, false) return ViewHolder() } override fun getItemCount(): Int = items.size override fun onBindViewHolder(holder: ViewHolder, position: Int) { val item = items[position] holder.bind(item) } override fun getItemViewType(position: Int) = position override fun getItemId(position: Int) = position.toLong() inner class ViewHolder : RecyclerView.ViewHolder(binding.root) { @SuppressLint("SetTextI18n") fun bind(item: Result) { binding.apply { //Layouts if (item.toUserId?.id == PHYSICIAN_DATA?.id) { blueLay.visibility = View.VISIBLE blueDate.visibility = View.VISIBLE blueBubble.isVisible = item.message != null doctorStatus.isVisible = !(item.deliveredAt == null && item.seenAt == null) whiteLay.isVisible = false whiteDate.isVisible = false whiteBubble.isVisible = false meStatus.isVisible = false } else { blueLay.isVisible = false blueDate.isVisible = false blueBubble.isVisible = false doctorStatus.isVisible = false whiteLay.visibility = View.VISIBLE whiteDate.visibility = View.VISIBLE whiteBubble.isVisible = item.message != null meStatus.isVisible = !(item.deliveredAt == null && item.seenAt == null) } //Message if (item.message != null) { whiteTxt.text = item.message?.replace("\"", "") whiteTxt.visibility = View.VISIBLE blueTxt.text = item.message?.replace("\"", "") blueTxt.visibility = View.VISIBLE } else { whiteTxt.visibility = View.GONE blueTxt.visibility = View.GONE } //File if (item.chatFile != null) { whiteFileLay.isVisible = true blueFileLay.isVisible = true fileVisibilityMode(item.chatFile.type!!) when (item.chatFile.type) { IMAGE -> { whiteLoading.visibility = View.VISIBLE blueLoading.visibility = View.VISIBLE ApiClient.getInstance().apisUseCase().getAvatarImage(userToken, item.chatFile.id!!) .applyIoScheduler() .subscribe({ whiteLoading.visibility = View.GONE blueLoading.visibility = View.GONE if (it.isSuccessful) { if (it.code() == 200) { if (it.body() != null) { val decodedBytes: ByteArray = Base64.decode(it.body()!!.data!!.file, Base64.DEFAULT) whiteImg.load(decodedBytes) blueImg.load(decodedBytes) } } } }, { whiteLoading.visibility = View.GONE blueLoading.visibility = View.GONE }) } VIDEO -> { whiteVideoPlayer.player = player blueVideoPlayer.player = player val mediaItem: MediaItem = MediaItem.fromUri(fileUrl(item.chatFile.id!!)) Handler(Looper.myLooper()!!).postDelayed( { player.setMediaItem(mediaItem) player.prepare() }, 200 ) } VOICE -> { } } } else { whiteFileLay.isVisible = false blueFileLay.isVisible = false } } } } private fun fileUrl(id: Int): String { return "${BASE_URL}core/shared/files/$id/downloads?token=${userToken.replace("Bearer ", "")}" } }
解决方案
1. 修复PlayerView可见性问题
布局中PlayerView默认设置了android:visibility="gone",需要在绑定视频时根据布局显示逻辑,将对应的PlayerView设置为VISIBLE。示例修改:
VIDEO -> { // 根据消息发送方判断显示哪个PlayerView val targetPlayerView = if (item.toUserId?.id == PHYSICIAN_DATA?.id) blueVideoPlayer else whiteVideoPlayer targetPlayerView.visibility = View.VISIBLE // 其余视频初始化逻辑... }
2. 解决单例ExoPlayer复用冲突
当前Adapter全局只创建一个player实例,RecyclerView滚动时所有视频项绑定同一个player,会导致资源冲突和展示异常。改为在ViewHolder中独立管理player,并在回收时释放资源:
inner class ViewHolder : RecyclerView.ViewHolder(binding.root) { private var player: ExoPlayer? = null @SuppressLint("SetTextI18n") fun bind(item: Result) { binding.apply { //...其他逻辑 when (item.chatFile.type) { VIDEO -> { val targetPlayerView = if (item.toUserId?.id == PHYSICIAN_DATA?.id) blueVideoPlayer else whiteVideoPlayer targetPlayerView.visibility = View.VISIBLE // 先释放之前的player实例 player?.release() // 创建新的player实例 player = ExoPlayer.Builder(context).build() targetPlayerView.player = player val mediaItem = MediaItem.fromUri(fileUrl(item.chatFile.id!!)) player?.setMediaItem(mediaItem) player?.prepare() // 仅展示缩略图,不需要自动播放 player?.playWhenReady = false } //...其他类型逻辑 } } } // 回收ViewHolder时释放player资源 fun releasePlayer() { player?.release() player = null } } // 在Adapter中重写回收方法 override fun onViewRecycled(holder: ViewHolder) { super.onViewRecycled(holder) holder.releasePlayer() }
3. 确保缩略图加载配置生效
- 确认布局中
app:use_artwork="true"已设置,该属性会让PlayerView自动加载视频内嵌的封面缩略图 - 如果视频无内嵌缩略图,可以通过
MediaMetadata手动设置外部缩略图:
val mediaMetadata = MediaMetadata.Builder() .setArtworkUri(Uri.parse("你的缩略图URL")) // 替换为实际的缩略图接口地址 .build() val mediaItem = MediaItem.Builder() .setUri(fileUrl(item.chatFile.id!!)) .setMediaMetadata(mediaMetadata) .build() player?.setMediaItem(mediaItem)
4. 验证视频URL与权限
- 测试
fileUrl生成的地址是否可正常访问,排查网络请求是否存在权限或路径问题 - 确保AndroidManifest.xml中已添加网络权限:
<uses-permission android:name="android.permission.INTERNET" />
5. 移除不必要的延迟逻辑
代码中Handler.postDelayed会导致加载延迟,甚至可能因为ViewHolder提前回收而失效,直接移除延迟,同步调用prepare即可。
内容的提问来源于stack exchange,提问作者KadKos
相关产品推荐
相关产品推荐

