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

如何在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.18 18:45:01