如何在Android中实现类似WhatsApp语音消息的SeekBar可视化及播放样式
Android实现类似WhatsApp语音消息的无依赖方案
一、静态气泡布局实现
首先完成语音消息的基础样式,区分发送方和接收方的气泡,用原生ShapeDrawable实现圆角背景,布局包含播放按钮、波形容器、时长文本。
接收方语音布局示例
<LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="start" android:background="@drawable/voice_receive_bg" android:paddingHorizontal="12dp" android:paddingVertical="8dp" android:orientation="horizontal" android:gravity="center_vertical"> <ImageView android:id="@+id/iv_play" android:layout_width="24dp" android:layout_height="24dp" android:src="@drawable/ic_play_circle" /> <com.yourpackage.WaveView android:id="@+id/wave_view" android:layout_width="60dp" android:layout_height="24dp" android:layout_marginHorizontal="8dp" /> <TextView android:id="@+id/tv_duration" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="00:18" android:textSize="12sp" /> </LinearLayout>
气泡背景Drawable
接收方背景(res/drawable/voice_receive_bg.xml):
<shape xmlns:android="http://schemas.android.com/apk/res/android"> <solid android:color="#f1f1f1" /> <corners android:radius="16dp" android:topLeftRadius="4dp" /> <!-- 模仿WhatsApp的差异化圆角 --> </shape>
发送方背景仅需修改solid颜色为主题色,layout_gravity设为end即可。
二、播放动态波形实现
自定义View绘制随机变化的波形,无需第三方库:
class WaveView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : View(context, attrs, defStyleAttr) { private val paint = Paint(Paint.ANTI_ALIAS_FLAG) private val waveHeights = mutableListOf(8f, 8f, 8f) // 初始高度 private val random = Random() private var isPlaying = false private val animator = ValueAnimator.ofFloat(0f, 1f).apply { duration = 300 repeatCount = ValueAnimator.INFINITE addUpdateListener { if (isPlaying) { updateWaves() invalidate() } } } init { paint.color = Color.parseColor("#666666") paint.style = Paint.Style.FILL } private fun updateWaves() { waveHeights.forEachIndexed { index, _ -> waveHeights[index] = (random.nextInt(14) + 6).toFloat() // 随机高度6-20dp } } fun startAnim() { isPlaying = true animator.start() } fun stopAnim() { isPlaying = false animator.cancel() waveHeights.fill(8f) invalidate() } override fun onDraw(canvas: Canvas) { super.onDraw(canvas) val width = measuredWidth.toFloat() val height = measuredHeight.toFloat() val barWidth = width / 6 val centerY = height / 2 waveHeights.forEachIndexed { index, h -> val left = index * 2 * barWidth val top = centerY - h val right = left + barWidth val bottom = centerY + h canvas.drawRoundRect(left, top, right, bottom, barWidth/2, barWidth/2, paint) } } }
三、音频播放控制逻辑
使用Android原生MediaPlayer实现播放/暂停控制,同步UI状态:
class VoiceMsgHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { private val ivPlay = itemView.findViewById<ImageView>(R.id.iv_play) private val waveView = itemView.findViewById<WaveView>(R.id.wave_view) private val tvDuration = itemView.findViewById<TextView>(R.id.tv_duration) private var mediaPlayer: MediaPlayer? = null private var isPlaying = false private lateinit var audioPath: String fun bind(voiceData: VoiceMessage) { audioPath = voiceData.filePath tvDuration.text = formatDuration(voiceData.durationMs) ivPlay.setOnClickListener { if (isPlaying) stopPlay() else startPlay() } } private fun startPlay() { isPlaying = true ivPlay.setImageResource(R.drawable.ic_pause_circle) waveView.startAnim() mediaPlayer = MediaPlayer().apply { setDataSource(audioPath) prepareAsync() setOnPreparedListener { start() } setOnCompletionListener { stopPlay() } setOnErrorListener { _, _, _ -> stopPlay() true } } } private fun stopPlay() { isPlaying = false ivPlay.setImageResource(R.drawable.ic_play_circle) waveView.stopAnim() mediaPlayer?.let { if (it.isPlaying) it.stop() it.release() } mediaPlayer = null } private fun formatDuration(durationMs: Int): String { val minutes = durationMs / 1000 / 60 val seconds = durationMs / 1000 % 60 return String.format("%02d:%02d", minutes, seconds) } // ViewHolder复用时释放资源 fun release() { stopPlay() } }
四、额外优化点
- 全局播放状态管理:当播放新语音时,自动停止列表中其他正在播放的语音
- 音频时长预计算:可以通过
MediaMetadataRetriever提前获取音频时长,避免播放时才加载 - 长按菜单:用
PopupMenu实现转发、删除等操作 - 播放进度同步:若需要显示播放进度条,可通过
MediaPlayer的getCurrentPosition()实时更新
内容的提问来源于stack exchange,提问作者Murad Farhat
相关产品推荐
相关产品推荐

