mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
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
This commit is contained in:
parent
4ba67beb53
commit
9e54787a7d
11 changed files with 465 additions and 239 deletions
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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<Int?, Float?>
|
||||
) {
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
248
app/src/main/java/com/geeksville/mesh/ui/NodeInfo.kt
Normal file
248
app/src/main/java/com/geeksville/mesh/ui/NodeInfo.kt
Normal file
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<NodeInfo>) {
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -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<NodeInfo> {
|
||||
|
||||
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<NodeInfo>
|
||||
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
|
||||
)
|
||||
|
||||
}
|
||||
|
|
@ -1,123 +1,18 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipToPadding="false"
|
||||
>
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
style="@style/Widget.App.CardView"
|
||||
<androidx.compose.ui.platform.ComposeView
|
||||
android:id="@+id/nodeCard"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
>
|
||||
tools:composableName="com.geeksville.mesh.ui.NodeInfoKt.NodeInfoSimplePreview"
|
||||
/>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="8dp"
|
||||
>
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/chip_node"
|
||||
android:layout_width="72dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAlignment="center"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="@string/some_username"
|
||||
/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/nodeNameView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
app:layout_constraintStart_toEndOf="@+id/chip_node"
|
||||
app:layout_constraintTop_toTopOf="@+id/chip_node"
|
||||
tools:text="@string/unknown_username"
|
||||
/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/distance_view"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toBottomOf="@+id/chip_node"
|
||||
app:layout_constraintBottom_toTopOf="@id/envMetrics"
|
||||
app:layout_constraintStart_toStartOf="@+id/chip_node"
|
||||
app:layout_constraintEnd_toEndOf="@+id/chip_node"
|
||||
android:layout_marginVertical="8dp"
|
||||
app:layout_constraintVertical_bias="0"
|
||||
tools:text="@string/sample_distance"
|
||||
/>
|
||||
|
||||
<androidx.compose.ui.platform.ComposeView
|
||||
android:id="@+id/nodePosition"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintStart_toEndOf="@+id/chip_node"
|
||||
app:layout_constraintTop_toBottomOf="@+id/nodeNameView"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="8dp"
|
||||
tools:composableName="com.geeksville.mesh.ui.LinkedCoordinatesKt.LinkedCoordinatesSimplePreview"
|
||||
/>
|
||||
|
||||
<androidx.compose.ui.platform.ComposeView
|
||||
android:id="@+id/batteryInfo"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
tools:composableName="com.geeksville.mesh.ui.BatteryInfoKt.BatteryInfoPreviewSimple"
|
||||
/>
|
||||
|
||||
<androidx.compose.ui.platform.ComposeView
|
||||
android:id="@+id/lastHeardInfo"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toBottomOf="@+id/batteryInfo"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_marginTop="4dp"
|
||||
tools:composableName="com.geeksville.mesh.ui.LastHeardInfoKt.LastHeardInfoPreview"
|
||||
/>
|
||||
|
||||
<androidx.constraintlayout.widget.Barrier
|
||||
android:id="@+id/barrier"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:barrierDirection="bottom"
|
||||
app:constraint_referenced_ids="nodePosition, lastHeardInfo"
|
||||
/>
|
||||
|
||||
<androidx.compose.ui.platform.ComposeView
|
||||
android:id="@+id/signalInfo"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toBottomOf="@id/barrier"
|
||||
app:layout_constraintBottom_toTopOf="@id/envMetrics"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintVertical_bias="0"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
tools:composableName="com.geeksville.mesh.ui.SignalInfoKt.SignalInfoSimplePreview"
|
||||
/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/envMetrics"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
tools:visibility="visible"
|
||||
tools:background="@android:color/holo_red_light"
|
||||
/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
</LinearLayout>
|
||||
|
|
@ -11,6 +11,8 @@
|
|||
<string name="sample_coords" translatable="false">55.332244 34.442211</string>
|
||||
<string name="sample_message" translatable="false">hey I found the cache, it is over here next to the big tiger. I\'m kinda scared.</string>
|
||||
|
||||
<string name="unknown_node_short_name" translatable="false">\???</string>
|
||||
|
||||
<string name="channel_name">Channel Name</string>
|
||||
<string name="channel_options">Channel options</string>
|
||||
<string name="qr_code">QR code</string>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue