安卓聊天应用适配器更新时MediaPlayer重复加载中断播放如何解决
问题根因
你当前的MediaPlayer实例创建、初始化逻辑全部放在了Adapter的item绑定流程中,每当列表刷新(新消息触发notifyDataSetChanged等操作)时,可见item会重新执行绑定逻辑,重复创建MediaPlayer并重新加载音频,导致正在播放的语音被中断。
解决方案
- 抽离全局唯一的MediaPlayer播放管理类,不要在Adapter的绑定逻辑中创建MediaPlayer实例
避免每次item重绑就新建播放器,统一管理播放状态、当前播放的音频URL、播放进度:
- 抽离全局唯一的MediaPlayer播放管理类,不要在Adapter的绑定逻辑中创建MediaPlayer实例
public class AudioPlayerManager { private static AudioPlayerManager instance; private MediaPlayer mediaPlayer; private String currentPlayingUrl; private int currentPlayProgress; private Handler handler; private Runnable progressRunnable; private AudioPlayerManager() { mediaPlayer = new MediaPlayer(); mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); handler = new Handler(Looper.getMainLooper()); } public static synchronized AudioPlayerManager getInstance() { if (instance == null) { instance = new AudioPlayerManager(); } return instance; } // 对外暴露的播放方法 public void play(String url, OnPlayStateChangeListener listener) { // 如果要播放的是当前正在播放的音频,直接回调状态即可,不用重新加载 if (url.equals(currentPlayingUrl)) { if (mediaPlayer.isPlaying()) { pause(); listener.onPause(currentPlayProgress); } else { resume(); listener.onPlay(mediaPlayer.getDuration(), currentPlayProgress); } return; } // 播放新音频,先释放之前的资源 release(); currentPlayingUrl = url; try { mediaPlayer.setDataSource(url); mediaPlayer.prepareAsync(); mediaPlayer.setOnPreparedListener(mp -> { int totalDuration = mp.getDuration(); mp.start(); startProgressUpdate(listener); listener.onPlay(totalDuration, 0); }); mediaPlayer.setOnCompletionListener(mp -> { stopProgressUpdate(); currentPlayProgress = 0; listener.onComplete(); currentPlayingUrl = null; }); mediaPlayer.setOnBufferingUpdateListener((mp, percent) -> { listener.onBufferingUpdate(percent); }); } catch (IOException e) { e.printStackTrace(); listener.onError(e); } } private void resume() { mediaPlayer.seekTo(currentPlayProgress); mediaPlayer.start(); startProgressUpdate(null); } private void pause() { mediaPlayer.pause(); stopProgressUpdate(); currentPlayProgress = mediaPlayer.getCurrentPosition(); } private void startProgressUpdate(OnPlayStateChangeListener listener) { progressRunnable = () -> { currentPlayProgress = mediaPlayer.getCurrentPosition(); if (listener != null) { listener.onProgressUpdate(currentPlayProgress, mediaPlayer.getDuration()); } handler.postDelayed(progressRunnable, 1000); }; handler.post(progressRunnable); } private void stopProgressUpdate() { if (progressRunnable != null) { handler.removeCallbacks(progressRunnable); } } public void release() { if (mediaPlayer.isPlaying()) { mediaPlayer.stop(); } mediaPlayer.reset(); stopProgressUpdate(); currentPlayingUrl = null; currentPlayProgress = 0; } public String getCurrentPlayingUrl() { return currentPlayingUrl; } public int getCurrentPlayProgress() { return currentPlayProgress; } public int getDuration() { return mediaPlayer.getDuration(); } public boolean isPlaying(String url) { return url.equals(currentPlayingUrl) && mediaPlayer.isPlaying(); } public void seekTo(int position) { if (mediaPlayer != null && currentPlayingUrl != null) { mediaPlayer.seekTo(position); currentPlayProgress = position; } } // 状态回调接口 public interface OnPlayStateChangeListener { void onPlay(int totalDuration, int progress); void onPause(int progress); void onComplete(); void onProgressUpdate(int progress, int totalDuration); void onBufferingUpdate(int percent); void onError(Exception e); } }
- 修改Adapter的item绑定逻辑,只做UI和播放状态的绑定,不处理播放器初始化
// onBindViewHolder中只做UI绑定 String audioUrl = finalUrlToLoad[1]; AudioPlayerManager playerManager = AudioPlayerManager.getInstance(); // 先根据当前播放状态初始化UI if (audioUrl.equals(playerManager.getCurrentPlayingUrl())) { binding.loadingAudio.setVisibility(GONE); binding.playPauseAudio.setVisibility(VISIBLE); binding.audioSeekbar.setMax(playerManager.getDuration()); binding.audioSeekbar.setProgress(playerManager.getCurrentPlayProgress()); binding.totalDurationAudio.setText(createTimeLabel(playerManager.getDuration())); if (playerManager.isPlaying(audioUrl)) { binding.playPauseAudio.setImageResource(R.drawable.play_to_pause); } else { binding.playPauseAudio.setImageResource(R.drawable.pause_to_play); } } else { binding.audioSeekbar.setProgress(0); binding.playPauseAudio.setImageResource(R.drawable.pause_to_play); binding.loadingAudio.setVisibility(GONE); binding.playPauseAudio.setVisibility(VISIBLE); } // 播放按钮点击逻辑 binding.playPauseAudio.setOnClickListener(view -> { playerManager.play(audioUrl, new AudioPlayerManager.OnPlayStateChangeListener() { @Override public void onPlay(int totalDuration, int progress) { binding.totalDurationAudio.setText(createTimeLabel(totalDuration)); binding.audioSeekbar.setMax(totalDuration); binding.playPauseAudio.setImageResource(R.drawable.play_to_pause); // 启动动画逻辑和之前一致 Drawable drawable = binding.playPauseAudio.getDrawable(); if( drawable instanceof AnimatedVectorDrawable) { ((AnimatedVectorDrawable) drawable).start(); } } @Override public void onPause(int progress) { binding.audioSeekbar.setProgress(progress); binding.playPauseAudio.setImageResource(R.drawable.pause_to_play); Drawable drawable = binding.playPauseAudio.getDrawable(); if( drawable instanceof AnimatedVectorDrawable) { ((AnimatedVectorDrawable) drawable).start(); } } @Override public void onComplete() { binding.audioSeekbar.setProgress(0); binding.playPauseAudio.setImageResource(R.drawable.pause_to_play); } @Override public void onProgressUpdate(int progress, int totalDuration) { binding.audioSeekbar.setProgress(progress); binding.totalDurationAudio.setText(createTimeLabel(totalDuration)); } @Override public void onBufferingUpdate(int percent) { binding.audioSeekbar.setSecondaryProgress(percent * binding.audioSeekbar.getMax() / 100); } @Override public void onError(Exception e) { // 自定义错误处理逻辑 } }); }); // 拖拽进度条逻辑 binding.audioSeekbar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int i, boolean b) { if (b && audioUrl.equals(playerManager.getCurrentPlayingUrl())) { playerManager.seekTo(i); seekBar.setProgress(i); } } @Override public void onStartTrackingTouch(SeekBar seekBar) { } @Override public void onStopTrackingTouch(SeekBar seekBar) { } });
- 改用DiffUtil实现列表增量更新
不要调用notifyDataSetChanged()刷新整个列表,新消息插入时使用DiffUtil计算新旧列表差异,只更新发生变化的item,避免正在播放的item被重新绑定:
- 改用DiffUtil实现列表增量更新
// 示例DiffUtil Callback实现 public class ChatDiffCallback extends DiffUtil.Callback { private List<ChatMessage> oldList; private List<ChatMessage> newList; public ChatDiffCallback(List<ChatMessage> oldList, List<ChatMessage> newList) { this.oldList = oldList; this.newList = newList; } @Override public int getOldListSize() { return oldList.size(); } @Override public int getNewListSize() { return newList.size(); } @Override public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { return oldList.get(oldItemPosition).getMessageId().equals(newList.get(newItemPosition).getMessageId()); } @Override public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { return oldList.get(oldItemPosition).equals(newList.get(newItemPosition)); } } // 列表更新时调用 DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new ChatDiffCallback(oldList, newList)); diffResult.dispatchUpdatesTo(adapter); adapter.setData(newList);
- 可选优化:添加音频文件缓存
已经加载过的音频文件缓存到本地,下次播放同URL音频时直接读取本地文件,减少重复网络请求,进一步提升体验。
- 可选优化:添加音频文件缓存
内容的提问来源于stack exchange,提问作者user16752070
相关产品推荐
相关产品推荐

