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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.31 01:27:27