mirror of
https://github.com/atsunatsu/Look4Sat.git
synced 2026-10-02 03:15:37 +00:00
feat(map): keep grid-mode viewport across page switches
The osmdroid map was recreated on every return to the Map page, resetting center/zoom to the default and forcing grid mode back onto the station grid. Now the viewport (center + zoom) is saved in the Activity-scoped MapFilterViewModel only in grid mode and restored on re-entry, so the user keeps exactly where they left off. Satellite mode keeps its original follow-the-satellite behavior; cold-start entries and in-page grid toggles still center on the station grid as before.
This commit is contained in:
1 parent
b47bd78ecd
commit
4dbe896124
2 files changed
+64
-5
No files matched your search
@@ -19,6 +19,18 @@ class MapFilterViewModel : ViewModel() {
|
|||||||
/** Last chosen award chip; null means "All". Initialized to VUCC once per process. */
|
/** Last chosen award chip; null means "All". Initialized to VUCC once per process. */
|
||||||
val selectedAward: MutableState<AwardType?> = mutableStateOf(AwardType.VUCC)
|
val selectedAward: MutableState<AwardType?> = mutableStateOf(AwardType.VUCC)
|
||||||
|
|
||||||
|
/** Last map viewport (center + zoom), saved when leaving the page. Null until first visit. */
|
||||||
|
var mapCenterLat: Double? = null
|
||||||
|
var mapCenterLon: Double? = null
|
||||||
|
var mapZoom: Double? = null
|
||||||
|
|
||||||
|
/** Persists the current viewport so a later re-entry restores exactly where the user left off. */
|
||||||
|
fun saveMapViewState(centerLat: Double, centerLon: Double, zoom: Double) {
|
||||||
|
mapCenterLat = centerLat
|
||||||
|
mapCenterLon = centerLon
|
||||||
|
mapZoom = zoom
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun factory() = viewModelFactory {
|
fun factory() = viewModelFactory {
|
||||||
initializer { MapFilterViewModel() }
|
initializer { MapFilterViewModel() }
|
||||||
|
|||||||
@@ -151,8 +151,23 @@ fun MapDestination(
|
|||||||
val container = (context.applicationContext as IContainerProvider).getMainContainer()
|
val container = (context.applicationContext as IContainerProvider).getMainContainer()
|
||||||
val viewModel: MapViewModel = viewModel(factory = MapViewModel.factory(container))
|
val viewModel: MapViewModel = viewModel(factory = MapViewModel.factory(container))
|
||||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
val mapView = rememberMapViewWithLifecycle()
|
val mapView = rememberMapViewWithLifecycle(mapFilterViewModel, restoreViewport = uiState.isGridMode)
|
||||||
val lifecycle = LocalLifecycleOwner.current.lifecycle
|
val lifecycle = LocalLifecycleOwner.current.lifecycle
|
||||||
|
// Save the viewport when leaving the page, but only in grid mode: the
|
||||||
|
// satellite mode intentionally keeps its original behavior (default center,
|
||||||
|
// follow the selected satellite), so a satellite-mode exit must not
|
||||||
|
// overwrite the last grid-mode viewport.
|
||||||
|
DisposableEffect(mapView, uiState.isGridMode) {
|
||||||
|
onDispose {
|
||||||
|
if (uiState.isGridMode) {
|
||||||
|
mapFilterViewModel.saveMapViewState(
|
||||||
|
mapView.mapCenter.latitude,
|
||||||
|
mapView.mapCenter.longitude,
|
||||||
|
mapView.zoomLevelDouble
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
DisposableEffect(lifecycle) {
|
DisposableEffect(lifecycle) {
|
||||||
val observer = LifecycleEventObserver { _, event ->
|
val observer = LifecycleEventObserver { _, event ->
|
||||||
when (event) {
|
when (event) {
|
||||||
@@ -222,6 +237,14 @@ private fun MapScreen(
|
|||||||
else vuccByMyGrid[selectedMyGrid] ?: emptySet()
|
else vuccByMyGrid[selectedMyGrid] ?: emptySet()
|
||||||
}
|
}
|
||||||
val isGridMode = uiState.isGridMode
|
val isGridMode = uiState.isGridMode
|
||||||
|
// True when this composition restored a saved viewport. Only grid mode
|
||||||
|
// restores the viewport the user left (satellite mode keeps its original
|
||||||
|
// follow-the-satellite behavior); this implies grid mode was already on
|
||||||
|
// when re-entering, which is why the grid-mode auto-centering below can
|
||||||
|
// treat the entry as already centered.
|
||||||
|
val restoredViewport = remember {
|
||||||
|
isGridMode && mapFilterViewModel.mapCenterLat != null && mapFilterViewModel.mapZoom != null
|
||||||
|
}
|
||||||
DisposableEffect(isGridMode, workedGrids) {
|
DisposableEffect(isGridMode, workedGrids) {
|
||||||
val receiver = object : org.osmdroid.events.MapEventsReceiver {
|
val receiver = object : org.osmdroid.events.MapEventsReceiver {
|
||||||
override fun singleTapConfirmedHelper(p: GeoPoint?): Boolean {
|
override fun singleTapConfirmedHelper(p: GeoPoint?): Boolean {
|
||||||
@@ -282,9 +305,14 @@ private fun MapScreen(
|
|||||||
}
|
}
|
||||||
ElevatedCard(modifier = Modifier.weight(1f)) {
|
ElevatedCard(modifier = Modifier.weight(1f)) {
|
||||||
Box(contentAlignment = Alignment.BottomCenter) {
|
Box(contentAlignment = Alignment.BottomCenter) {
|
||||||
// Track grid-mode transitions; initial value false so that entering
|
// Track grid-mode transitions. When the map was just restored to
|
||||||
// the map page with grid mode already ON also centers the map.
|
// the viewport the user left (re-entry after a page switch) and
|
||||||
var prevGridMode by remember { mutableStateOf(false) }
|
// grid mode is already on, treat the entry as already centered so
|
||||||
|
// the restored position survives. Cold-start entries and in-page
|
||||||
|
// toggles still center on the station grid as before.
|
||||||
|
var prevGridMode by remember {
|
||||||
|
mutableStateOf(restoredViewport && uiState.isGridMode)
|
||||||
|
}
|
||||||
AndroidView({ mapView }) { view ->
|
AndroidView({ mapView }) { view ->
|
||||||
// Award filter mode: show the selected award's regions with
|
// Award filter mode: show the selected award's regions with
|
||||||
// worked ones filled green; hide satellite layers like grid mode.
|
// worked ones filled green; hide satellite layers like grid mode.
|
||||||
@@ -298,6 +326,10 @@ private fun MapScreen(
|
|||||||
prevGridMode = uiState.isGridMode
|
prevGridMode = uiState.isGridMode
|
||||||
} else {
|
} else {
|
||||||
// Center on the station grid whenever entering grid mode.
|
// Center on the station grid whenever entering grid mode.
|
||||||
|
// (prevGridMode is initialized to "already centered" when
|
||||||
|
// the map was just restored to the viewport the user left,
|
||||||
|
// so a re-entry keeps the restored position; in-page
|
||||||
|
// toggles still center normally.)
|
||||||
// Only mark the transition as consumed once a real position
|
// Only mark the transition as consumed once a real position
|
||||||
// was available; otherwise a first frame with a null
|
// was available; otherwise a first frame with a null
|
||||||
// stationPosition would swallow the centering forever.
|
// stationPosition would swallow the centering forever.
|
||||||
@@ -1174,7 +1206,10 @@ private fun setMoonPosition(moonLatDeg: Double, moonLonDeg: Double, mapView: Map
|
|||||||
|
|
||||||
// region MapView lifecycle
|
// region MapView lifecycle
|
||||||
@Composable
|
@Composable
|
||||||
private fun rememberMapViewWithLifecycle(): MapView {
|
private fun rememberMapViewWithLifecycle(
|
||||||
|
mapFilterViewModel: MapFilterViewModel,
|
||||||
|
restoreViewport: Boolean
|
||||||
|
): MapView {
|
||||||
val tileSource = XYTileSource("tiles", 0, 6, 256, ".webp", emptyArray<String>())
|
val tileSource = XYTileSource("tiles", 0, 6, 256, ".webp", emptyArray<String>())
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val isVertical = isVerticalLayout()
|
val isVertical = isVerticalLayout()
|
||||||
@@ -1190,8 +1225,19 @@ private fun rememberMapViewWithLifecycle(): MapView {
|
|||||||
setTileSource(tileSource)
|
setTileSource(tileSource)
|
||||||
minZoomLevel = getMinZoom(resources.displayMetrics.heightPixels, isVertical)
|
minZoomLevel = getMinZoom(resources.displayMetrics.heightPixels, isVertical)
|
||||||
maxZoomLevel = 7.0
|
maxZoomLevel = 7.0
|
||||||
|
// Restore the viewport the user left on the previous visit, but only
|
||||||
|
// in grid mode: satellite mode intentionally keeps its original
|
||||||
|
// behavior (default center, then follows the selected satellite).
|
||||||
|
val savedLat = mapFilterViewModel.mapCenterLat
|
||||||
|
val savedLon = mapFilterViewModel.mapCenterLon
|
||||||
|
val savedZoom = mapFilterViewModel.mapZoom
|
||||||
|
if (restoreViewport && savedLat != null && savedLon != null && savedZoom != null) {
|
||||||
|
controller.setCenter(GeoPoint(savedLat, savedLon))
|
||||||
|
controller.setZoom(savedZoom)
|
||||||
|
} else {
|
||||||
controller.setCenter(GeoPoint(48.8575, 6.3514))
|
controller.setCenter(GeoPoint(48.8575, 6.3514))
|
||||||
controller.setZoom(minZoomLevel + 2)
|
controller.setZoom(minZoomLevel + 2)
|
||||||
|
}
|
||||||
zoomController.setVisibility(CustomZoomButtonsController.Visibility.NEVER)
|
zoomController.setVisibility(CustomZoomButtonsController.Visibility.NEVER)
|
||||||
overlayManager.tilesOverlay.loadingBackgroundColor = Color.TRANSPARENT
|
overlayManager.tilesOverlay.loadingBackgroundColor = Color.TRANSPARENT
|
||||||
overlayManager.tilesOverlay.loadingLineColor = Color.TRANSPARENT
|
overlayManager.tilesOverlay.loadingLineColor = Color.TRANSPARENT
|
||||||
@@ -1209,6 +1255,7 @@ private fun rememberMapViewWithLifecycle(): MapView {
|
|||||||
}
|
}
|
||||||
// The overlay caches below are file-level (shared across MapView instances), so they must be
|
// The overlay caches below are file-level (shared across MapView instances), so they must be
|
||||||
// released with the MapView or they keep the Activity and its bitmaps alive after disposal.
|
// released with the MapView or they keep the Activity and its bitmaps alive after disposal.
|
||||||
|
// (The viewport save lives in MapDestination, where the grid/satellite mode is known.)
|
||||||
DisposableEffect(mapView) {
|
DisposableEffect(mapView) {
|
||||||
onDispose { clearMapCaches() }
|
onDispose { clearMapCaches() }
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user