diff --git a/app/src/main/java/com/geeksville/mesh/ui/NodeInfo.kt b/app/src/main/java/com/geeksville/mesh/ui/NodeInfo.kt index 4f622a62..b8709c01 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/NodeInfo.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/NodeInfo.kt @@ -29,10 +29,14 @@ 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.ConfigProtos import com.geeksville.mesh.NodeInfo import com.geeksville.mesh.R +import com.geeksville.mesh.ui.compose.ElevationInfo +import com.geeksville.mesh.ui.compose.SatelliteCountInfo import com.geeksville.mesh.ui.preview.NodeInfoPreviewParameterProvider import com.geeksville.mesh.ui.theme.AppTheme +import com.geeksville.mesh.util.metersIn @OptIn(ExperimentalMaterialApi::class) @Composable @@ -65,7 +69,7 @@ fun NodeInfo( .fillMaxWidth() .padding(8.dp) ) { - val (chip, dist, name, pos, batt, heard, sig, env) = createRefs() + val (chip, dist, name, pos, alt, sats, batt, heard, sig, env) = createRefs() val barrierBattHeard = createStartBarrier(batt, heard) val sigBarrier = createBottomBarrier(pos, heard) @@ -131,6 +135,7 @@ fun NodeInfo( style = style ) + val position = thatNodeInfo.position LinkedCoordinates( modifier = Modifier.constrainAs(pos) { linkTo( @@ -148,11 +153,60 @@ fun NodeInfo( ) width = Dimension.preferredWrapContent }, - position = thatNodeInfo.position, + position = position, format = gpsFormat, nodeName = nodeName ) + val signalShown = signalInfo( + modifier = Modifier.constrainAs(sig) { + top.linkTo(sigBarrier, 4.dp) + bottom.linkTo(env.top, 4.dp) + end.linkTo(parent.end) + }, + nodeInfo = thatNodeInfo, + isThisNode = isThisNode + ) + + if (position?.isValid() == true) { + val system = ConfigProtos.Config.DisplayConfig.DisplayUnits.forNumber(distanceUnits) + val altitude = position.altitude.metersIn(system) + val elevationSuffix = stringResource(id = R.string.elevation_suffix) + + ElevationInfo( + modifier = Modifier.constrainAs(alt) { + top.linkTo(pos.bottom, 4.dp) + if (signalShown) { + baseline.linkTo(sig.baseline) + } + linkTo( + start = pos.start, + end = sig.start, + endMargin = 8.dp, + bias = 0F, + ) + width = Dimension.preferredWrapContent + }, + altitude = altitude, + system = system, + suffix = elevationSuffix + ) + + SatelliteCountInfo( + modifier = Modifier.constrainAs(sats) { + top.linkTo(alt.bottom, 4.dp) + linkTo( + start = pos.start, + end = env.start, + endMargin = 8.dp, + bias = 0F, + ) + width = Dimension.preferredWrapContent + }, + satCount = position.satellitesInView + ) + } + BatteryInfo( modifier = Modifier.constrainAs(batt) { top.linkTo(parent.top) @@ -170,22 +224,16 @@ fun NodeInfo( 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) + if (signalShown) { + top.linkTo(sig.bottom, 4.dp) + } else { + top.linkTo(pos.bottom, 4.dp) + } end.linkTo(parent.end) }, text = envMetrics, 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 9c780356..3f7dc9eb 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/SignalInfo.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/SignalInfo.kt @@ -11,11 +11,11 @@ import com.geeksville.mesh.ui.preview.NodeInfoPreviewParameterProvider import com.geeksville.mesh.ui.theme.AppTheme @Composable -fun SignalInfo( +fun signalInfo( modifier: Modifier = Modifier, nodeInfo: NodeInfo, isThisNode: Boolean -) { +): Boolean { val text = if (isThisNode) { "ChUtil %.1f%% AirUtilTX %.1f%%".format( nodeInfo.deviceMetrics?.channelUtilization, @@ -30,13 +30,16 @@ fun SignalInfo( } } } - if (text.isNotEmpty()) { + return if (text.isNotEmpty()) { Text( modifier = modifier, text = text, color = MaterialTheme.colors.onSurface, fontSize = MaterialTheme.typography.button.fontSize ) + true + } else { + false } } @@ -44,7 +47,7 @@ fun SignalInfo( @Preview(showBackground = true) fun SignalInfoSimplePreview() { AppTheme { - SignalInfo( + signalInfo( nodeInfo = NodeInfo( num = 1, position = null, @@ -68,7 +71,7 @@ fun SignalInfoPreview( nodeInfo: NodeInfo ) { AppTheme { - SignalInfo( + signalInfo( nodeInfo = nodeInfo, isThisNode = false ) @@ -83,9 +86,9 @@ fun SignalInfoSelfPreview( nodeInfo: NodeInfo ) { AppTheme { - SignalInfo( + 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 2f44db28..5bd02332 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/UsersFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/UsersFragment.kt @@ -4,8 +4,6 @@ import android.animation.ValueAnimator import android.content.res.ColorStateList import android.graphics.Color import android.os.Bundle -import android.text.SpannableString -import android.text.style.StrikethroughSpan import android.view.LayoutInflater import android.view.MenuItem import android.view.View @@ -99,12 +97,6 @@ class UsersFragment : ScreenFragment("Users"), Logging { var nodes = arrayOf() private set - private fun CharSequence.strike() = SpannableString(this).apply { - setSpan(StrikethroughSpan(), 0, this.length, SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE) - } - - private fun CharSequence.strikeIf(isIgnored: Boolean) = if (isIgnored) strike() else this - private fun popup(view: View, position: Int) { if (!model.isConnected()) return val node = nodes[position] @@ -173,61 +165,12 @@ class UsersFragment : ScreenFragment("Users"), Logging { popup.show() } - /** - * Called when RecyclerView needs a new [ViewHolder] of the given type to represent - * an item. - * - * - * This new ViewHolder should be constructed with a new View that can represent the items - * of the given type. You can either create a new View manually or inflate it from an XML - * layout file. - * - * - * The new ViewHolder will be used to display items of the adapter using - * [.onBindViewHolder]. Since it will be re-used to display - * different items in the data set, it is a good idea to cache references to sub views of - * the View to avoid unnecessary [View.findViewById] calls. - * - * @param parent The ViewGroup into which the new View will be added after it is bound to - * an adapter position. - * @param viewType The view type of the new View. - * - * @return A new ViewHolder that holds a View of the given view type. - * @see .getItemViewType - * @see .onBindViewHolder - */ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { return ViewHolder(ComposeView(parent.context)) } - /** - * Returns the total number of items in the data set held by the adapter. - * - * @return The total number of items in this adapter. - */ override fun getItemCount(): Int = nodes.size - /** - * Called by RecyclerView to display the data at the specified position. This method should - * update the contents of the [ViewHolder.itemView] to reflect the item at the given - * position. - * - * - * Note that unlike [android.widget.ListView], RecyclerView will not call this method - * again if the position of the item changes in the data set unless the item itself is - * invalidated or the new position cannot be determined. For this reason, you should only - * use the `position` parameter while acquiring the related data item inside - * this method and should not keep a copy of it. If you need the position of an item later - * on (e.g. in a click listener), use [ViewHolder.getAdapterPosition] which will - * have the updated adapter position. - * - * Override [.onBindViewHolder] instead if Adapter can - * handle efficient partial bind. - * - * @param holder The ViewHolder which should be updated to represent the contents of the - * item at the given position in the data set. - * @param position The position of the item within the adapter's data set. - */ override fun onBindViewHolder(holder: ViewHolder, position: Int) { val thisNode = nodes[0] val thatNode = nodes[position] @@ -243,6 +186,10 @@ class UsersFragment : ScreenFragment("Users"), Logging { } } + override fun onViewRecycled(holder: ViewHolder) { + holder.composeView.disposeComposition() + } + // Called when our node DB changes fun onNodesChanged(nodesIn: Array) { if (nodesIn.size > 1) { diff --git a/app/src/main/java/com/geeksville/mesh/ui/compose/ElevationInfo.kt b/app/src/main/java/com/geeksville/mesh/ui/compose/ElevationInfo.kt new file mode 100644 index 00000000..05ab3c2f --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/ui/compose/ElevationInfo.kt @@ -0,0 +1,46 @@ +package com.geeksville.mesh.ui.compose + +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.tooling.preview.Preview +import com.geeksville.mesh.ConfigProtos.Config.DisplayConfig.DisplayUnits +import com.geeksville.mesh.util.toString + +@Composable +fun ElevationInfo( + modifier: Modifier = Modifier, + altitude: Float, + system: DisplayUnits, + suffix: String +) { + val annotatedString = buildAnnotatedString { + append(altitude.toString(system)) + MaterialTheme.typography.overline.toSpanStyle().let { style -> + withStyle(style) { + append(" $suffix") + } + } + } + + Text( + modifier = modifier, + fontSize = MaterialTheme.typography.button.fontSize, + text = annotatedString, + ) +} + +@Composable +@Preview +fun ElevationInfoPreview() { + MaterialTheme { + ElevationInfo( + altitude = 100.0f, + system = DisplayUnits.METRIC, + suffix = "ASL" + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/ui/compose/SatelliteCountInfo.kt b/app/src/main/java/com/geeksville/mesh/ui/compose/SatelliteCountInfo.kt new file mode 100644 index 00000000..318be324 --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/ui/compose/SatelliteCountInfo.kt @@ -0,0 +1,57 @@ +package com.geeksville.mesh.ui.compose + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.height +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.geeksville.mesh.R +import com.geeksville.mesh.ui.theme.AppTheme + +@Composable +fun SatelliteCountInfo( + modifier: Modifier = Modifier, + satCount: Int, +) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + Icon( + modifier = Modifier.height(18.dp), + imageVector = ImageVector.vectorResource(id = R.drawable.ic_satellite), + contentDescription = null, + tint = MaterialTheme.colors.onSurface, + ) + Text( + text = "$satCount", + fontSize = MaterialTheme.typography.button.fontSize, + color = MaterialTheme.colors.onSurface, + ) + } +} + +@Composable +@Preview( + showBackground = true, + uiMode = android.content.res.Configuration.UI_MODE_NIGHT_YES +) +@Preview( + showBackground = true, +) +fun SatelliteCountInfoPreview() { + AppTheme { + SatelliteCountInfo( + satCount = 5, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/util/DistanceExtensions.kt b/app/src/main/java/com/geeksville/mesh/util/DistanceExtensions.kt new file mode 100644 index 00000000..fd692470 --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/util/DistanceExtensions.kt @@ -0,0 +1,46 @@ +package com.geeksville.mesh.util + +import com.geeksville.mesh.ConfigProtos + +enum class DistanceUnit( + val symbol: String, + val multiplier: Float, + val system: Int +) { + METERS("m", 1F, ConfigProtos.Config.DisplayConfig.DisplayUnits.METRIC_VALUE), + KILOMETERS("km", 0.001F, ConfigProtos.Config.DisplayConfig.DisplayUnits.METRIC_VALUE), + FEET("ft", 3.28084F, ConfigProtos.Config.DisplayConfig.DisplayUnits.IMPERIAL_VALUE), + MILES("mi", 0.000621371F, ConfigProtos.Config.DisplayConfig.DisplayUnits.IMPERIAL_VALUE), +} + +fun Int.metersIn(unit: DistanceUnit): Float { + return this * unit.multiplier +} + +fun Int.metersIn(system: ConfigProtos.Config.DisplayConfig.DisplayUnits): Float { + return this * when (system.number) { + ConfigProtos.Config.DisplayConfig.DisplayUnits.METRIC_VALUE -> DistanceUnit.METERS.multiplier + ConfigProtos.Config.DisplayConfig.DisplayUnits.IMPERIAL_VALUE -> DistanceUnit.FEET.multiplier + else -> throw IllegalArgumentException("Unknown distance system $system") + } +} + +fun Float.toString(unit: DistanceUnit): String { + return "%.1f %s".format(this, unit.symbol) +} + +fun Float.toString( + system: ConfigProtos.Config.DisplayConfig.DisplayUnits +): String { + return "%.1f %s".format(this, + when (system.number) { + ConfigProtos.Config.DisplayConfig.DisplayUnits.METRIC_VALUE -> { + DistanceUnit.METERS.symbol + } + ConfigProtos.Config.DisplayConfig.DisplayUnits.IMPERIAL_VALUE -> { + DistanceUnit.FEET.symbol + } + else -> throw IllegalArgumentException("Unknown distance system $system") + }, + ) +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_satellite.xml b/app/src/main/res/drawable/ic_satellite.xml new file mode 100644 index 00000000..b9bbd50a --- /dev/null +++ b/app/src/main/res/drawable/ic_satellite.xml @@ -0,0 +1,14 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bd825c2f..f6ec70be 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -13,6 +13,8 @@ \??? + ASL + Channel Name Channel options QR code