Replace RX/MIC bars with S-meter (S1-S9, +20..+60, peak-hold)

This commit is contained in:
ua1zbe
2026-08-18 15:15:34 +03:00
parent b63ef121f9
commit 88739f17a9
3 changed files with 144 additions and 65 deletions

View File

@@ -35,6 +35,7 @@ import ru.ua1zbe.svxremote.prefs.Prefs
import ru.ua1zbe.svxremote.proto.MsgParser import ru.ua1zbe.svxremote.proto.MsgParser
import ru.ua1zbe.svxremote.proto.ReflectorClient import ru.ua1zbe.svxremote.proto.ReflectorClient
import ru.ua1zbe.svxremote.ui.PttButton import ru.ua1zbe.svxremote.ui.PttButton
import ru.ua1zbe.svxremote.ui.SMeterView
import ru.ua1zbe.svxremote.ui.SoundMeterPreviewView import ru.ua1zbe.svxremote.ui.SoundMeterPreviewView
private const val REQ_MIC = 1001 private const val REQ_MIC = 1001
@@ -58,10 +59,8 @@ class MainActivity : Activity(), SvxService.Listener {
private lateinit var lblTxState: TextView private lateinit var lblTxState: TextView
private lateinit var lblConnectHint: TextView private lateinit var lblConnectHint: TextView
private lateinit var connectZone: View private lateinit var connectZone: View
private lateinit var rxBarFill: View private lateinit var rxMeter: SMeterView
private lateinit var rxDb: TextView private lateinit var micMeter: SMeterView
private lateinit var micBarFill: View
private lateinit var micDb: TextView
private lateinit var pttButton: PttButton private lateinit var pttButton: PttButton
private lateinit var btnSettings: ImageButton private lateinit var btnSettings: ImageButton
private lateinit var btnMonitor: ImageButton private lateinit var btnMonitor: ImageButton
@@ -134,10 +133,9 @@ class MainActivity : Activity(), SvxService.Listener {
connectZone = findViewById(R.id.connect_zone) connectZone = findViewById(R.id.connect_zone)
lblCallsign = findViewById(R.id.lbl_callsign) lblCallsign = findViewById(R.id.lbl_callsign)
lblTg = findViewById(R.id.lbl_tg) lblTg = findViewById(R.id.lbl_tg)
rxBarFill = findViewById(R.id.rx_bar_fill) rxMeter = findViewById(R.id.rx_meter)
rxDb = findViewById(R.id.rx_db) micMeter = findViewById(R.id.mic_meter)
micBarFill = findViewById(R.id.mic_bar_fill) micMeter.setColor(0xFFF85149.toInt())
micDb = findViewById(R.id.mic_db)
pttButton = findViewById(R.id.ptt_button) pttButton = findViewById(R.id.ptt_button)
btnSettings = findViewById(R.id.btn_settings) btnSettings = findViewById(R.id.btn_settings)
btnMonitor = findViewById(R.id.btn_monitor) btnMonitor = findViewById(R.id.btn_monitor)
@@ -482,17 +480,10 @@ class MainActivity : Activity(), SvxService.Listener {
override fun onLevel(source: Int, level: Float) { override fun onLevel(source: Int, level: Float) {
mainHandler.post { 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 if (source == 0) { // SRC_MIC
micBarFill.scaleX = level.coerceIn(0f, 1f) micMeter.setLevel(level)
micDb.text = db
} else { // SRC_SPEAKER } else { // SRC_SPEAKER
rxBarFill.scaleX = level.coerceIn(0f, 1f) rxMeter.setLevel(level)
rxDb.text = db
} }
} }
} }

View File

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

View File

@@ -141,40 +141,22 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:orientation="horizontal"
android:gravity="center_vertical"> android:gravity="top">
<TextView <TextView
android:layout_width="34dp" android:layout_width="34dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:text="@string/meter_rx" android:text="@string/meter_rx"
android:textColor="@color/text_secondary" android:textColor="@color/text_secondary"
android:textSize="13sp" android:textSize="13sp"
android:textStyle="bold" /> android:textStyle="bold" />
<FrameLayout <ru.ua1zbe.svxremote.ui.SMeterView
android:id="@+id/rx_meter"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="16dp" android:layout_height="52dp"
android:layout_weight="1" android:layout_weight="1" />
android:background="@drawable/bg_meter">
<View
android:id="@+id/rx_bar_fill"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/bg_meter_fill_rx"
android:pivotX="0"
android:scaleX="0" />
</FrameLayout>
<TextView
android:id="@+id/rx_db"
android:layout_width="64dp"
android:layout_height="wrap_content"
android:gravity="end"
android:fontFamily="monospace"
android:text="@string/meter_idle"
android:textColor="@color/text_primary"
android:textSize="13sp" />
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout
@@ -182,40 +164,22 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="10dp" android:layout_marginTop="10dp"
android:orientation="horizontal" android:orientation="horizontal"
android:gravity="center_vertical"> android:gravity="top">
<TextView <TextView
android:layout_width="34dp" android:layout_width="34dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:text="@string/meter_mic" android:text="@string/meter_mic"
android:textColor="@color/text_secondary" android:textColor="@color/text_secondary"
android:textSize="13sp" android:textSize="13sp"
android:textStyle="bold" /> android:textStyle="bold" />
<FrameLayout <ru.ua1zbe.svxremote.ui.SMeterView
android:id="@+id/mic_meter"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="16dp" android:layout_height="52dp"
android:layout_weight="1" android:layout_weight="1" />
android:background="@drawable/bg_meter">
<View
android:id="@+id/mic_bar_fill"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/bg_meter_fill_mic"
android:pivotX="0"
android:scaleX="0" />
</FrameLayout>
<TextView
android:id="@+id/mic_db"
android:layout_width="64dp"
android:layout_height="wrap_content"
android:gravity="end"
android:fontFamily="monospace"
android:text="@string/meter_idle"
android:textColor="@color/text_primary"
android:textSize="13sp" />
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>