DoggyZhang 3 месяцев назад
Родитель
Сommit
7f6a8e97f0

+ 377 - 0
app/src/main/java/com/adealink/weparty/commonui/widget/waveview/SoundWaveView.kt

@@ -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
+    }
+}

+ 7 - 0
app/src/main/res/values/attrs.xml

@@ -591,4 +591,11 @@
         <attr name="price_text_size" format="float" />
         <attr name="unit_text_size" format="float" />
     </declare-styleable>
+
+    <declare-styleable name="SoundWaveView">
+        <attr name="barWidth" format="dimension" />
+        <attr name="barSpacing" format="dimension" />
+        <attr name="waveSpeed" format="float" />
+        <attr name="showGlow" format="boolean" />
+    </declare-styleable>
 </resources>

+ 1 - 1
module/account/src/main/java/com/adealink/weparty/account/login/viewmodel/LoginViewModel.kt

@@ -47,7 +47,7 @@ class LoginViewModel : BaseViewModel(), ILoginViewModel, ILoginListener {
         override fun onSuccess(authType: ThirdType, token: String, codeVerifier: String?) {
             cancelCountdown()
 
-            Log.d("zhangfei", "authCallback.onSuccess, $authType, $token, $codeVerifier")
+            Log.d(TAG_ACCOUNT_LOGIN, "authCallback.onSuccess, $authType, $token, $codeVerifier")
             authLogin(authType, token, codeVerifier = codeVerifier)
         }
 

+ 0 - 2
module/im/src/main/java/com/adealink/weparty/im/list/adapter/viewbinder/SessionListItemViewBinder.kt

@@ -4,7 +4,6 @@ import android.view.LayoutInflater
 import android.view.ViewGroup
 import com.adealink.frame.data.json.froJsonErrorNull
 import com.adealink.frame.dot.NumDot
-import com.adealink.frame.log.Log
 import com.adealink.frame.util.onClick
 import com.adealink.weparty.commonui.recycleview.adapter.BindingViewHolder
 import com.adealink.weparty.commonui.recycleview.adapter.multitype.ItemViewBinder
@@ -25,7 +24,6 @@ class SessionListItemViewBinder(
         holder: BindingViewHolder<LayoutSessionListItemBinding>,
         item: CommonSessionListItemData,
     ) {
-        Log.d("zhangfei", "onBindViewHolder, $item")
         holder.binding.root.onClick {
             listener.onItemClick(it, item.data.type, item.data)
         }

+ 52 - 20
module/im/src/main/java/com/adealink/weparty/im/session/comp/SessionBottomAudioComp.kt

@@ -2,11 +2,13 @@ package com.adealink.weparty.im.session.comp
 
 import android.annotation.SuppressLint
 import androidx.lifecycle.LifecycleOwner
+import com.adealink.frame.aab.util.getCompatColor
 import com.adealink.frame.log.Log
 import com.adealink.frame.mvvm.view.ViewComponent
 import com.adealink.frame.mvvm.viewmodel.activityViewModels
 import com.adealink.frame.util.onClick
 import com.adealink.frame.util.runOnUiThread
+import com.adealink.weparty.commonui.ext.dpf
 import com.adealink.weparty.commonui.toast.util.showToast
 import com.adealink.weparty.im.R
 import com.adealink.weparty.im.databinding.LayoutSessionBottomVoiceBarBinding
@@ -25,6 +27,9 @@ import com.tencent.qcloud.tuikit.tuichat.presenter.ChatPresenter
 import com.tencent.qcloud.tuikit.tuichat.util.ChatMessageBuilder
 import java.util.Timer
 import java.util.TimerTask
+import kotlin.math.max
+import kotlin.math.pow
+import com.adealink.weparty.R as APP_R
 
 @SuppressLint("SetTextI18n")
 class SessionBottomAudioComp(
@@ -34,6 +39,10 @@ class SessionBottomAudioComp(
     val presenter: ChatPresenter? = null
 ) : ViewComponent(lifecycleOwner) {
 
+    companion object {
+        private const val MIN_VOICE_DB = 2f
+    }
+
     private var mTimer: Timer? = null
     private var times = 0
     private val inputViewModel by activityViewModels<SessionInputViewModel>()
@@ -150,9 +159,7 @@ class SessionBottomAudioComp(
                     showToast(R.string.im_audio_say_time_short)
                     return
                 }
-//                if (mVoiceWaveView != null) {
-//                    mVoiceWaveView.stop()
-//                }
+                voiceBar.vVoiceWave.stopAnimation()
                 sendAudioMessage(outputPath, duration)
             }
 
@@ -176,9 +183,7 @@ class SessionBottomAudioComp(
                     TAG_IM_AUDIO,
                     "record audio failed, errorCode $errorCode, errorMessage $errorMessage"
                 )
-//                if (mVoiceWaveView != null) {
-//                    mVoiceWaveView.stop()
-//                }
+                voiceBar.vVoiceWave.stopAnimation()
             }
 
             override fun onCanceled() {
@@ -186,23 +191,18 @@ class SessionBottomAudioComp(
 //                if (mChatInputHandler != null) {
 //                    mChatInputHandler.onRecordStatusChanged(InputView.ChatInputHandler.RECORD_CANCEL)
 //                }
-//                if (mVoiceWaveView != null) {
-//                    mVoiceWaveView.stop()
-//                }
+                voiceBar.vVoiceWave.stopAnimation()
                 hideVoiceLayout()
             }
 
             override fun onVoiceDb(db: Double) {
-                //Log.d(TAG_IM_AUDIO, "startAudioRecord, onVoiceDb: $db")
-                //var db = db
-                // TODO: 声音分贝
-//                if (mSendAudioButtonLayout.getVisibility() == View.VISIBLE) {
-//                    if (db == 0.0) {
-//                        db = 2.0
-//                    }
-//                    mVoiceWaveView.addBody(db.toInt())
-//                    mVoiceWaveView.start()
-//                }
+                Log.d(TAG_IM_AUDIO, "startAudioRecord, onVoiceDb: $db")
+                voiceBar.vVoiceWave.updateDecibel(
+                    max(
+                        soundWaveDbInterpolator(db).toFloat(), MIN_VOICE_DB
+                    )
+                )
+                voiceBar.vVoiceWave.startAnimation()
             }
         })
     }
