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

Android中RecyclerView多ViewType下MediaPlayer崩溃问题修复求助

问题解决:RecyclerView多类型ViewHolder中MediaPlayer调用setDataSource崩溃

问题场景

在RecyclerView的Adapter中实现两种类型的ViewHolder,集成MediaPlayer实现语音播放功能,点击播放按钮时应用崩溃,错误指向mediaPlayer.setDataSource(url),抛出java.lang.IllegalStateException。

错误原因分析

  1. MediaPlayer实例共享与状态失效:Adapter中创建单个MediaPlayer实例并传递给所有ViewHolder,调用mediaPlayer.release()后该实例进入无效状态,后续再调用setDataSource会触发非法状态异常。
  2. 播放状态局部维护:每个ViewHolder自行维护currentPlayingPosition,导致多ViewHolder的播放状态无法统一管理,易出现逻辑混乱。
  3. 同步prepare阻塞UI:直接调用mediaPlayer.prepare()会阻塞主线程,且未处理IO或状态异常。

解决方案

1. 全局统一管理MediaPlayer与播放状态

将MediaPlayer实例和当前播放位置移至Adapter中全局维护,避免多ViewHolder共享失效实例,同时统一控制播放逻辑。

2. 规范MediaPlayer生命周期

每次切换播放项时,先释放旧MediaPlayer实例,再创建新实例执行操作,确保setDataSource调用时实例处于可用初始状态。

3. 异步准备避免UI阻塞

改用prepareAsync()配合OnPreparedListener,在准备完成后启动播放,避免阻塞主线程,同时处理准备过程中的异常。

4. 解耦ViewHolder与MediaPlayer操作

ViewHolder仅负责触发播放事件,具体MediaPlayer操作交由Adapter处理,降低模块耦合度。

修改后的关键代码

Adapter代码调整

class ChatAdapterNew constructor(
    private val context: Context, val items: MutableList<Result>
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {

    private val viewTypeBlue = 1
    private val viewTypeWhite = 2
    private var mediaPlayer: MediaPlayer? = null
    private var currentPlayingPosition = -1

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        return if (viewType == viewTypeBlue) {
            BlueViewHolder(
                ItemChatBlueBinding.inflate(LayoutInflater.from(parent.context), parent, false),
                context, ::handleVoicePlay
            )
        } else {
            WhiteViewHolder(
                ItemChatWhiteBinding.inflate(LayoutInflater.from(parent.context), parent, false),
                context, ::handleVoicePlay
            )
        }
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        val item = items[position]
        when (holder) {
            is BlueViewHolder -> holder.bindData(item, position == currentPlayingPosition)
            is WhiteViewHolder -> holder.bindData(item, position == currentPlayingPosition)
        }
    }

    // 统一处理语音播放逻辑
    private fun handleVoicePlay(position: Int, url: String) {
        if (position == currentPlayingPosition) {
            // 同一位置切换播放/暂停
            mediaPlayer?.let { mp ->
                if (mp.isPlaying) {
                    mp.pause()
                    notifyItemChanged(position)
                } else {
                    mp.start()
                    notifyItemChanged(position)
                }
            }
        } else {
            // 切换播放项,先释放旧实例
            mediaPlayer?.release()
            mediaPlayer = MediaPlayer().apply {
                setAudioAttributes(
                    AudioAttributes.Builder()
                        .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
                        .setUsage(AudioAttributes.USAGE_MEDIA)
                        .build()
                )
                try {
                    setDataSource(url)
                    prepareAsync()
                    setOnPreparedListener {
                        it.start()
                        val oldPosition = currentPlayingPosition
                        currentPlayingPosition = position
                        notifyItemChanged(oldPosition)
                        notifyItemChanged(position)
                    }
                    setOnCompletionListener {
                        it.release()
                        mediaPlayer = null
                        currentPlayingPosition = -1
                        notifyItemChanged(position)
                    }
                    setOnErrorListener { _, _, _ ->
                        it.release()
                        mediaPlayer = null
                        currentPlayingPosition = -1
                        notifyItemChanged(position)
                        true
                    }
                } catch (e: IOException) {
                    e.printStackTrace()
                    currentPlayingPosition = -1
                    notifyItemChanged(position)
                } catch (e: IllegalStateException) {
                    e.printStackTrace()
                    currentPlayingPosition = -1
                    notifyItemChanged(position)
                }
            }
        }
    }

    override fun getItemCount() = items.size

    override fun getItemViewType(position: Int): Int {
        return if (items[position].toUserId?.id == PHYSICIAN_DATA?.id) {
            viewTypeBlue
        } else {
            viewTypeWhite
        }
    }

    override fun getItemId(position: Int) = position.toLong()

    // 页面销毁时释放资源
    fun releaseMediaPlayer() {
        mediaPlayer?.release()
        mediaPlayer = null
    }
}

