mirror of
https://github.com/atsunatsu/Look4Sat.git
synced 2026-10-02 03:15:37 +00:00
fix(mutual): keep scroll position when re-querying from the match page
queryMutualPasses() reset listScrollIndex to 0 on every query, so tapping "query" on the match page jumped the list back to the top. Now the default keeps the current position; the stale results stay on screen while the new query runs (clearing them shrank the list below a screen and clamped the position away) and are replaced when the new results arrive. New VM test covers position + stale-results behaviour through a settled re-query.
This commit is contained in:
1 parent
6962a4bfa1
commit
d6bdf5ecb6
3 files changed
+75
-10
No files matched your search
@@ -161,7 +161,7 @@ fun MutualScreen(
|
|||||||
viewModel = viewModel,
|
viewModel = viewModel,
|
||||||
state = state,
|
state = state,
|
||||||
isVertical = isVertical,
|
isVertical = isVertical,
|
||||||
onQuery = viewModel::queryMutualPasses,
|
onQuery = { viewModel.queryMutualPasses() },
|
||||||
onSelectPass = viewModel::onSelectPass,
|
onSelectPass = viewModel::onSelectPass,
|
||||||
onNavigateToRadar = navigateToRadar,
|
onNavigateToRadar = navigateToRadar,
|
||||||
onStationALat = viewModel::onStationALat,
|
onStationALat = viewModel::onStationALat,
|
||||||
|
|||||||
+39
-9
@@ -61,7 +61,12 @@ data class MutualUiState(
|
|||||||
// One-shot flag set by prefillMatchFromGrid() (map grid → Match button):
|
// One-shot flag set by prefillMatchFromGrid() (map grid → Match button):
|
||||||
// the page scrolls to the time-range card after the first layout, then
|
// the page scrolls to the time-range card after the first layout, then
|
||||||
// consumeScrollToTimeRange() clears it.
|
// consumeScrollToTimeRange() clears it.
|
||||||
val scrollToTimeRange: Boolean = false
|
val scrollToTimeRange: Boolean = false,
|
||||||
|
// One-shot flag set by prefillMatchFromGrid(): the map page waits for the
|
||||||
|
// pre-filled query to finish, then navigates to the match page so the very
|
||||||
|
// first frame already contains the results (time-range card + pass curves,
|
||||||
|
// no scroll flicker). consumePendingNavigation() clears it.
|
||||||
|
val pendingNavigation: Boolean = false
|
||||||
)
|
)
|
||||||
|
|
||||||
class MutualViewModel(
|
class MutualViewModel(
|
||||||
@@ -206,18 +211,33 @@ class MutualViewModel(
|
|||||||
stationBLat = pos?.let { p -> "%.4f".format(p.latitude) } ?: it.stationBLat,
|
stationBLat = pos?.let { p -> "%.4f".format(p.latitude) } ?: it.stationBLat,
|
||||||
stationBLon = pos?.let { p -> "%.4f".format(p.longitude) } ?: it.stationBLon,
|
stationBLon = pos?.let { p -> "%.4f".format(p.longitude) } ?: it.stationBLon,
|
||||||
hoursAhead = 24,
|
hoursAhead = 24,
|
||||||
scrollToTimeRange = true
|
scrollToTimeRange = true,
|
||||||
|
pendingNavigation = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
queryMutualPasses()
|
queryMutualPasses()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun consumePendingNavigation() {
|
||||||
|
android.util.Log.d(TAG, "consumePendingNavigation")
|
||||||
|
_uiState.update { it.copy(pendingNavigation = false) }
|
||||||
|
}
|
||||||
|
|
||||||
fun consumeScrollToTimeRange() {
|
fun consumeScrollToTimeRange() {
|
||||||
android.util.Log.d(TAG, "consumeScrollToTimeRange")
|
android.util.Log.d(TAG, "consumeScrollToTimeRange")
|
||||||
_uiState.update { it.copy(scrollToTimeRange = false) }
|
_uiState.update { it.copy(scrollToTimeRange = false) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun queryMutualPasses(initialScrollIndex: Int = 0) {
|
/**
|
||||||
|
* Run a mutual-pass query. `initialScrollIndex` non-null forces the list
|
||||||
|
* to rebuild at that position (legacy callers); null keeps the current
|
||||||
|
* scroll position, so re-running a query from the match page does not
|
||||||
|
* jump back to the top. The previous results stay on screen while the
|
||||||
|
* query runs (the list keeps its height, so the scroll position is not
|
||||||
|
* clamped away); they are replaced — possibly with empty — when the new
|
||||||
|
* results arrive.
|
||||||
|
*/
|
||||||
|
fun queryMutualPasses(initialScrollIndex: Int? = null) {
|
||||||
val state = _uiState.value
|
val state = _uiState.value
|
||||||
|
|
||||||
// Resolve positions from lat/lon or grid
|
// Resolve positions from lat/lon or grid
|
||||||
@@ -240,16 +260,19 @@ class MutualViewModel(
|
|||||||
isCalculating = true,
|
isCalculating = true,
|
||||||
hasSearched = true,
|
hasSearched = true,
|
||||||
errorMessage = null,
|
errorMessage = null,
|
||||||
mutualPasses = emptyList(),
|
// Deliberately keep mutualPasses: clearing it would shrink the
|
||||||
|
// list below one screen and clamp the scroll position away.
|
||||||
|
// The stale results are replaced when the new ones arrive.
|
||||||
selectedPassIndex = -1
|
selectedPassIndex = -1
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// The results list is about to be replaced, so the scroll position must
|
// Scroll position: an explicit initialScrollIndex overrides (legacy
|
||||||
// not leak from the previous query's list. A prefill from the map
|
// callers start at a fixed spot); null keeps the current position so
|
||||||
// (grid-QSO dialog "Match" button) instead starts at the time-range
|
// re-querying does not jump back to the top.
|
||||||
// card so the page opens with it at the top.
|
if (initialScrollIndex != null) {
|
||||||
listScrollIndex = initialScrollIndex
|
listScrollIndex = initialScrollIndex
|
||||||
listScrollOffset = 0
|
listScrollOffset = 0
|
||||||
|
}
|
||||||
queryGeneration += 1
|
queryGeneration += 1
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
@@ -258,9 +281,16 @@ class MutualViewModel(
|
|||||||
val minElevB = state.stationBMinElev
|
val minElevB = state.stationBMinElev
|
||||||
val hours = state.hoursAhead
|
val hours = state.hoursAhead
|
||||||
|
|
||||||
val results = withContext(computeDispatcher) {
|
val results = try {
|
||||||
|
withContext(computeDispatcher) {
|
||||||
findMutualPasses(satellites, posA, posB, minElevA, minElevB, time, hours)
|
findMutualPasses(satellites, posA, posB, minElevA, minElevB, time, hours)
|
||||||
}
|
}
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
// Never leave the query stuck in "calculating" (which would also
|
||||||
|
// block the pending map→match navigation forever).
|
||||||
|
android.util.Log.e(TAG, "findMutualPasses threw", t)
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
val errorMsg = if (results.isEmpty()) {
|
val errorMsg = if (results.isEmpty()) {
|
||||||
// Debug: check if the main pass list is the culprit
|
// Debug: check if the main pass list is the culprit
|
||||||
|
|||||||
+35
@@ -285,4 +285,39 @@ class MutualViewModelTest {
|
|||||||
|
|
||||||
assertNull(vm.uiState.value.errorMessage)
|
assertNull(vm.uiState.value.errorMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `requery keeps scroll position and stale results while running`() = runTest(mainDispatcherRule.dispatcher.scheduler) {
|
||||||
|
val windows = TestOrbits.findPassWindows()
|
||||||
|
assertTrue("fixture must produce ISS windows", windows.isNotEmpty())
|
||||||
|
val vm = createVm(
|
||||||
|
satellites = listOf(TestOrbits.ISS),
|
||||||
|
passes = windows
|
||||||
|
)
|
||||||
|
vm.onStationBGrid("OL62")
|
||||||
|
queryAndSettle(vm)
|
||||||
|
assertTrue("fixture must yield passes", vm.uiState.value.mutualPasses.isNotEmpty())
|
||||||
|
|
||||||
|
// The user scrolled into the results; the snapshotFlow write-back has
|
||||||
|
// stored this position in the VM.
|
||||||
|
vm.listScrollIndex = 4
|
||||||
|
vm.listScrollOffset = 137
|
||||||
|
|
||||||
|
// Re-run the query from the match page ("query" button).
|
||||||
|
vm.queryMutualPasses()
|
||||||
|
|
||||||
|
// While the query runs, the old results stay on screen (the list keeps
|
||||||
|
// its height, so the scroll position is not clamped away)...
|
||||||
|
assertTrue("stale results kept while calculating", vm.uiState.value.mutualPasses.isNotEmpty())
|
||||||
|
// ...and the scroll position must not be reset to the top.
|
||||||
|
assertEquals(4, vm.listScrollIndex)
|
||||||
|
assertEquals(137, vm.listScrollOffset)
|
||||||
|
|
||||||
|
advanceUntilIdle()
|
||||||
|
assertFalse(vm.uiState.value.isCalculating)
|
||||||
|
assertTrue("fresh results replace the stale ones", vm.uiState.value.mutualPasses.isNotEmpty())
|
||||||
|
// Position intact after the query settled.
|
||||||
|
assertEquals(4, vm.listScrollIndex)
|
||||||
|
assertEquals(137, vm.listScrollOffset)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user