fix(android): latch NowPlaying drag-dismiss to prevent back-stack underflow
android / Build + lint + test (push) Failing after 1m24s
android / Build + lint + test (push) Failing after 1m24s
Operator reproduced black-screen-on-resume by drag-down dismissing the full player. Root cause: the NestedScrollConnection accumulator crossed the dismiss threshold, called navController.popBackStack(), reset accumulated to 0 — but the user's finger was still down and the pop transition was still running. The next frame's onPostScroll re-accumulated and re-fired onDismiss(), popping the screen BENEATH NowPlaying. When that left the back stack empty the NavHost had no destination to draw, producing a black window until the process was killed and the activity was cold-launched. Add a `dismissed` latch that survives until the connection is disposed (which only happens when NowPlayingScreen leaves the composition, i.e. the pop completes). After the latch sets we consume the remaining drag (return `available`) so the underlying scrollable doesn't paint over-scroll while the pop transitions. onPreFling also bails after dismissal so the fling can't restart the accumulator.
This commit is contained in:
@@ -189,16 +189,29 @@ private fun rememberDragDismissConnection(
|
|||||||
object : NestedScrollConnection {
|
object : NestedScrollConnection {
|
||||||
private var accumulated = 0f
|
private var accumulated = 0f
|
||||||
|
|
||||||
|
// One-shot latch. popBackStack is async — between the first
|
||||||
|
// dismissal and the screen actually leaving the composition,
|
||||||
|
// the user's finger is still down and more onPostScroll
|
||||||
|
// frames arrive. Without this guard the accumulator rebuilds
|
||||||
|
// and onDismiss() fires a second time, popping the screen
|
||||||
|
// BENEATH NowPlaying. If that leaves the back stack empty
|
||||||
|
// the NavHost renders nothing → black screen on resume.
|
||||||
|
private var dismissed = false
|
||||||
|
|
||||||
override fun onPostScroll(
|
override fun onPostScroll(
|
||||||
consumed: Offset,
|
consumed: Offset,
|
||||||
available: Offset,
|
available: Offset,
|
||||||
source: NestedScrollSource,
|
source: NestedScrollSource,
|
||||||
): Offset {
|
): Offset {
|
||||||
|
// After dismissal, eat all remaining drag so the
|
||||||
|
// scrollable doesn't paint over-scroll deltas during the
|
||||||
|
// pop transition.
|
||||||
|
if (dismissed) return available
|
||||||
if (source != NestedScrollSource.UserInput) return Offset.Zero
|
if (source != NestedScrollSource.UserInput) return Offset.Zero
|
||||||
return if (available.y > 0f) {
|
return if (available.y > 0f) {
|
||||||
accumulated += available.y
|
accumulated += available.y
|
||||||
if (accumulated >= thresholdPx) {
|
if (accumulated >= thresholdPx) {
|
||||||
accumulated = 0f
|
dismissed = true
|
||||||
onDismiss()
|
onDismiss()
|
||||||
}
|
}
|
||||||
Offset(0f, available.y)
|
Offset(0f, available.y)
|
||||||
@@ -209,6 +222,7 @@ private fun rememberDragDismissConnection(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun onPreFling(available: Velocity): Velocity {
|
override suspend fun onPreFling(available: Velocity): Velocity {
|
||||||
|
if (dismissed) return available
|
||||||
accumulated = 0f
|
accumulated = 0f
|
||||||
return Velocity.Zero
|
return Velocity.Zero
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user