WhiteViewHolder代码调整

class WhiteViewHolder internal constructor(
    whiteBinding: ItemChatWhiteBinding, 
    private val context: Context,
    private val playHandler: (Int, String) -> Unit // 接收Adapter的播放回调
) : RecyclerView.ViewHolder(whiteBinding.root), SeekBar.OnSeekBarChangeListener {

    private val binding: ItemChatWhiteBinding = whiteBinding
    private val seekBarUpdater by lazy { SeekBarUpdater() }
    private var isPlaying = false

    @SuppressLint("SetTextI18n")
    fun bindData(item: ResponseChatList.Data.Result, isCurrentPlaying: Boolean) {
        binding.apply {
            isPlaying = isCurrentPlaying
            updateViewState()

            if (item.chatFile != null) {
                fileVisibilityMode(item.chatFile.type!!)
                whiteFileLay.isVisible = true
                when (item.chatFile.type) {
                    VOICE -> {
                        whiteVoiceBtn.setOnClickListener {
                            val url = fileUrl(item.chatFile.id!!)
                            playHandler.invoke(bindingAdapterPosition, url)
                        }
                        whiteVoiceProgress.setOnSeekBarChangeListener(this@WhiteViewHolder)
                    }
                }
            } else {
                whiteFileLay.isVisible = false
            }
        }
    }

    private fun fileUrl(id: Int): String {
        return "${BASE_URL}core/shared/files/$id/downloads?token=${userToken.replace("Bearer ", "")}"
    }

    fun updateViewState() {
        binding.apply {
            if (isPlaying) {
                whiteVoiceBtn.setImageResource(R.drawable.ic_pause)
                whiteVoiceProgress.isEnabled = true
                whiteVoiceProgress.postDelayed(seekBarUpdater, 100)
            } else {
                whiteVoiceBtn.setImageResource(R.drawable.ic_play)
                whiteVoiceProgress.isEnabled = false
                whiteVoiceProgress.progress = 0
                whiteVoiceProgress.removeCallbacks(seekBarUpdater)
            }
        }
    }

    override fun onProgressChanged(p0: SeekBar?, p1: Int, p2: Boolean) {
        if (p2) {
            (binding.root.tag as? ChatAdapterNew)?.mediaPlayer?.seekTo(p1)
        }
    }

    override fun onStartTrackingTouch(p0: SeekBar?) {}

    override fun onStopTrackingTouch(p0: SeekBar?) {}

    inner class SeekBarUpdater : Runnable {
        override fun run() {
            (binding.root.tag as? ChatAdapterNew)?.let { adapter ->
                adapter.mediaPlayer?.let { mp ->
                    if (adapter.currentPlayingPosition == bindingAdapterPosition) {
                        binding.whiteVoiceProgress.max = mp.duration
                        binding.whiteVoiceProgress.progress = mp.currentPosition
                        binding.whiteVoiceProgress.postDelayed(this, 100)
                    }
                }
            }
        }
    }
}

额外注意事项

  • 在Activity/Fragment的onDestroy方法中调用Adapter的releaseMediaPlayer(),避免内存泄漏。
  • 确保应用拥有INTERNET权限,Android 10及以上需处理媒体访问相关权限。
  • 可添加加载状态提示,在MediaPlayer准备过程中显示加载动画,提升用户体验。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.30 11:28:29