|
|
@@ -0,0 +1,377 @@
|
|
|
+package com.adealink.weparty.commonui.widget.waveview
|
|
|
+
|
|
|
+import android.content.Context
|
|
|
+import android.graphics.BlurMaskFilter
|
|
|
+import android.graphics.Canvas
|
|
|
+import android.graphics.Color
|
|
|
+import android.graphics.DashPathEffect
|
|
|
+import android.graphics.Paint
|
|
|
+import android.graphics.RectF
|
|
|
+import android.os.Handler
|
|
|
+import android.os.Looper
|
|
|
+import android.util.AttributeSet
|
|
|
+import android.view.View
|
|
|
+import androidx.core.content.withStyledAttributes
|
|
|
+import androidx.core.graphics.toColorInt
|
|
|
+import com.adealink.weparty.R
|
|
|
+import java.util.LinkedList
|
|
|
+import kotlin.math.PI
|
|
|
+import kotlin.math.max
|
|
|
+import kotlin.math.min
|
|
|
+import kotlin.math.sin
|
|
|
+import kotlin.random.Random
|
|
|
+
|
|
|
+class SoundWaveView @JvmOverloads constructor(
|
|
|
+ context: Context,
|
|
|
+ attrs: AttributeSet? = null,
|
|
|
+ defStyleAttr: Int = 0
|
|
|
+) : View(context, attrs, defStyleAttr) {
|
|
|
+
|
|
|
+ // 柱状数据
|
|
|
+ private val barHeights = LinkedList<Float>()
|
|
|
+ private val barColors = LinkedList<Int>()
|
|
|
+
|
|
|
+ // 绘制相关
|
|
|
+ private val barPaint = Paint().apply {
|
|
|
+ isAntiAlias = true
|
|
|
+ style = Paint.Style.FILL
|
|
|
+ }
|
|
|
+
|
|
|
+ private val glowPaint = Paint().apply {
|
|
|
+ isAntiAlias = true
|
|
|
+ style = Paint.Style.FILL
|
|
|
+ maskFilter = BlurMaskFilter(15f, BlurMaskFilter.Blur.NORMAL)
|
|
|
+ }
|
|
|
+
|
|
|
+ private val bgPaint = Paint().apply {
|
|
|
+ color = "#0A0A0A".toColorInt()
|
|
|
+ style = Paint.Style.FILL
|
|
|
+ }
|
|
|
+
|
|
|
+ // 渐变颜色
|
|
|
+ private var lowColor = "#4CAF50".toColorInt() // 低分贝 - 绿色
|
|
|
+ private var midColor = "#FF9800".toColorInt() // 中分贝 - 橙色
|
|
|
+ private var highColor = "#F44336".toColorInt() // 高分贝 - 红色
|
|
|
+
|
|
|
+ // 动画控制
|
|
|
+ private val handler = Handler(Looper.getMainLooper())
|
|
|
+ private var isAnimating = false
|
|
|
+ private val animationRunnable = object : Runnable {
|
|
|
+ override fun run() {
|
|
|
+ updateBars()
|
|
|
+ invalidate()
|
|
|
+ if (isAnimating) {
|
|
|
+ handler.postDelayed(this, FRAME_DELAY)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 配置参数
|
|
|
+ var dbValue: Float = 50f
|
|
|
+ set(value) {
|
|
|
+ field = value.coerceIn(0f, 100f)
|
|
|
+ }
|
|
|
+
|
|
|
+ var barWidth: Float = 8f
|
|
|
+ set(value) {
|
|
|
+ field = value.coerceIn(4f, 20f)
|
|
|
+ }
|
|
|
+
|
|
|
+ var barSpacing: Float = 4f
|
|
|
+ set(value) {
|
|
|
+ field = value.coerceIn(2f, 10f)
|
|
|
+ }
|
|
|
+
|
|
|
+ var showGlow: Boolean = true
|
|
|
+ set(value) {
|
|
|
+ field = value
|
|
|
+ invalidate()
|
|
|
+ }
|
|
|
+
|
|
|
+ var waveSpeed: Float = 3f
|
|
|
+ set(value) {
|
|
|
+ field = value.coerceIn(1f, 10f)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 初始化
|
|
|
+ init {
|
|
|
+ // 初始填充一些数据
|
|
|
+ val initialBars = calculateMaxBars()
|
|
|
+ repeat(initialBars) {
|
|
|
+ barHeights.add(height * 0.3f)
|
|
|
+ barColors.add(lowColor)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 从属性获取配置
|
|
|
+ attrs?.let {
|
|
|
+ context.withStyledAttributes(it, R.styleable.SoundWaveView) {
|
|
|
+ barWidth = getDimension(R.styleable.SoundWaveView_barWidth, 8f)
|
|
|
+ barSpacing = getDimension(R.styleable.SoundWaveView_barSpacing, 4f)
|
|
|
+ waveSpeed = getFloat(R.styleable.SoundWaveView_waveSpeed, 3f)
|
|
|
+ showGlow = getBoolean(R.styleable.SoundWaveView_showGlow, true)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
|
|
+ super.onSizeChanged(w, h, oldw, oldh)
|
|
|
+ // 重新初始化数据以适应新的宽度
|
|
|
+ initializeBarData()
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun calculateMaxBars(): Int {
|
|
|
+ val barTotalWidth = barWidth + barSpacing
|
|
|
+ return (width / barTotalWidth).toInt() + 10 // 多加一些作为缓冲
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun initializeBarData() {
|
|
|
+ barHeights.clear()
|
|
|
+ barColors.clear()
|
|
|
+// val maxBars = calculateMaxBars()
|
|
|
+// repeat(maxBars) {
|
|
|
+// barHeights.add(height * 0.3f)
|
|
|
+// barColors.add(lowColor)
|
|
|
+// }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 更新分贝值并添加新的柱状条
|
|
|
+ fun updateDecibel(db: Float) {
|
|
|
+ this.dbValue = db
|
|
|
+
|
|
|
+ // 根据分贝值计算柱状高度
|
|
|
+ val barHeight = calculateBarHeight(dbValue)
|
|
|
+
|
|
|
+ // 根据分贝值确定颜色
|
|
|
+ val barColor = calculateBarColor(dbValue)
|
|
|
+
|
|
|
+ // 添加到数据列表
|
|
|
+ barHeights.add(barHeight)
|
|
|
+ barColors.add(barColor)
|
|
|
+
|
|
|
+ // 保持数据量在合理范围内
|
|
|
+ if (barHeights.size > MAX_BARS) {
|
|
|
+ for (i in 0 until barHeights.size - MAX_BARS) {
|
|
|
+ barHeights.removeFirst()
|
|
|
+ barColors.removeFirst()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun calculateBarHeight(db: Float): Float {
|
|
|
+ // 将分贝值映射为柱状高度(0-100分贝映射为高度的0.1-0.9倍)
|
|
|
+ val normalizedDb = db / 100f
|
|
|
+
|
|
|
+ // 使用非线性映射,使变化更明显
|
|
|
+ val heightRatio = if (normalizedDb < 0.5) {
|
|
|
+ normalizedDb * 1.2f // 低分贝区变化较快
|
|
|
+ } else {
|
|
|
+ 0.6f + (normalizedDb - 0.5f) * 0.6f // 高分贝区变化较慢
|
|
|
+ }
|
|
|
+
|
|
|
+ var height = height * (0.1f + heightRatio * 0.8f)
|
|
|
+
|
|
|
+ // 添加随机抖动,使效果更自然
|
|
|
+ val randomJitter = Random.nextFloat() * height * 0.05f
|
|
|
+ val time = System.currentTimeMillis() / 1000.0
|
|
|
+ val waveJitter = sin(time * PI * 2) * height * 0.03f
|
|
|
+
|
|
|
+ height += (randomJitter + waveJitter.toFloat())
|
|
|
+
|
|
|
+ return height.coerceIn(0f, height.toFloat())
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun calculateBarColor(db: Float): Int {
|
|
|
+ // 根据分贝值计算颜色
|
|
|
+ val normalizedDb = db / 100f
|
|
|
+
|
|
|
+ return when {
|
|
|
+ normalizedDb < 0.33 -> {
|
|
|
+ // 绿色到橙色渐变
|
|
|
+ val ratio = normalizedDb / 0.33f
|
|
|
+ interpolateColor(lowColor, midColor, ratio)
|
|
|
+ }
|
|
|
+
|
|
|
+ normalizedDb < 0.66 -> {
|
|
|
+ // 橙色到红色渐变
|
|
|
+ val ratio = (normalizedDb - 0.33f) / 0.33f
|
|
|
+ interpolateColor(midColor, highColor, ratio)
|
|
|
+ }
|
|
|
+
|
|
|
+ else -> {
|
|
|
+ // 红色
|
|
|
+ highColor
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun interpolateColor(startColor: Int, endColor: Int, ratio: Float): Int {
|
|
|
+ val startA = Color.alpha(startColor)
|
|
|
+ val startR = Color.red(startColor)
|
|
|
+ val startG = Color.green(startColor)
|
|
|
+ val startB = Color.blue(startColor)
|
|
|
+
|
|
|
+ val endA = Color.alpha(endColor)
|
|
|
+ val endR = Color.red(endColor)
|
|
|
+ val endG = Color.green(endColor)
|
|
|
+ val endB = Color.blue(endColor)
|
|
|
+
|
|
|
+ val a = (startA + (endA - startA) * ratio).toInt()
|
|
|
+ val r = (startR + (endR - startR) * ratio).toInt()
|
|
|
+ val g = (startG + (endG - startG) * ratio).toInt()
|
|
|
+ val b = (startB + (endB - startB) * ratio).toInt()
|
|
|
+
|
|
|
+ return Color.argb(a, r, g, b)
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun updateBars() {
|
|
|
+ // 平滑移动:向左移动
|
|
|
+ val barsToMove = (waveSpeed * 0.3).toInt()
|
|
|
+ if (barsToMove > 0 && barHeights.size > barsToMove) {
|
|
|
+ // 移除最左边的条
|
|
|
+ repeat(barsToMove) {
|
|
|
+ barHeights.removeFirst()
|
|
|
+ barColors.removeFirst()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 添加新的条来填充
|
|
|
+ val barHeight = calculateBarHeight(dbValue)
|
|
|
+ val barColor = calculateBarColor(dbValue)
|
|
|
+ repeat(barsToMove) {
|
|
|
+ barHeights.add(barHeight)
|
|
|
+ barColors.add(barColor)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ override fun onDraw(canvas: Canvas) {
|
|
|
+ super.onDraw(canvas)
|
|
|
+
|
|
|
+ // 绘制背景
|
|
|
+ //canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), bgPaint)
|
|
|
+
|
|
|
+ // 绘制柱状声波
|
|
|
+ drawBars(canvas)
|
|
|
+
|
|
|
+ // 绘制中心线
|
|
|
+ drawCenterLine(canvas)
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun drawBars(canvas: Canvas) {
|
|
|
+ if (barHeights.isEmpty()) return
|
|
|
+
|
|
|
+ val barTotalWidth = barWidth + barSpacing
|
|
|
+ val visibleBars = min(barHeights.size, (width / barTotalWidth).toInt() + 1)
|
|
|
+ val startIndex = max(0, barHeights.size - visibleBars)
|
|
|
+
|
|
|
+ // 创建圆形矩形
|
|
|
+ val rect = RectF()
|
|
|
+ val radius = barWidth / 3
|
|
|
+
|
|
|
+ for (i in 0 until visibleBars) {
|
|
|
+ val index = startIndex + i
|
|
|
+ if (index >= barHeights.size) break
|
|
|
+
|
|
|
+ // 计算条形位置(从右向左绘制)
|
|
|
+ val x = width - (barHeights.size - index) * barTotalWidth
|
|
|
+ val barHeight = barHeights[index]
|
|
|
+ val barColor = barColors[index]
|
|
|
+
|
|
|
+ // 计算条形顶部和底部位置(从中心向上下展开)
|
|
|
+ val centerY = height / 2f
|
|
|
+ val top = centerY - barHeight / 2
|
|
|
+ val bottom = centerY + barHeight / 2
|
|
|
+
|
|
|
+ // 设置条形颜色
|
|
|
+ barPaint.color = barColor
|
|
|
+
|
|
|
+ // 绘制发光效果
|
|
|
+ if (showGlow) {
|
|
|
+ glowPaint.color = Color.argb(
|
|
|
+ 100, Color.red(barColor),
|
|
|
+ Color.green(barColor), Color.blue(barColor)
|
|
|
+ )
|
|
|
+
|
|
|
+ rect.set(x - 2, top - 5, x + barWidth + 2, bottom + 5)
|
|
|
+ canvas.drawRoundRect(rect, radius + 2, radius + 2, glowPaint)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 绘制主条形
|
|
|
+ rect.set(x, top, x + barWidth, bottom)
|
|
|
+ canvas.drawRoundRect(rect, radius, radius, barPaint)
|
|
|
+
|
|
|
+ // 绘制条形内部高光
|
|
|
+ if (barHeight > 20) {
|
|
|
+ val highlightPaint = Paint().apply {
|
|
|
+ color = Color.argb(50, 255, 255, 255)
|
|
|
+ style = Paint.Style.FILL
|
|
|
+ }
|
|
|
+ val highlightRect = RectF(
|
|
|
+ x + 1,
|
|
|
+ centerY - barHeight / 4,
|
|
|
+ x + barWidth - 1,
|
|
|
+ centerY
|
|
|
+ )
|
|
|
+ canvas.drawRoundRect(highlightRect, radius / 2, radius / 2, highlightPaint)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun drawCenterLine(canvas: Canvas) {
|
|
|
+ val linePaint = Paint().apply {
|
|
|
+ color = "#33FFFFFF".toColorInt()
|
|
|
+ strokeWidth = 1f
|
|
|
+ pathEffect = DashPathEffect(floatArrayOf(10f, 5f), 0f)
|
|
|
+ }
|
|
|
+ val centerY = height / 2f
|
|
|
+ canvas.drawLine(0f, centerY, width.toFloat(), centerY, linePaint)
|
|
|
+ }
|
|
|
+
|
|
|
+ fun startAnimation() {
|
|
|
+ if (!isAnimating) {
|
|
|
+ isAnimating = true
|
|
|
+ handler.post(animationRunnable)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ fun stopAnimation() {
|
|
|
+ isAnimating = false
|
|
|
+ handler.removeCallbacks(animationRunnable)
|
|
|
+ }
|
|
|
+
|
|
|
+ fun clear() {
|
|
|
+ barHeights.clear()
|
|
|
+ barColors.clear()
|
|
|
+ initializeBarData()
|
|
|
+ invalidate()
|
|
|
+ }
|
|
|
+
|
|
|
+ // 设置自定义颜色
|
|
|
+ fun setColorRange(low: Int, mid: Int, high: Int) {
|
|
|
+ lowColor = low
|
|
|
+ midColor = mid
|
|
|
+ highColor = high
|
|
|
+ invalidate()
|
|
|
+ }
|
|
|
+
|
|
|
+ // 批量更新分贝值(用于真实音频数据)
|
|
|
+ fun updateDecibelBatch(dbs: List<Float>) {
|
|
|
+ dbs.forEach { db ->
|
|
|
+ updateDecibel(db)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ override fun onAttachedToWindow() {
|
|
|
+ super.onAttachedToWindow()
|
|
|
+ startAnimation()
|
|
|
+ }
|
|
|
+
|
|
|
+ override fun onDetachedFromWindow() {
|
|
|
+ super.onDetachedFromWindow()
|
|
|
+ stopAnimation()
|
|
|
+ }
|
|
|
+
|
|
|
+ companion object {
|
|
|
+ private const val FRAME_DELAY = 16L // 约60fps
|
|
|
+ private const val MAX_BARS = 500
|
|
|
+ }
|
|
|
+}
|