kopia lustrzana https://github.com/meshtastic/Meshtastic-Android
Add elevation and number of GPS satellites to node info (#895)
* 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 * Add elevation and number of GPS satellites to node info list Add some extension functions for easier conversion between units and systems * Dispose composition on recycle to avoid lingering spacing from previous layouts Remove comments explaning adapter functionality Remove unused methods * Use previous string for denoting unknown node names * Align properly if altitude but no signal infopull/902/head
rodzic
1468b26d3b
commit
248982d14c
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<NodeInfo>()
|
||||
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<NodeInfo>) {
|
||||
if (nodesIn.size > 1) {
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
},
|
||||
)
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
<vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
>
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M11.62,1L17.28,6.67L15.16,8.79L13.04,6.67L11.62,8.09L13.95,10.41L12.79,11.58L13.24,12.04C14.17,11.61 15.31,11.77 16.07,12.54L12.54,16.07C11.77,15.31 11.61,14.17 12.04,13.24L11.58,12.79L10.41,13.95L8.09,11.62L6.67,13.04L8.79,15.16L6.67,17.28L1,11.62L3.14,9.5L5.26,11.62L6.67,10.21L3.84,7.38C3.06,6.6 3.06,5.33 3.84,4.55L4.55,3.84C5.33,3.06 6.6,3.06 7.38,3.84L10.21,6.67L11.62,5.26L9.5,3.14L11.62,1M18,14A4,4 0,0 1,14 18V16A2,2 0,0 0,16 14H18M22,14A8,8 0,0 1,14 22V20A6,6 0,0 0,20 14H22Z"
|
||||
android:fillAlpha="0.5"
|
||||
/>
|
||||
</vector>
|
|
@ -13,6 +13,8 @@
|
|||
|
||||
<string name="unknown_node_short_name" translatable="false">\???</string>
|
||||
|
||||
<string name="elevation_suffix">ASL</string>
|
||||
|
||||
<string name="channel_name">Channel Name</string>
|
||||
<string name="channel_options">Channel options</string>
|
||||
<string name="qr_code">QR code</string>
|
||||
|
|
Ładowanie…
Reference in New Issue