From 88739f17a982edbaf9d329446696d076c54db87a Mon Sep 17 00:00:00 2001 From: ua1zbe Date: Tue, 18 Aug 2026 15:15:34 +0300 Subject: [PATCH] Replace RX/MIC bars with S-meter (S1-S9, +20..+60, peak-hold) --- .../java/ru/ua1zbe/svxremote/MainActivity.kt | 25 ++-- .../java/ru/ua1zbe/svxremote/ui/SMeterView.kt | 124 ++++++++++++++++++ app/src/main/res/layout/activity_main.xml | 60 ++------- 3 files changed, 144 insertions(+), 65 deletions(-) create mode 100644 app/src/main/java/ru/ua1zbe/svxremote/ui/SMeterView.kt diff --git a/app/src/main/java/ru/ua1zbe/svxremote/MainActivity.kt b/app/src/main/java/ru/ua1zbe/svxremote/MainActivity.kt index 463ee1a..5814bd2 100644 --- a/app/src/main/java/ru/ua1zbe/svxremote/MainActivity.kt +++ b/app/src/main/java/ru/ua1zbe/svxremote/MainActivity.kt @@ -35,6 +35,7 @@ import ru.ua1zbe.svxremote.prefs.Prefs import ru.ua1zbe.svxremote.proto.MsgParser import ru.ua1zbe.svxremote.proto.ReflectorClient import ru.ua1zbe.svxremote.ui.PttButton +import ru.ua1zbe.svxremote.ui.SMeterView import ru.ua1zbe.svxremote.ui.SoundMeterPreviewView private const val REQ_MIC = 1001 @@ -58,10 +59,8 @@ class MainActivity : Activity(), SvxService.Listener { private lateinit var lblTxState: TextView private lateinit var lblConnectHint: TextView private lateinit var connectZone: View - private lateinit var rxBarFill: View - private lateinit var rxDb: TextView - private lateinit var micBarFill: View - private lateinit var micDb: TextView + private lateinit var rxMeter: SMeterView + private lateinit var micMeter: SMeterView private lateinit var pttButton: PttButton private lateinit var btnSettings: ImageButton private lateinit var btnMonitor: ImageButton @@ -134,10 +133,9 @@ class MainActivity : Activity(), SvxService.Listener { connectZone = findViewById(R.id.connect_zone) lblCallsign = findViewById(R.id.lbl_callsign) lblTg = findViewById(R.id.lbl_tg) - rxBarFill = findViewById(R.id.rx_bar_fill) - rxDb = findViewById(R.id.rx_db) - micBarFill = findViewById(R.id.mic_bar_fill) - micDb = findViewById(R.id.mic_db) + rxMeter = findViewById(R.id.rx_meter) + micMeter = findViewById(R.id.mic_meter) + micMeter.setColor(0xFFF85149.toInt()) pttButton = findViewById(R.id.ptt_button) btnSettings = findViewById(R.id.btn_settings) btnMonitor = findViewById(R.id.btn_monitor) @@ -482,17 +480,10 @@ class MainActivity : Activity(), SvxService.Listener { override fun onLevel(source: Int, level: Float) { mainHandler.post { - val db = if (level <= 0.001f) { - getString(R.string.meter_idle) - } else { - getString(R.string.meter_db, (level * 60).toInt().coerceIn(1, 60)) - } if (source == 0) { // SRC_MIC - micBarFill.scaleX = level.coerceIn(0f, 1f) - micDb.text = db + micMeter.setLevel(level) } else { // SRC_SPEAKER - rxBarFill.scaleX = level.coerceIn(0f, 1f) - rxDb.text = db + rxMeter.setLevel(level) } } } diff --git a/app/src/main/java/ru/ua1zbe/svxremote/ui/SMeterView.kt b/app/src/main/java/ru/ua1zbe/svxremote/ui/SMeterView.kt new file mode 100644 index 0000000..8dc2b4a --- /dev/null +++ b/app/src/main/java/ru/ua1zbe/svxremote/ui/SMeterView.kt @@ -0,0 +1,124 @@ +package ru.ua1zbe.svxremote.ui + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Paint +import android.os.Handler +import android.os.Looper +import android.util.AttributeSet +import android.view.View +import kotlin.math.roundToInt + +/** + * S-метр: полоса со шкалой S1–S9, +20…+60, заливкой уровня, белой пик-меткой + * (peak-hold) и цифрой текущего уровня справа («S9», «+43» или «---»). + * Уровень 0..1; S9 = 55% пути, +60 = 88%. + */ +class SMeterView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : View(context, attrs) { + + private var fillColor = 0xFF58A6FF.toInt() + private val track = Paint().apply { color = 0xFF21262D.toInt() } + private val fill = Paint().apply { color = fillColor } + private val tick = Paint().apply { color = 0xFF8B949E.toInt(); strokeWidth = dp(1f) } + private val tickMajor = Paint().apply { color = 0xFF8B949E.toInt(); strokeWidth = dp(1.5f) } + private val peakPaint = Paint().apply { color = 0xFFFFFFFF.toInt(); strokeWidth = dp(2f) } + private val label = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = 0xFF8B949E.toInt() + textSize = sp(9) + } + private val valuePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = 0xFFE6EDF3.toInt() + textSize = sp(12) + textAlign = Paint.Align.RIGHT + } + private val idlePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = 0xFF8B949E.toInt() + textSize = sp(12) + textAlign = Paint.Align.RIGHT + } + + @Volatile + private var target = 0f + private var shown = 0f + private var peak = 0f + + private val handler = Handler(Looper.getMainLooper()) + private val ticker = object : Runnable { + override fun run() { + // Сглаживание уровня и медленный спад пика + shown += (target - shown) * 0.35f + if (Math.abs(target - shown) < 0.002f) shown = target + peak = maxOf(peak * 0.985f, shown) + if (peak < 0.003f) peak = 0f + invalidate() + handler.postDelayed(this, 100) + } + } + + fun setColor(color: Int) { + fillColor = color + fill.color = color + } + + fun setLevel(level: Float) { + target = level.coerceIn(0f, 1f) + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + handler.post(ticker) + } + + override fun onDetachedFromWindow() { + handler.removeCallbacksAndMessages(null) + super.onDetachedFromWindow() + } + + private fun dp(v: Float): Float = v * resources.displayMetrics.density + private fun sp(v: Int): Float = v * resources.displayMetrics.scaledDensity + + private fun levelText(level: Float): String = when { + level < 0.005f -> "---" + level < 0.55f -> "S${(level / 0.55f * 9f).roundToInt().coerceIn(1, 9)}" + else -> "+${(((level - 0.55f) / 0.45f) * 60f).roundToInt().coerceIn(1, 99)}" + } + + override fun onDraw(canvas: Canvas) { + val w = width.toFloat() + val h = height.toFloat() + val left = dp(2f) + val right = w - dp(2f) + val bandTop = dp(4f) + val bandH = dp(14f) + + // полоса + canvas.drawRect(left, bandTop, right, bandTop + bandH, track) + val lvl = shown.coerceIn(0f, 1f) + if (lvl > 0.002f) { + canvas.drawRect(left, bandTop, left + lvl * (right - left), bandTop + bandH, fill) + } + + // деления: S1..S9, +20..+60 + val marks = listOf("S1" to 0.11f, "S3" to 0.22f, "S5" to 0.33f, "S7" to 0.44f, + "S9" to 0.55f, "+20" to 0.66f, "+40" to 0.77f, "+60" to 0.88f) + for ((text, pos) in marks) { + val mx = left + pos * (right - left) + val major = pos == 0.55f || pos == 0.88f + canvas.drawLine(mx, bandTop, mx, bandTop + bandH, if (major) tickMajor else tick) + canvas.drawText(text, mx - dp(8f), bandTop + bandH + dp(11f), label) + } + + // пик-метка + if (peak > 0.003f) { + val px = left + peak.coerceIn(0f, 1f) * (right - left) + canvas.drawLine(px, bandTop - dp(3f), px, bandTop + bandH + dp(3f), peakPaint) + } + + // цифра уровня справа от полосы (и чуть выше делений) + val p = if (target < 0.005f) idlePaint else valuePaint + canvas.drawText(levelText(target), right, bandTop + bandH - dp(3f), p) + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index bcd4ac5..25207b8 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -141,40 +141,22 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" - android:gravity="center_vertical"> + android:gravity="top"> - - - - - - + android:layout_height="52dp" + android:layout_weight="1" /> + android:gravity="top"> - - - - - - + android:layout_height="52dp" + android:layout_weight="1" />