Replace RX/MIC bars with S-meter (S1-S9, +20..+60, peak-hold)
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
124
app/src/main/java/ru/ua1zbe/svxremote/ui/SMeterView.kt
Normal file
124
app/src/main/java/ru/ua1zbe/svxremote/ui/SMeterView.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -141,40 +141,22 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical">
|
||||
android:gravity="top">
|
||||
|
||||
<TextView
|
||||
android:layout_width="34dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp"
|
||||
android:text="@string/meter_rx"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="13sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<FrameLayout
|
||||
<ru.ua1zbe.svxremote.ui.SMeterView
|
||||
android:id="@+id/rx_meter"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="16dp"
|
||||
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" />
|
||||
android:layout_height="52dp"
|
||||
android:layout_weight="1" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
@@ -182,40 +164,22 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical">
|
||||
android:gravity="top">
|
||||
|
||||
<TextView
|
||||
android:layout_width="34dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp"
|
||||
android:text="@string/meter_mic"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="13sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<FrameLayout
|
||||
<ru.ua1zbe.svxremote.ui.SMeterView
|
||||
android:id="@+id/mic_meter"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="16dp"
|
||||
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" />
|
||||
android:layout_height="52dp"
|
||||
android:layout_weight="1" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user