From c2eb2df8c0bf24a3da4f19b7f4e3d3243141a72b Mon Sep 17 00:00:00 2001 From: beetzung Date: Thu, 5 Mar 2026 21:36:02 +0100 Subject: [PATCH] feat(map): dark status bar on map screen, white on dashboard/manage; faster map animations Made-with: Cursor --- .../org/db3/airmq/features/map/MapScreen.kt | 77 +++++++++++++++---- .../features/navigation/AirMQNavGraph.kt | 4 +- 2 files changed, 65 insertions(+), 16 deletions(-) diff --git a/app/src/main/kotlin/org/db3/airmq/features/map/MapScreen.kt b/app/src/main/kotlin/org/db3/airmq/features/map/MapScreen.kt index 76be2e4..51bec86 100644 --- a/app/src/main/kotlin/org/db3/airmq/features/map/MapScreen.kt +++ b/app/src/main/kotlin/org/db3/airmq/features/map/MapScreen.kt @@ -49,6 +49,9 @@ import org.osmdroid.util.GeoPoint import org.osmdroid.views.MapView import org.osmdroid.views.overlay.Marker import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.rememberModalBottomSheetState import androidx.core.graphics.createBitmap import androidx.core.graphics.drawable.toDrawable @@ -83,6 +86,7 @@ fun MapScreen( ) } +@OptIn(ExperimentalMaterial3Api::class) @Composable private fun MapScreenContent( uiState: State, @@ -90,12 +94,19 @@ private fun MapScreenContent( showMap: Boolean ) { val context = LocalContext.current + val centerOnMarker = uiState.selectedMarkerId?.let { id -> + uiState.items.find { it.id == id } + } + val sheetHeightFraction = if (uiState.devicePanelState != null) 0.5f else 0f Box(modifier = Modifier.fillMaxSize()) { if (showMap) { AirMQMap( items = uiState.items, - onMarkerClick = { onEvent(Event.MarkerClicked(it)) } + onMarkerClick = { onEvent(Event.MarkerClicked(it)) }, + centerOnMarker = centerOnMarker, + sheetHeightFraction = sheetHeightFraction, + modifier = Modifier.fillMaxSize() ) } else { Box( @@ -143,16 +154,20 @@ private fun MapScreenContent( } uiState.devicePanelState?.let { panelData -> - MapDevicePanel( - data = panelData, - onClose = { onEvent(Event.DevicePanelClosed) }, - onOpenDevice = { onEvent(Event.DeviceOpenClicked) }, - onRangeSelected = { onEvent(Event.TimeRangeSelected(it)) }, - onDateBack = { onEvent(Event.DateBackClicked) }, - onDateForward = { onEvent(Event.DateForwardClicked) }, - onSensorSelected = { onEvent(Event.DeviceSensorSelected(it)) }, - modifier = Modifier.align(Alignment.BottomCenter) - ) + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + ModalBottomSheet( + onDismissRequest = { onEvent(Event.DevicePanelClosed) }, + sheetState = sheetState + ) { + MapDevicePanelContent( + data = panelData, + onOpenDevice = { onEvent(Event.DeviceOpenClicked) }, + onRangeSelected = { onEvent(Event.TimeRangeSelected(it)) }, + onDateBack = { onEvent(Event.DateBackClicked) }, + onDateForward = { onEvent(Event.DateForwardClicked) }, + onSensorSelected = { onEvent(Event.DeviceSensorSelected(it)) } + ) + } } if (uiState.isLoading) { @@ -172,7 +187,10 @@ private fun MapScreenContent( @Composable private fun AirMQMap( items: List, - onMarkerClick: (String) -> Unit + onMarkerClick: (String) -> Unit, + centerOnMarker: MapMarker? = null, + sheetHeightFraction: Float = 0f, + modifier: Modifier = Modifier ) { val context = LocalContext.current val lifecycleOwner = LocalLifecycleOwner.current @@ -202,8 +220,31 @@ private fun AirMQMap( } } + val mapAnimationSpeedMs = 200L + LaunchedEffect(centerOnMarker, sheetHeightFraction) { + centerOnMarker?.let { marker -> + val markerGeo = GeoPoint(marker.latitude, marker.longitude) + val zoomLevel = 15.5 + if (sheetHeightFraction > 0f) { + mapView.post { + val height = mapView.height + val width = mapView.width + if (height > 0 && width > 0) { + val sheetHeightPx = (height * sheetHeightFraction).toInt() + val offsetCenterY = height / 2 + sheetHeightPx / 2 + val projection = mapView.projection + val offsetGeo = projection.fromPixels(width / 2, offsetCenterY) + mapView.controller.animateTo(offsetGeo, zoomLevel, mapAnimationSpeedMs) + } + } + } else { + mapView.controller.animateTo(markerGeo, zoomLevel, mapAnimationSpeedMs) + } + } + } + AndroidView( - modifier = Modifier.fillMaxSize(), + modifier = modifier.fillMaxSize(), factory = { mapView }, update = { map -> map.overlays.removeAll { it is Marker } @@ -226,8 +267,14 @@ private fun AirMQMap( map.overlays.add(marker) } - items.firstOrNull()?.let { first -> - map.controller.animateTo(GeoPoint(first.latitude, first.longitude)) + if (centerOnMarker == null) { + items.firstOrNull()?.let { first -> + map.controller.animateTo( + GeoPoint(first.latitude, first.longitude), + map.zoomLevelDouble, + 200L + ) + } } map.invalidate() } diff --git a/app/src/main/kotlin/org/db3/airmq/features/navigation/AirMQNavGraph.kt b/app/src/main/kotlin/org/db3/airmq/features/navigation/AirMQNavGraph.kt index 23d67f7..8be52d1 100644 --- a/app/src/main/kotlin/org/db3/airmq/features/navigation/AirMQNavGraph.kt +++ b/app/src/main/kotlin/org/db3/airmq/features/navigation/AirMQNavGraph.kt @@ -71,7 +71,9 @@ fun AirMQNavGraph(modifier: Modifier = Modifier) { val view = LocalView.current SideEffect { val window = (view.context as? Activity)?.window ?: return@SideEffect - WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !showBottomBar + // Map: dark icons (light map background); Dashboard/Manage: white icons (dark nav bar) + val lightStatusBars = currentRoute == AirMqRoutes.MAP || !showBottomBar + WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = lightStatusBars } Scaffold(