@@ -220,7 +220,7 @@ class SessionBottomAudioComp(
 
     private fun showVoiceLayout() {
         Log.d(TAG_IM_AUDIO, "showVoiceLayout")
-//        initVoiceWaveView()
+        initVoiceWaveView()
 //        mSendAudioButtonLayout.setVisibility(View.VISIBLE)
 //        voiceBtn.setVisibility(View.GONE)
 //        hideInputMoreButton()
@@ -229,6 +229,29 @@ class SessionBottomAudioComp(
 //        showVoiceDeleteImage()
     }
 
+    private fun initVoiceWaveView() {
+        voiceBar.vVoiceWave.setColorRange(
+            getCompatColor(APP_R.color.color_FF4E5969),
+            getCompatColor(APP_R.color.color_FF4E5969),
+            getCompatColor(APP_R.color.color_FF4E5969)
+        )
+        voiceBar.vVoiceWave.barSpacing = 2.dpf()
+        voiceBar.vVoiceWave.showGlow = false
+        voiceBar.vVoiceWave.clear()
+//        voiceBar.vVoiceWave.addHeader(MIN_VOICE_DB)
+//        for (i in 0..100) {
+//            voiceBar.vVoiceWave.addBody(MIN_VOICE_DB)
+//        }
+//        voiceBar.vVoiceWave.addFooter(MIN_VOICE_DB)
+//        voiceBar.vVoiceWave.addHeader(0)
+//        for (i in 0..100) {
+//            voiceBar.vVoiceWave.addBody(0)
+//        }
+//        voiceBar.vVoiceWave.addFooter(0)
+//        voiceDeleteImage.setBackgroundResource(R.drawable.minimalist_delete_icon)
+//        mSendAudioButton.setBackground(getResources().getDrawable(R.drawable.minimalist_corner_bg_blue))
+    }
+
     private fun stopAudioRecord() {
         Log.d(TAG_IM_AUDIO, "stopAudioRecord")
         AudioRecorder.stopRecord()
@@ -274,4 +297,13 @@ class SessionBottomAudioComp(
             if ((miss % 3600) % 60 > 9) ((miss % 3600) % 60).toString() + "" else "0" + (miss % 3600) % 60
         return "$mm:$ss"
     }
+
+    /**
+     * 声音分贝数值(0-100)
+     */
+    private fun soundWaveDbInterpolator(db: Double): Double {
+        val dbp = db / 100.0
+        val factor = 2.0
+        return db * dbp.pow(factor)
+    }
 }

+ 15 - 1
module/im/src/main/res/layout/layout_session_bottom_voice_bar.xml

@@ -17,17 +17,31 @@
 
         <androidx.appcompat.widget.AppCompatTextView
             android:id="@+id/tv_record_time"
-            android:layout_width="wrap_content"
+            android:layout_width="50dp"
             android:layout_height="wrap_content"
             android:fontFamily="@font/poppins_semibold"
+            android:gravity="start|center_vertical"
             android:includeFontPadding="false"
             android:textColor="@color/color_FF1D2129"
             android:textSize="18sp"
+            app:autoSizeMaxTextSize="18sp"
+            app:autoSizeMinTextSize="15sp"
+            app:autoSizeTextType="uniform"
             app:layout_constraintBottom_toBottomOf="parent"
             app:layout_constraintStart_toStartOf="parent"
             app:layout_constraintTop_toTopOf="parent"
             tools:text="0:03" />
 
+        <com.adealink.weparty.commonui.widget.waveview.SoundWaveView
+            android:id="@+id/v_voice_wave"
+            android:layout_width="0dp"
+            android:layout_height="28dp"
+            android:layout_marginStart="6dp"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toEndOf="@id/tv_record_time"
+            app:layout_constraintTop_toTopOf="parent"
+            tools:ignore="RtlHardcoded" />
 
     </androidx.constraintlayout.widget.ConstraintLayout>