Android中RecyclerView多ViewType下MediaPlayer崩溃问题修复求助
问题解决:RecyclerView多类型ViewHolder中MediaPlayer调用setDataSource崩溃
问题场景
在RecyclerView的Adapter中实现两种类型的ViewHolder,集成MediaPlayer实现语音播放功能,点击播放按钮时应用崩溃,错误指向mediaPlayer.setDataSource(url),抛出java.lang.IllegalStateException。
错误原因分析
- MediaPlayer实例共享与状态失效:Adapter中创建单个MediaPlayer实例并传递给所有ViewHolder,调用
mediaPlayer.release()后该实例进入无效状态,后续再调用setDataSource会触发非法状态异常。 - 播放状态局部维护:每个ViewHolder自行维护
currentPlayingPosition,导致多ViewHolder的播放状态无法统一管理,易出现逻辑混乱。 - 同步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
相关产品推荐
相关产品推荐

