Align map sensor selector with legacy dropdown UI.
Restore the old map sensor control styling and interaction in Compose, and add previews for collapsed/expanded, search, and device panel states to speed up UI iteration. Made-with: Cursor
This commit is contained in:
@@ -23,6 +23,7 @@ import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import org.db3.airmq.R
|
||||
import org.db3.airmq.features.map.MapScreenContract.Action
|
||||
import org.db3.airmq.features.map.MapScreenContract.Event
|
||||
import org.db3.airmq.sdk.map.domain.MapItem
|
||||
@@ -66,6 +67,13 @@ fun MapScreen(
|
||||
MapTopControls(
|
||||
selectedSensor = uiState.selectedTopSensor,
|
||||
onSensorSelected = { viewModel.onEvent(Event.TopSensorSelected(it)) },
|
||||
onHelpClick = {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.map_sensor_help_title),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
},
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopEnd)
|
||||
.padding(top = 20.dp, end = 16.dp)
|
||||
|
||||
@@ -7,26 +7,38 @@ import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.width
|
||||
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.size
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.db3.airmq.R
|
||||
import org.db3.airmq.features.common.AirMqButton
|
||||
@@ -36,36 +48,166 @@ import org.db3.airmq.features.map.MapScreenContract.DeviceSensorType
|
||||
import org.db3.airmq.features.map.MapScreenContract.SearchResult
|
||||
import org.db3.airmq.features.map.MapScreenContract.SensorType
|
||||
import org.db3.airmq.features.map.MapScreenContract.TimeRange
|
||||
import org.db3.airmq.ui.theme.AirMQTheme
|
||||
|
||||
@Composable
|
||||
fun MapTopControls(
|
||||
selectedSensor: SensorType,
|
||||
onSensorSelected: (SensorType) -> Unit,
|
||||
onHelpClick: () -> Unit,
|
||||
initiallyExpanded: Boolean = false,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
var isExpanded by remember(initiallyExpanded) { mutableStateOf(initiallyExpanded) }
|
||||
val arrowRotation = if (isExpanded) 180f else 0f
|
||||
val selectedLabel = when (selectedSensor) {
|
||||
SensorType.DUST -> stringResource(id = R.string.map_sensor_air_quality)
|
||||
SensorType.RADIOACTIVITY -> stringResource(id = R.string.map_sensor_radioactivity)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.width(234.dp)
|
||||
.wrapContentHeight()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopStart)
|
||||
.padding(start = 10.dp, top = 10.dp, end = 10.dp)
|
||||
) {
|
||||
Surface(
|
||||
modifier = modifier,
|
||||
shape = RoundedCornerShape(22.dp),
|
||||
tonalElevation = 2.dp,
|
||||
shadowElevation = 6.dp,
|
||||
modifier = Modifier
|
||||
.width(214.dp)
|
||||
.height(36.dp)
|
||||
.clickable { isExpanded = !isExpanded },
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
shadowElevation = 10.dp,
|
||||
color = Color.White
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 10.dp, vertical = 8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(start = 12.dp, end = 48.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
FilterChip(
|
||||
selected = selectedSensor == SensorType.DUST,
|
||||
onClick = { onSensorSelected(SensorType.DUST) },
|
||||
label = { Text(stringResource(id = R.string.map_sensor_dust)) }
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_arrow_down_dark),
|
||||
contentDescription = null,
|
||||
tint = Color(0x8A000000),
|
||||
modifier = Modifier.rotate(arrowRotation)
|
||||
)
|
||||
FilterChip(
|
||||
selected = selectedSensor == SensorType.RADIOACTIVITY,
|
||||
onClick = { onSensorSelected(SensorType.RADIOACTIVITY) },
|
||||
label = { Text(stringResource(id = R.string.map_sensor_radioactivity)) }
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = selectedLabel.uppercase(),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = Color(0x61000000)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (isExpanded) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.width(214.dp)
|
||||
.padding(top = 8.dp),
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
shadowElevation = 10.dp,
|
||||
color = Color.White
|
||||
) {
|
||||
Column(modifier = Modifier.padding(top = 8.dp, bottom = 10.dp)) {
|
||||
SensorTypeItem(
|
||||
iconRes = R.drawable.ic_dust,
|
||||
titleRes = R.string.map_sensor_air_quality,
|
||||
selected = selectedSensor == SensorType.DUST,
|
||||
onClick = {
|
||||
onSensorSelected(SensorType.DUST)
|
||||
isExpanded = false
|
||||
}
|
||||
)
|
||||
HorizontalDivider(color = Color(0x1F000000))
|
||||
SensorTypeItem(
|
||||
iconRes = R.drawable.ic_radiation,
|
||||
titleRes = R.string.map_sensor_radioactivity,
|
||||
selected = selectedSensor == SensorType.RADIOACTIVITY,
|
||||
onClick = {
|
||||
onSensorSelected(SensorType.RADIOACTIVITY)
|
||||
isExpanded = false
|
||||
}
|
||||
)
|
||||
Text(
|
||||
text = stringResource(id = R.string.map_sensor_help_link),
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.padding(top = 12.dp)
|
||||
.clickable {
|
||||
onHelpClick()
|
||||
isExpanded = false
|
||||
},
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = Color(0x8A000000),
|
||||
textDecoration = TextDecoration.Underline
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopEnd)
|
||||
.size(56.dp)
|
||||
.background(
|
||||
brush = Brush.linearGradient(
|
||||
colors = listOf(Color(0xFF00FF6D), Color(0xFF03AEF9))
|
||||
),
|
||||
shape = CircleShape
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(
|
||||
id = if (selectedSensor == SensorType.RADIOACTIVITY) {
|
||||
R.drawable.ic_radiation
|
||||
} else {
|
||||
R.drawable.ic_dust
|
||||
}
|
||||
),
|
||||
contentDescription = null,
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SensorTypeItem(
|
||||
iconRes: Int,
|
||||
titleRes: Int,
|
||||
selected: Boolean,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val textColor = if (selected) Color(0xFF1C1C1C) else Color(0xFF616161)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onClick)
|
||||
.padding(horizontal = 16.dp, vertical = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = iconRes),
|
||||
contentDescription = null,
|
||||
tint = textColor,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
Text(
|
||||
text = stringResource(id = titleRes),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = textColor
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -340,3 +482,118 @@ private fun DeviceSensorRow(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun PreviewMapTopControlsCollapsedDust() {
|
||||
AirMQTheme {
|
||||
MapTopControls(
|
||||
selectedSensor = SensorType.DUST,
|
||||
onSensorSelected = {},
|
||||
onHelpClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun PreviewMapTopControlsExpandedDust() {
|
||||
AirMQTheme {
|
||||
MapTopControls(
|
||||
selectedSensor = SensorType.DUST,
|
||||
onSensorSelected = {},
|
||||
onHelpClick = {},
|
||||
initiallyExpanded = true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun PreviewMapTopControlsExpandedRadioactivity() {
|
||||
AirMQTheme {
|
||||
MapTopControls(
|
||||
selectedSensor = SensorType.RADIOACTIVITY,
|
||||
onSensorSelected = {},
|
||||
onHelpClick = {},
|
||||
initiallyExpanded = true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun PreviewMapSearchOverlayEmpty() {
|
||||
AirMQTheme {
|
||||
MapSearchOverlay(
|
||||
query = "Minsk",
|
||||
results = emptyList(),
|
||||
onQueryChanged = {},
|
||||
onClose = {},
|
||||
onResultClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun PreviewMapSearchOverlayWithResults() {
|
||||
AirMQTheme {
|
||||
MapSearchOverlay(
|
||||
query = "Minsk",
|
||||
results = listOf(
|
||||
SearchResult(id = "1", title = "AirMQ #42", subtitle = "Minsk"),
|
||||
SearchResult(id = "2", title = "AirMQ #91", subtitle = "Salihorsk")
|
||||
),
|
||||
onQueryChanged = {},
|
||||
onClose = {},
|
||||
onResultClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun PreviewMapDevicePanelDust() {
|
||||
AirMQTheme {
|
||||
MapDevicePanel(
|
||||
data = DevicePanelState(
|
||||
id = "42",
|
||||
name = "AirMQ #42",
|
||||
status = "Online",
|
||||
selectedRange = TimeRange.DAY,
|
||||
displayedDateRange = "Today",
|
||||
selectedSensor = DeviceSensorType.DUST
|
||||
),
|
||||
onClose = {},
|
||||
onOpenDevice = {},
|
||||
onRangeSelected = {},
|
||||
onDateBack = {},
|
||||
onDateForward = {},
|
||||
onSensorSelected = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun PreviewMapDevicePanelRadioactivity() {
|
||||
AirMQTheme {
|
||||
MapDevicePanel(
|
||||
data = DevicePanelState(
|
||||
id = "43",
|
||||
name = "AirMQ #43",
|
||||
status = "Offline",
|
||||
selectedRange = TimeRange.WEEK,
|
||||
displayedDateRange = "Last 7 days",
|
||||
selectedSensor = DeviceSensorType.RADIOACTIVITY
|
||||
),
|
||||
onClose = {},
|
||||
onOpenDevice = {},
|
||||
onRangeSelected = {},
|
||||
onDateBack = {},
|
||||
onDateForward = {},
|
||||
onSensorSelected = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
15
app/src/main/res/drawable/ic_arrow_down_dark.xml
Normal file
15
app/src/main/res/drawable/ic_arrow_down_dark.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillAlpha="0"
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M0,0h24v24h-24z" />
|
||||
<path
|
||||
android:fillAlpha="0.54"
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M8.54,11.38l2.72,2.72c0.41,0.41 1.07,0.41 1.48,0l2.72,-2.72c0.66,-0.66 0.19,-1.79 -0.74,-1.79l-5.44,0C8.34,9.59 7.88,10.72 8.54,11.38z"
|
||||
android:strokeAlpha="0.54" />
|
||||
</vector>
|
||||
42
app/src/main/res/drawable/ic_dust.xml
Normal file
42
app/src/main/res/drawable/ic_dust.xml
Normal file
@@ -0,0 +1,42 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M15.62,11.36m-2.57,0a2.57,2.57 0,1 1,5.14 0a2.57,2.57 0,1 1,-5.14 0" />
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M10.16,17.59m-2.3,0a2.3,2.3 0,1 1,4.6 0a2.3,2.3 0,1 1,-4.6 0" />
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M6.09,7.24m-2.31,0a2.31,2.31 0,1 1,4.62 0a2.31,2.31 0,1 1,-4.62 0" />
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M21.35,10.63m-1.65,0a1.65,1.65 0,1 1,3.3 0a1.65,1.65 0,1 1,-3.3 0" />
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M5.76,13.19m-1.83,0a1.83,1.83 0,1 1,3.66 0a1.83,1.83 0,1 1,-3.66 0" />
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M11.78,6.51m-1.83,0a1.83,1.83 0,1 1,3.66 0a1.83,1.83 0,1 1,-3.66 0" />
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M4.08,18.66m-1.47,0a1.47,1.47 0,1 1,2.94 0a1.47,1.47 0,1 1,-2.94 0" />
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M22.08,16.1m-0.72,0a0.72,0.72 0,1 1,1.44 0a0.72,0.72 0,1 1,-1.44 0" />
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M16.42,4.67m-0.8,0a0.8,0.8 0,1 1,1.6 0a0.8,0.8 0,1 1,-1.6 0" />
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M16.42,16.59m-1.09,0a1.09,1.09 0,1 1,2.18 0a1.09,1.09 0,1 1,-2.18 0" />
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M1.72,10.63m-0.72,0a0.72,0.72 0,1 1,1.44 0a0.72,0.72 0,1 1,-1.44 0" />
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M10.81,12.08m-0.9,0a0.9,0.9 0,1 1,1.8 0a0.9,0.9 0,1 1,-1.8 0" />
|
||||
</vector>
|
||||
18
app/src/main/res/drawable/ic_radiation.xml
Normal file
18
app/src/main/res/drawable/ic_radiation.xml
Normal file
@@ -0,0 +1,18 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M13.79,11.19c0,0.66 -0.36,1.23 -0.9,1.53c-0.25,0.17 -0.56,0.25 -0.88,0.25s-0.63,-0.08 -0.89,-0.25c-0.53,-0.3 -0.9,-0.87 -0.9,-1.53s0.37,-1.25 0.9,-1.54c0.26,-0.17 0.57,-0.25 0.89,-0.25s0.63,0.08 0.88,0.25C13.43,9.95 13.79,10.53 13.79,11.19z" />
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M10.19,8.06c-1.07,0.63 -1.8,1.8 -1.8,3.13H2.7c-0.4,0 -0.73,-0.34 -0.7,-0.74C2.23,7.3 3.9,4.57 6.36,2.9c0.33,-0.23 0.79,-0.1 0.99,0.24L10.19,8.06z" />
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M22,10.45c0.03,0.4 -0.3,0.74 -0.7,0.74h-5.69c0,-1.33 -0.73,-2.52 -1.8,-3.13l2.84,-4.92c0.2,-0.34 0.66,-0.47 0.99,-0.24C20.1,4.57 21.77,7.3 22,10.45z" />
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M16.37,20.21c-1.32,0.64 -2.8,1 -4.36,1c-1.57,0 -3.06,-0.36 -4.37,-1c-0.37,-0.17 -0.49,-0.63 -0.29,-0.99l2.84,-4.92c0.53,0.31 1.16,0.49 1.82,0.49s1.27,-0.17 1.8,-0.49l2.84,4.92C16.85,19.58 16.74,20.04 16.37,20.21z" />
|
||||
</vector>
|
||||
@@ -1,7 +1,10 @@
|
||||
<resources>
|
||||
<string name="app_name">AirMQ</string>
|
||||
<string name="map_sensor_dust">Dust</string>
|
||||
<string name="map_sensor_air_quality">Air quality</string>
|
||||
<string name="map_sensor_radioactivity">Radioactivity</string>
|
||||
<string name="map_sensor_help_link">What does this mean?</string>
|
||||
<string name="map_sensor_help_title">What do AirMQ sensors measure?</string>
|
||||
<string name="map_search_action_content_desc">Search</string>
|
||||
<string name="map_my_location_content_desc">My location</string>
|
||||
<string name="map_back">Back</string>
|
||||
|
||||
Reference in New Issue
Block a user