From 9e54787a7d69ad130a0255b3a53097109ac91b07 Mon Sep 17 00:00:00 2001 From: Davis Date: Thu, 7 Mar 2024 01:39:02 -0700 Subject: [PATCH] Move Node info completely to Compose (#886) * Move battery info to compose - always show voltage level and icons to match battery percentage Use tool text in preview, rather than actually set text value Simplify node info layout to avoid defining margins on everything * Move node position to Compose * Update hyperlink color to match previous value * Use compose preview in layout editor * Use compose preview in layout editor * Add simple preview for use in layout * Move last heard node info to Compose Clean up layout of node info * Move signal info to Compose and simplify bind * Prevent long coordinates from colliding with signal info * Move the rest of the node info card to compose Breaks the blinking feature when navigating from chat Wrap position to new line if overflow * Adjust layout and text sizing to closer match original * Use constraint layout for tighter display on busy nodes * Construct environment metrics so that there aren't trailing spaces if current is zero * Swap viewholder root for compose view rather than inflating layout Fix padding lost when changing out view holder root Intelligently update the list with only nodes that changed * Remove unused method, and adjust replacement method to match the same decimal precisions as before * Use previous string for denoting unknown node names * Mark unknown short name as non-translatable --- app/build.gradle | 1 + .../main/java/com/geeksville/mesh/NodeInfo.kt | 42 +-- .../com/geeksville/mesh/ui/BatteryInfo.kt | 17 +- .../com/geeksville/mesh/ui/LastHeardInfo.kt | 4 +- .../geeksville/mesh/ui/LinkedCoordinates.kt | 6 +- .../java/com/geeksville/mesh/ui/NodeInfo.kt | 248 ++++++++++++++++++ .../java/com/geeksville/mesh/ui/SignalInfo.kt | 36 ++- .../com/geeksville/mesh/ui/UsersFragment.kt | 129 ++++----- .../ui/preview/PreviewParameterProviders.kt | 108 ++++++-- .../main/res/layout/adapter_node_layout.xml | 111 +------- app/src/main/res/values/strings.xml | 2 + 11 files changed, 465 insertions(+), 239 deletions(-) create mode 100644 app/src/main/java/com/geeksville/mesh/ui/NodeInfo.kt diff --git a/app/build.gradle b/app/build.gradle index 761e8654f..344956d01 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -194,6 +194,7 @@ dependencies { def composeBom = platform('androidx.compose:compose-bom:2024.02.01') implementation composeBom androidTestImplementation composeBom + implementation "androidx.constraintlayout:constraintlayout-compose:1.0.1" implementation 'androidx.compose.material:material' implementation 'androidx.activity:activity-compose' diff --git a/app/src/main/java/com/geeksville/mesh/NodeInfo.kt b/app/src/main/java/com/geeksville/mesh/NodeInfo.kt index bf3ace02e..729e7b187 100644 --- a/app/src/main/java/com/geeksville/mesh/NodeInfo.kt +++ b/app/src/main/java/com/geeksville/mesh/NodeInfo.kt @@ -179,6 +179,32 @@ data class EnvironmentMetrics( override fun toString(): String { return "EnvironmentMetrics(time=${time}, temperature=${temperature}, humidity=${relativeHumidity}, pressure=${barometricPressure}), resistance=${gasResistance}, voltage=${voltage}, current=${current}" } + + fun getDisplayString(inFahrenheit: Boolean = false): String { + val temp = if (temperature != 0f) { + if (inFahrenheit) { + val fahrenheit = temperature * 1.8F + 32 + String.format("%.1f°F", fahrenheit) + } else { + String.format("%.1f°C", temperature) + } + } else null + val humidity = if (relativeHumidity != 0f) String.format("%.0f%%", relativeHumidity) else null + val pressure = if (barometricPressure != 0f) String.format("%.1fhPa", barometricPressure) else null + val gas = if (gasResistance != 0f) String.format("%.0fMΩ", gasResistance) else null + val voltage = if (voltage != 0f) String.format("%.2fV", voltage) else null + val current = if (current != 0f) String.format("%.1fmA", current) else null + + return listOfNotNull( + temp, + humidity, + pressure, + gas, + voltage, + current + ).joinToString(" ") + } + } @Parcelize @@ -213,22 +239,6 @@ data class NodeInfo( val voltage get() = deviceMetrics?.voltage val batteryStr get() = if (batteryLevel in 1..100) String.format("%d%%", batteryLevel) else "" - private fun Float.envFormat(unit: String, decimalPlaces: Int = 1): String = - if (this != 0f) String.format("%.${decimalPlaces}f$unit", this) else "" - - fun envMetricStr(isFahrenheit: Boolean = false): String = buildString { - val env = environmentMetrics ?: return "" - if (env.temperature != 0f) append( - if (!isFahrenheit) env.temperature.envFormat("°C ") - else (env.temperature * 1.8f + 32).envFormat("°F ") - ) - append(env.relativeHumidity.envFormat("%% ", 0)) - append(env.barometricPressure.envFormat("hPa ")) - append(env.gasResistance.envFormat("MΩ ", 0)) - append(env.voltage.envFormat("V ", 2)) - append(env.current.envFormat("mA")) - } - /** * true if the device was heard from recently */ diff --git a/app/src/main/java/com/geeksville/mesh/ui/BatteryInfo.kt b/app/src/main/java/com/geeksville/mesh/ui/BatteryInfo.kt index 8462f6c50..1c1f96628 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/BatteryInfo.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/BatteryInfo.kt @@ -19,7 +19,11 @@ import com.geeksville.mesh.R import com.geeksville.mesh.ui.theme.AppTheme @Composable -fun BatteryInfo(batteryLevel: Int?, voltage: Float?) { +fun BatteryInfo( + modifier: Modifier = Modifier, + batteryLevel: Int?, + voltage: Float? +) { val infoString = "%d%% %.1fV".format(batteryLevel, voltage) val (image, level) = when (batteryLevel) { in 0 .. 4 -> R.drawable.ic_battery_alert to " $infoString" @@ -32,6 +36,7 @@ fun BatteryInfo(batteryLevel: Int?, voltage: Float?) { } Row( + modifier = modifier, verticalAlignment = Alignment.CenterVertically ) { Icon( @@ -56,7 +61,10 @@ fun BatteryInfoPreview( batteryInfo: Pair ) { AppTheme { - BatteryInfo(batteryInfo.first, batteryInfo.second) + BatteryInfo( + batteryLevel = batteryInfo.first, + voltage = batteryInfo.second + ) } } @@ -64,7 +72,10 @@ fun BatteryInfoPreview( @Preview fun BatteryInfoPreviewSimple() { AppTheme { - BatteryInfo(85, 3.7F) + BatteryInfo( + batteryLevel = 85, + voltage = 3.7F + ) } } diff --git a/app/src/main/java/com/geeksville/mesh/ui/LastHeardInfo.kt b/app/src/main/java/com/geeksville/mesh/ui/LastHeardInfo.kt index 4fe38330e..b494d97eb 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/LastHeardInfo.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/LastHeardInfo.kt @@ -19,9 +19,11 @@ import com.geeksville.mesh.util.formatAgo @Composable fun LastHeardInfo( + modifier: Modifier = Modifier, lastHeard: Int ) { Row( + modifier = modifier, verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(2.dp) ) { @@ -44,6 +46,6 @@ fun LastHeardInfo( @Preview(showBackground = true, uiMode = android.content.res.Configuration.UI_MODE_NIGHT_YES) fun LastHeardInfoPreview() { AppTheme { - LastHeardInfo((System.currentTimeMillis() / 1000).toInt() - 8600) + LastHeardInfo(lastHeard = (System.currentTimeMillis() / 1000).toInt() - 8600) } } \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/ui/LinkedCoordinates.kt b/app/src/main/java/com/geeksville/mesh/ui/LinkedCoordinates.kt index 2071a9e1a..b64da7be1 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/LinkedCoordinates.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/LinkedCoordinates.kt @@ -3,6 +3,7 @@ package com.geeksville.mesh.ui import androidx.compose.foundation.text.ClickableText import androidx.compose.material.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.SpanStyle @@ -21,11 +22,12 @@ import java.net.URLEncoder @Composable fun LinkedCoordinates( + modifier : Modifier = Modifier, position: Position?, format: Int, nodeName: String? ) { - if (position != null) { + if (position?.isValid() == true) { val uriHandler = LocalUriHandler.current val style = SpanStyle( color = HyperlinkBlue, @@ -46,8 +48,8 @@ fun LinkedCoordinates( pop() } ClickableText( + modifier = modifier, text = annotatedString, - maxLines = 1, onClick = { offset -> debug("Clicked on link") annotatedString.getStringAnnotations(tag = "gps", start = offset, end = offset) diff --git a/app/src/main/java/com/geeksville/mesh/ui/NodeInfo.kt b/app/src/main/java/com/geeksville/mesh/ui/NodeInfo.kt new file mode 100644 index 000000000..4f622a627 --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/ui/NodeInfo.kt @@ -0,0 +1,248 @@ +package com.geeksville.mesh.ui + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.defaultMinSize +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.material.Card +import androidx.compose.material.Chip +import androidx.compose.material.ChipDefaults +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.LocalTextStyle +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import androidx.constraintlayout.compose.ConstraintLayout +import androidx.constraintlayout.compose.Dimension +import com.geeksville.mesh.NodeInfo +import com.geeksville.mesh.R +import com.geeksville.mesh.ui.preview.NodeInfoPreviewParameterProvider +import com.geeksville.mesh.ui.theme.AppTheme + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun NodeInfo( + thisNodeInfo: NodeInfo, + thatNodeInfo: NodeInfo, + gpsFormat: Int, + distanceUnits: Int, + tempInFahrenheit: Boolean, + isIgnored: Boolean = false, + onClicked: () -> Unit = {} +) { + val unknownShortName = stringResource(id = R.string.unknown_node_short_name) + val unknownLongName = stringResource(id = R.string.unknown_username) + + val nodeName = thatNodeInfo.user?.longName ?: unknownLongName + val isThisNode = thisNodeInfo.num == thatNodeInfo.num + val distance = thisNodeInfo.distanceStr(thatNodeInfo, distanceUnits) + val (textColor, nodeColor) = thatNodeInfo.colors + + Card( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp, vertical = 4.dp) + .defaultMinSize(minHeight = 80.dp) + ) { + Surface { + ConstraintLayout( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + ) { + val (chip, dist, name, pos, batt, heard, sig, env) = createRefs() + val barrierBattHeard = createStartBarrier(batt, heard) + val sigBarrier = createBottomBarrier(pos, heard) + + Box( + // removes the extra spacing above the chip + modifier = Modifier + .height(32.dp) + .constrainAs(chip) { + top.linkTo(parent.top) + start.linkTo(parent.start) + } + ) { + Chip( + modifier = Modifier.width(72.dp), + onClick = onClicked, + colors = ChipDefaults.chipColors( + backgroundColor = Color(nodeColor), + contentColor = Color(textColor) + ), + content = { + Text( + modifier = Modifier.fillMaxWidth(), + text = (thatNodeInfo.user?.shortName ?: unknownShortName).strikeIf(isIgnored), + fontWeight = FontWeight.Normal, + fontSize = MaterialTheme.typography.button.fontSize, + textAlign = TextAlign.Center, + ) + }, + ) + } + + if (distance != null) { + Text( + modifier = Modifier.constrainAs(dist) { + top.linkTo(chip.bottom, 8.dp) + start.linkTo(chip.start) + end.linkTo(chip.end) + }, + text = distance, + fontSize = MaterialTheme.typography.button.fontSize, + ) + } + + val style = if (nodeName == unknownLongName) { + LocalTextStyle.current.copy(fontStyle = FontStyle.Italic) + } else { + LocalTextStyle.current + } + Text( + modifier = Modifier.constrainAs(name) { + top.linkTo(parent.top) + linkTo( + start = chip.end, + end = barrierBattHeard, + bias = 0F, + startMargin = 8.dp, + endMargin = 8.dp, + + ) + width = Dimension.preferredWrapContent + }, + text = nodeName.strikeIf(isIgnored), + style = style + ) + + LinkedCoordinates( + modifier = Modifier.constrainAs(pos) { + linkTo( + top = name.bottom, + bottom = sig.top, + bias = 0F, + topMargin = 4.dp, + bottomMargin = 4.dp + ) + linkTo( + start = name.start, + end = barrierBattHeard, + bias = 0F, + endMargin = 8.dp + ) + width = Dimension.preferredWrapContent + }, + position = thatNodeInfo.position, + format = gpsFormat, + nodeName = nodeName + ) + + BatteryInfo( + modifier = Modifier.constrainAs(batt) { + top.linkTo(parent.top) + end.linkTo(parent.end) + }, + batteryLevel = thatNodeInfo.batteryLevel, + voltage = thatNodeInfo.voltage + ) + + LastHeardInfo( + modifier = Modifier.constrainAs(heard) { + top.linkTo(batt.bottom, 4.dp) + end.linkTo(parent.end) + }, + lastHeard = thatNodeInfo.lastHeard + ) + + SignalInfo( + modifier = Modifier.constrainAs(sig) { + top.linkTo(sigBarrier, 4.dp) + bottom.linkTo(env.top, 4.dp) + end.linkTo(parent.end) + }, + nodeInfo = thatNodeInfo, + isThisNode = isThisNode + ) + + val envMetrics = thatNodeInfo.environmentMetrics + ?.getDisplayString(tempInFahrenheit) ?: "" + if (envMetrics.isNotBlank()) { + Text( + modifier = Modifier.constrainAs(env) { + top.linkTo(sig.bottom, 4.dp) + end.linkTo(parent.end) + }, + text = envMetrics, + color = MaterialTheme.colors.onSurface, + fontSize = MaterialTheme.typography.button.fontSize + ) + } + } + } + } +} + +private fun String.strike() = AnnotatedString( + this, + spanStyles = listOf( + AnnotatedString.Range( + SpanStyle(textDecoration = TextDecoration.LineThrough), + start = 0, + end = this.length + ) + ) +) +private fun String.strikeIf(isIgnored: Boolean): AnnotatedString = if (isIgnored) strike() else AnnotatedString(this) + +@Composable +@Preview(showBackground = false) +fun NodeInfoSimplePreview() { + AppTheme { + val thisNodeInfo = NodeInfoPreviewParameterProvider().values.first() + val thatNodeInfo = NodeInfoPreviewParameterProvider().values.last() + NodeInfo( + thisNodeInfo = thisNodeInfo, + thatNodeInfo = thatNodeInfo, + 1, + 0, + true + ) + } +} + +@Composable +@Preview( + showBackground = true, + uiMode = android.content.res.Configuration.UI_MODE_NIGHT_YES, +) +fun NodeInfoPreview( + @PreviewParameter(NodeInfoPreviewParameterProvider::class) + thatNodeInfo: NodeInfo +) { + AppTheme { + val thisNodeInfo = NodeInfoPreviewParameterProvider().values.first() + NodeInfo( + thisNodeInfo, + thatNodeInfo, + 0, + 1, + true + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/ui/SignalInfo.kt b/app/src/main/java/com/geeksville/mesh/ui/SignalInfo.kt index c092a3dd6..9c780356d 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/SignalInfo.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/SignalInfo.kt @@ -3,6 +3,7 @@ package com.geeksville.mesh.ui import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import com.geeksville.mesh.NodeInfo @@ -11,6 +12,7 @@ import com.geeksville.mesh.ui.theme.AppTheme @Composable fun SignalInfo( + modifier: Modifier = Modifier, nodeInfo: NodeInfo, isThisNode: Boolean ) { @@ -30,6 +32,7 @@ fun SignalInfo( } if (text.isNotEmpty()) { Text( + modifier = modifier, text = text, color = MaterialTheme.colors.onSurface, fontSize = MaterialTheme.typography.button.fontSize @@ -41,16 +44,19 @@ fun SignalInfo( @Preview(showBackground = true) fun SignalInfoSimplePreview() { AppTheme { - SignalInfo(NodeInfo( - num = 1, - position = null, - lastHeard = 0, - channel = 0, - snr = 12.5F, - rssi = -42, - deviceMetrics = null, - user = null - ), false) + SignalInfo( + nodeInfo = NodeInfo( + num = 1, + position = null, + lastHeard = 0, + channel = 0, + snr = 12.5F, + rssi = -42, + deviceMetrics = null, + user = null + ), + isThisNode = false + ) } } @@ -62,7 +68,10 @@ fun SignalInfoPreview( nodeInfo: NodeInfo ) { AppTheme { - SignalInfo(nodeInfo, false) + SignalInfo( + nodeInfo = nodeInfo, + isThisNode = false + ) } } @@ -74,6 +83,9 @@ fun SignalInfoSelfPreview( nodeInfo: NodeInfo ) { AppTheme { - SignalInfo(nodeInfo, true) + SignalInfo( + nodeInfo = nodeInfo, + isThisNode = true + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/ui/UsersFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/UsersFragment.kt index 6313fcf84..2f44db28e 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/UsersFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/UsersFragment.kt @@ -12,6 +12,7 @@ import android.view.View import android.view.ViewGroup import android.view.animation.LinearInterpolator import androidx.appcompat.widget.PopupMenu +import androidx.compose.ui.platform.ComposeView import androidx.core.animation.doOnEnd import androidx.fragment.app.activityViewModels import androidx.lifecycle.asLiveData @@ -20,10 +21,8 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearSmoothScroller import androidx.recyclerview.widget.RecyclerView import com.geeksville.mesh.NodeInfo -import com.geeksville.mesh.Position import com.geeksville.mesh.R import com.geeksville.mesh.android.Logging -import com.geeksville.mesh.databinding.AdapterNodeLayoutBinding import com.geeksville.mesh.databinding.NodelistFragmentBinding import com.geeksville.mesh.model.UIViewModel import com.geeksville.mesh.ui.theme.AppTheme @@ -48,21 +47,11 @@ class UsersFragment : ScreenFragment("Users"), Logging { private var displayUnits = 0 private var displayFahrenheit = false - // Provide a direct reference to each of the views within a data item - // Used to cache the views within the item layout for fast access - class ViewHolder(itemView: AdapterNodeLayoutBinding) : RecyclerView.ViewHolder(itemView.root) { - val chipNode = itemView.chipNode - val nodeNameView = itemView.nodeNameView - val distanceView = itemView.distanceView - val envMetrics = itemView.envMetrics - val background = itemView.nodeCard - val nodePosition = itemView.nodePosition - val batteryInfo = itemView.batteryInfo - val lastHeard = itemView.lastHeardInfo - val signalInfo = itemView.signalInfo + class ViewHolder(val composeView: ComposeView) : RecyclerView.ViewHolder(composeView) { + // TODO not working with compose changes fun blink() { - val bg = background.backgroundTintList + val bg = composeView.backgroundTintList ValueAnimator.ofArgb( Color.parseColor("#00FFFFFF"), Color.parseColor("#33FFFFFF") @@ -73,38 +62,33 @@ class UsersFragment : ScreenFragment("Users"), Logging { repeatCount = 3 repeatMode = ValueAnimator.REVERSE addUpdateListener { - background.backgroundTintList = ColorStateList.valueOf(it.animatedValue as Int) + composeView.backgroundTintList = ColorStateList.valueOf(it.animatedValue as Int) } start() doOnEnd { - background.backgroundTintList = bg + composeView.backgroundTintList = bg } } } fun bind( - nodeInfo: NodeInfo, - isThisNode: Boolean, + thisNodeInfo: NodeInfo, + thatNodeInfo: NodeInfo, gpsFormat: Int, + distanceUnits: Int, + tempInFahrenheit: Boolean, + onChipClicked: () -> Unit ) { - batteryInfo.setContent { + composeView.setContent { AppTheme { - BatteryInfo(nodeInfo.batteryLevel, nodeInfo.voltage) - } - } - nodePosition.setContent { - AppTheme { - LinkedCoordinates(nodeInfo.validPosition, gpsFormat, nodeInfo.user?.longName) - } - } - this.lastHeard.setContent { - AppTheme { - LastHeardInfo(nodeInfo.lastHeard) - } - } - this.signalInfo.setContent { - AppTheme { - SignalInfo(nodeInfo, isThisNode) + NodeInfo( + thisNodeInfo = thisNodeInfo, + thatNodeInfo = thatNodeInfo, + gpsFormat = gpsFormat, + distanceUnits = distanceUnits, + tempInFahrenheit = tempInFahrenheit, + onClicked = onChipClicked + ) } } } @@ -213,13 +197,7 @@ class UsersFragment : ScreenFragment("Users"), Logging { * @see .onBindViewHolder */ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val inflater = LayoutInflater.from(requireContext()) - - // Inflate the custom layout - val contactView = AdapterNodeLayoutBinding.inflate(inflater, parent, false) - - // Return a new holder instance - return ViewHolder(contactView) + return ViewHolder(ComposeView(parent.context)) } /** @@ -251,53 +229,40 @@ class UsersFragment : ScreenFragment("Users"), Logging { * @param position The position of the item within the adapter's data set. */ override fun onBindViewHolder(holder: ViewHolder, position: Int) { - val n = nodes[position] - val (textColor, nodeColor) = n.colors - val isIgnored: Boolean = ignoreIncomingList.contains(n.num) - val isThisNode = n.num == nodes[0].num + val thisNode = nodes[0] + val thatNode = nodes[position] - holder.bind(n, isThisNode, gpsFormat) - - holder.nodeNameView.text = n.user?.longName - - with(holder.chipNode) { - text = (n.user?.shortName ?: "UNK").strikeIf(isIgnored) - chipBackgroundColor = ColorStateList.valueOf(nodeColor) - setTextColor(textColor) - } - - val distance = nodes[0].distanceStr(n, displayUnits) - if (distance != null) { - holder.distanceView.text = distance - holder.distanceView.visibility = View.VISIBLE - } else { - holder.distanceView.visibility = View.INVISIBLE - } - - val envMetrics = n.envMetricStr(displayFahrenheit) - if (envMetrics.isNotEmpty()) { - holder.envMetrics.text = envMetrics - holder.envMetrics.visibility = View.VISIBLE - } else { - holder.envMetrics.visibility = View.GONE - } - - holder.chipNode.setOnClickListener { - popup(it, position) - } - holder.itemView.setOnLongClickListener { - popup(it, position) - true + holder.bind( + thisNodeInfo = thisNode, + thatNodeInfo = thatNode, + gpsFormat = gpsFormat, + distanceUnits = displayUnits, + tempInFahrenheit = displayFahrenheit + ) { + popup(holder.composeView, position) } } - /// Called when our node DB changes + // Called when our node DB changes fun onNodesChanged(nodesIn: Array) { - if (nodesIn.size > 1) + if (nodesIn.size > 1) { nodesIn.sortWith(compareByDescending { it.lastHeard }, 1) + } + + val previousNodes = nodes + val indexChanged = nodesIn.mapIndexed { index, nodeInfo -> + previousNodes.getOrNull(index) != nodeInfo + } + if (indexChanged.isEmpty()) return + nodes = nodesIn - notifyDataSetChanged() // FIXME, this is super expensive and redraws all nodes + for (i in indexChanged.indices) { + if (indexChanged[i]) { + notifyItemChanged(i) + } + } } + } override fun onCreateView( diff --git a/app/src/main/java/com/geeksville/mesh/ui/preview/PreviewParameterProviders.kt b/app/src/main/java/com/geeksville/mesh/ui/preview/PreviewParameterProviders.kt index 3e2a029c6..789b7c869 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/preview/PreviewParameterProviders.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/preview/PreviewParameterProviders.kt @@ -2,26 +2,104 @@ package com.geeksville.mesh.ui.preview import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.geeksville.mesh.DeviceMetrics +import com.geeksville.mesh.DeviceMetrics.Companion.currentTime +import com.geeksville.mesh.EnvironmentMetrics +import com.geeksville.mesh.MeshProtos +import com.geeksville.mesh.MeshUser import com.geeksville.mesh.NodeInfo +import com.geeksville.mesh.Position +import kotlin.random.Random class NodeInfoPreviewParameterProvider: PreviewParameterProvider { + + val mickeyMouse = NodeInfo( + num = 1955, + position = Position( + latitude = 33.812511, + longitude = -117.918976, + altitude = 138, + ), + lastHeard = currentTime(), + channel = 0, + snr = 12.5F, + rssi = -42, + deviceMetrics = DeviceMetrics( + channelUtilization = 2.4F, + airUtilTx = 3.5F, + batteryLevel = 85, + voltage = 3.7F + ), + user = MeshUser( + longName = "Micky Mouse", + shortName = "MM", + id = "mickeyMouseId", + hwModel = MeshProtos.HardwareModel.TBEAM + ) + ) + + private val minnieMouse = mickeyMouse.copy( + num = Random.nextInt(), + user = MeshUser( + longName = "Minnie Mouse", + shortName = "MiMo", + id = "minnieMouseId", + hwModel = MeshProtos.HardwareModel.HELTEC_V3 + ), + snr = 12.5F, + rssi = -42, + position = null + ) + + private val donaldDuck = NodeInfo( + num = Random.nextInt(), + position = Position( + latitude = 33.80523471893125, + longitude = -117.92084605996297, + altitude = 121, + ), + lastHeard = currentTime() - 300, + channel = 0, + snr = 12.5F, + rssi = -42, + deviceMetrics = DeviceMetrics( + channelUtilization = 2.4F, + airUtilTx = 3.5F, + batteryLevel = 85, + voltage = 3.7F + ), + user = MeshUser( + longName = "Donald Duck, the Grand Duck of the Ducks", + shortName = "DoDu", + id = "donaldDuckId", + hwModel = MeshProtos.HardwareModel.HELTEC_V3, + ), + environmentMetrics = EnvironmentMetrics( + temperature = 28.0F, + relativeHumidity = 50.0F, + barometricPressure = 1013.25F, + gasResistance = 0.0F, + voltage = 3.7F, + current = 0.0F + ) + ) + + private val unknown = donaldDuck.copy( + user = null, + environmentMetrics = null + ) + + private val almostNothing = NodeInfo( + num = Random.nextInt(), + ) + override val values: Sequence get() = sequenceOf( - NodeInfo( - num = 1, - position = null, - lastHeard = 0, - channel = 0, - snr = 12.5F, - rssi = -42, - deviceMetrics = DeviceMetrics( - channelUtilization = 2.4F, - airUtilTx = 3.5F, - batteryLevel = 85, - voltage = 3.7F - ), - user = null - ) + mickeyMouse, // "this" node + unknown, + almostNothing, + minnieMouse, + donaldDuck ) + } \ No newline at end of file diff --git a/app/src/main/res/layout/adapter_node_layout.xml b/app/src/main/res/layout/adapter_node_layout.xml index 0817f6ff3..96216953c 100644 --- a/app/src/main/res/layout/adapter_node_layout.xml +++ b/app/src/main/res/layout/adapter_node_layout.xml @@ -1,123 +1,18 @@ - + tools:composableName="com.geeksville.mesh.ui.NodeInfoKt.NodeInfoSimplePreview" + /> - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5cb7f18a3..bd825c2fe 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -11,6 +11,8 @@ 55.332244 34.442211 hey I found the cache, it is over here next to the big tiger. I\'m kinda scared. + \??? + Channel Name Channel options QR code