kopia lustrzana https://github.com/meshtastic/Meshtastic-Android
feat: Scrollable Signal Chart (#1505)
* Removed repeated calculation. * Centralized the radius used to plot points and draw lines within GraphUtil.kt. * Updated the signal metrics chart to use the scroll features. * SignalMetricsChart long method warning suppression.pull/1490/head
rodzic
d14a8de78e
commit
70a08c9d31
|
@ -65,6 +65,7 @@ import com.geeksville.mesh.ui.BatteryInfo
|
|||
import com.geeksville.mesh.ui.components.CommonCharts.MS_PER_SEC
|
||||
import com.geeksville.mesh.ui.components.CommonCharts.DATE_TIME_FORMAT
|
||||
import com.geeksville.mesh.ui.theme.Orange
|
||||
import com.geeksville.mesh.util.GraphUtil
|
||||
import com.geeksville.mesh.util.GraphUtil.plotPoint
|
||||
import com.geeksville.mesh.util.GraphUtil.createPath
|
||||
|
||||
|
@ -155,12 +156,12 @@ private fun DeviceMetricsChart(
|
|||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
val graphColor = MaterialTheme.colors.onSurface
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
val scrollState = rememberScrollState()
|
||||
val configuration = LocalConfiguration.current
|
||||
val screenWidth = configuration.screenWidthDp
|
||||
val dp by remember(key1 = selectedTime) {
|
||||
mutableStateOf(selectedTime.dp(screenWidth, time = (newest.time - oldest.time).toLong()))
|
||||
mutableStateOf(selectedTime.dp(screenWidth, time = timeDiff.toLong()))
|
||||
}
|
||||
|
||||
Row {
|
||||
|
@ -195,7 +196,6 @@ private fun DeviceMetricsChart(
|
|||
|
||||
val height = size.height
|
||||
val width = size.width
|
||||
val dataPointRadius = 2.dp.toPx()
|
||||
for (i in telemetries.indices) {
|
||||
val telemetry = telemetries[i]
|
||||
|
||||
|
@ -207,7 +207,6 @@ private fun DeviceMetricsChart(
|
|||
plotPoint(
|
||||
drawContext = drawContext,
|
||||
color = DEVICE_METRICS_COLORS[Device.CH_UTIL.ordinal],
|
||||
radius = dataPointRadius,
|
||||
x = x,
|
||||
value = telemetry.deviceMetrics.channelUtilization,
|
||||
divisor = MAX_PERCENT_VALUE
|
||||
|
@ -217,7 +216,6 @@ private fun DeviceMetricsChart(
|
|||
plotPoint(
|
||||
drawContext = drawContext,
|
||||
color = DEVICE_METRICS_COLORS[Device.AIR_UTIL.ordinal],
|
||||
radius = dataPointRadius,
|
||||
x = x,
|
||||
value = telemetry.deviceMetrics.airUtilTx,
|
||||
divisor = MAX_PERCENT_VALUE
|
||||
|
@ -246,7 +244,7 @@ private fun DeviceMetricsChart(
|
|||
path = path,
|
||||
color = DEVICE_METRICS_COLORS[Device.BATTERY.ordinal],
|
||||
style = Stroke(
|
||||
width = dataPointRadius,
|
||||
width = GraphUtil.RADIUS,
|
||||
cap = StrokeCap.Round
|
||||
)
|
||||
)
|
||||
|
|
|
@ -17,9 +17,8 @@
|
|||
|
||||
package com.geeksville.mesh.ui.components
|
||||
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Typeface
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
|
@ -31,8 +30,10 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.material.Card
|
||||
import androidx.compose.material.Surface
|
||||
|
@ -46,11 +47,8 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.nativeCanvas
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
|
@ -60,11 +58,10 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||
import com.geeksville.mesh.MeshProtos.MeshPacket
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.model.MetricsViewModel
|
||||
import com.geeksville.mesh.model.TimeFrame
|
||||
import com.geeksville.mesh.ui.components.CommonCharts.MS_PER_SEC
|
||||
import com.geeksville.mesh.ui.components.CommonCharts.LINE_LIMIT
|
||||
import com.geeksville.mesh.ui.components.CommonCharts.TEXT_PAINT_ALPHA
|
||||
import com.geeksville.mesh.ui.components.CommonCharts.LEFT_LABEL_SPACING
|
||||
import com.geeksville.mesh.ui.components.CommonCharts.DATE_TIME_FORMAT
|
||||
import com.geeksville.mesh.util.GraphUtil.plotPoint
|
||||
|
||||
private val METRICS_COLORS = listOf(Color.Green, Color.Blue)
|
||||
|
||||
|
@ -108,6 +105,7 @@ fun SignalMetricsScreen(
|
|||
.fillMaxWidth()
|
||||
.fillMaxHeight(fraction = 0.33f),
|
||||
meshPackets = data.reversed(),
|
||||
selectedTimeFrame,
|
||||
promptInfoDialog = { displayInfoDialog = true }
|
||||
)
|
||||
|
||||
|
@ -126,21 +124,30 @@ fun SignalMetricsScreen(
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
private fun SignalMetricsChart(
|
||||
modifier: Modifier = Modifier,
|
||||
meshPackets: List<MeshPacket>,
|
||||
selectedTime: TimeFrame,
|
||||
promptInfoDialog: () -> Unit
|
||||
) {
|
||||
|
||||
ChartHeader(amount = meshPackets.size)
|
||||
if (meshPackets.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
val (oldest, newest) = remember(key1 = meshPackets) {
|
||||
Pair(
|
||||
meshPackets.minBy { it.rxTime },
|
||||
meshPackets.maxBy { it.rxTime }
|
||||
)
|
||||
}
|
||||
val timeDiff = newest.rxTime - oldest.rxTime
|
||||
|
||||
TimeLabels(
|
||||
oldest = meshPackets.first().rxTime,
|
||||
newest = meshPackets.last().rxTime
|
||||
oldest = oldest.rxTime,
|
||||
newest = newest.rxTime
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
@ -149,51 +156,75 @@ private fun SignalMetricsChart(
|
|||
val snrDiff = Metric.SNR.difference()
|
||||
val rssiDiff = Metric.RSSI.difference()
|
||||
|
||||
Box(contentAlignment = Alignment.TopStart) {
|
||||
val scrollState = rememberScrollState()
|
||||
val configuration = LocalConfiguration.current
|
||||
val screenWidth = configuration.screenWidthDp
|
||||
val dp by remember(key1 = selectedTime) {
|
||||
mutableStateOf(selectedTime.dp(screenWidth, time = (newest.rxTime - oldest.rxTime).toLong()))
|
||||
}
|
||||
|
||||
ChartOverlay(
|
||||
modifier = modifier,
|
||||
lineColors = List(size = 5) { graphColor },
|
||||
labelColor = METRICS_COLORS[Metric.SNR.ordinal],
|
||||
minValue = Metric.SNR.min,
|
||||
maxValue = Metric.SNR.max,
|
||||
leaveSpace = true
|
||||
Row {
|
||||
YAxisLabels(
|
||||
modifier = modifier.weight(weight = .1f),
|
||||
METRICS_COLORS[Metric.RSSI.ordinal],
|
||||
minValue = Metric.RSSI.min,
|
||||
maxValue = Metric.RSSI.max,
|
||||
)
|
||||
LeftYLabels(modifier = modifier, labelColor = METRICS_COLORS[Metric.RSSI.ordinal])
|
||||
Box(
|
||||
contentAlignment = Alignment.TopStart,
|
||||
modifier = Modifier
|
||||
.horizontalScroll(state = scrollState, reverseScrolling = true)
|
||||
.weight(1f)
|
||||
) {
|
||||
HorizontalLinesOverlay(
|
||||
modifier.width(dp),
|
||||
lineColors = List(size = 5) { graphColor },
|
||||
minValue = Metric.SNR.min,
|
||||
maxValue = Metric.SNR.max
|
||||
)
|
||||
|
||||
/* Plot SNR and RSSI */
|
||||
Canvas(modifier = modifier) {
|
||||
TimeAxisOverlay(
|
||||
modifier.width(dp),
|
||||
oldest = oldest.rxTime,
|
||||
newest = newest.rxTime,
|
||||
selectedTime.lineInterval()
|
||||
)
|
||||
|
||||
val height = size.height
|
||||
val width = size.width - 28.dp.toPx()
|
||||
val spacing = LEFT_LABEL_SPACING.dp.toPx()
|
||||
val spacePerEntry = (width - spacing) / meshPackets.size
|
||||
/* Plot SNR and RSSI */
|
||||
Canvas(modifier = modifier.width(dp)) {
|
||||
val width = size.width
|
||||
/* Plot */
|
||||
for (packet in meshPackets) {
|
||||
|
||||
/* Plot */
|
||||
val dataPointRadius = 2.dp.toPx()
|
||||
for ((i, packet) in meshPackets.withIndex()) {
|
||||
val xRatio = (packet.rxTime - oldest.rxTime).toFloat() / timeDiff
|
||||
val x = xRatio * width
|
||||
|
||||
val x = spacing + i * spacePerEntry
|
||||
/* SNR */
|
||||
plotPoint(
|
||||
drawContext = drawContext,
|
||||
color = METRICS_COLORS[Metric.SNR.ordinal],
|
||||
x = x,
|
||||
value = packet.rxSnr - Metric.SNR.min,
|
||||
divisor = snrDiff
|
||||
)
|
||||
|
||||
/* SNR */
|
||||
val snrRatio = (packet.rxSnr - Metric.SNR.min) / snrDiff
|
||||
val ySNR = height - (snrRatio * height)
|
||||
drawCircle(
|
||||
color = METRICS_COLORS[Metric.SNR.ordinal],
|
||||
radius = dataPointRadius,
|
||||
center = Offset(x, ySNR)
|
||||
)
|
||||
|
||||
/* RSSI */
|
||||
val rssiRatio = (packet.rxRssi - Metric.RSSI.min) / rssiDiff
|
||||
val yRssi = height - (rssiRatio * height)
|
||||
drawCircle(
|
||||
color = METRICS_COLORS[Metric.RSSI.ordinal],
|
||||
radius = dataPointRadius,
|
||||
center = Offset(x, yRssi)
|
||||
)
|
||||
/* RSSI */
|
||||
plotPoint(
|
||||
drawContext = drawContext,
|
||||
color = METRICS_COLORS[Metric.RSSI.ordinal],
|
||||
x = x,
|
||||
value = packet.rxRssi - Metric.RSSI.min,
|
||||
divisor = rssiDiff
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
YAxisLabels(
|
||||
modifier = modifier.weight(weight = .1f),
|
||||
METRICS_COLORS[Metric.SNR.ordinal],
|
||||
minValue = Metric.SNR.min,
|
||||
maxValue = Metric.SNR.max,
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
@ -203,48 +234,6 @@ private fun SignalMetricsChart(
|
|||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a set of Y labels on the left side of the graph.
|
||||
* Currently only used for the RSSI labels.
|
||||
*/
|
||||
@Composable
|
||||
private fun LeftYLabels(
|
||||
modifier: Modifier,
|
||||
labelColor: Color,
|
||||
) {
|
||||
val range = Metric.RSSI.difference()
|
||||
val verticalSpacing = range / LINE_LIMIT
|
||||
val density = LocalDensity.current
|
||||
Canvas(modifier = modifier) {
|
||||
|
||||
val height = size.height
|
||||
|
||||
/* Y Labels */
|
||||
|
||||
val textPaint = Paint().apply {
|
||||
color = labelColor.toArgb()
|
||||
textAlign = Paint.Align.LEFT
|
||||
textSize = density.run { 12.dp.toPx() }
|
||||
typeface = setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.BOLD))
|
||||
alpha = TEXT_PAINT_ALPHA
|
||||
}
|
||||
drawContext.canvas.nativeCanvas.apply {
|
||||
var label = Metric.RSSI.min
|
||||
for (i in 0..LINE_LIMIT) {
|
||||
val ratio = (label - Metric.RSSI.min) / range
|
||||
val y = height - (ratio * height)
|
||||
drawText(
|
||||
"${label.toInt()}",
|
||||
4.dp.toPx(),
|
||||
y + 4.dp.toPx(),
|
||||
textPaint
|
||||
)
|
||||
label += verticalSpacing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SignalMetricsCard(meshPacket: MeshPacket) {
|
||||
val time = meshPacket.rxTime * MS_PER_SEC
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
|
||||
package com.geeksville.mesh.util
|
||||
|
||||
import android.content.res.Resources
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
@ -25,6 +26,8 @@ import com.geeksville.mesh.TelemetryProtos.Telemetry
|
|||
|
||||
object GraphUtil {
|
||||
|
||||
val RADIUS = Resources.getSystem().displayMetrics.density * 2
|
||||
|
||||
/**
|
||||
* @param value Must be zero-scaled before passing.
|
||||
* @param divisor The range for the data set.
|
||||
|
@ -32,7 +35,6 @@ object GraphUtil {
|
|||
fun plotPoint(
|
||||
drawContext: DrawContext,
|
||||
color: Color,
|
||||
radius: Float,
|
||||
x: Float,
|
||||
value: Float,
|
||||
divisor: Float,
|
||||
|
@ -42,7 +44,7 @@ object GraphUtil {
|
|||
val y = height - (ratio * height)
|
||||
drawContext.canvas.drawCircle(
|
||||
center = Offset(x, y),
|
||||
radius = radius,
|
||||
radius = RADIUS,
|
||||
paint = androidx.compose.ui.graphics.Paint().apply { this.color = color }
|
||||
)
|
||||
}
|
||||
|
|
Ładowanie…
Reference in New Issue