fix(android): NowPlaying drag-down dismiss via NestedScroll (#2)
android / Build + lint + test (push) Failing after 1m25s
android / Build + lint + test (push) Failing after 1m25s
The previous detectVerticalDragGestures modifier on the Scaffold never fired because the body Column applies verticalScroll, which wins the touch-slop competition for every vertical drag delta before the outer detector sees it. User-visible symptom: swiping down on the NowPlaying screen did nothing. Replace with a NestedScrollConnection. verticalScroll offers its over-scroll deltas to the nearest ancestor connection via the standard nested-scroll protocol; when the column is at the top of its scroll range, downward drag deltas surface in onPostScroll unconsumed (available.y > 0). Accumulate those toward a 200 px threshold and pop the back stack. - Upward over-scroll or any consumed delta resets the accumulator so a partial drag-down followed by drag-up doesn't latch. - onPreFling resets on lift-off so a lazy swipe doesn't dismiss later after the user has released. - Slider thumb retains its own pointerInput and is not affected. - Source filter (UserInput) ignores nested-scroll-driven animations (e.g. flings from inner scrollables).
This commit is contained in:
@@ -22,7 +22,11 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.foundation.gestures.detectVerticalDragGestures
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.unit.Velocity
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.TopAppBar
|
||||
@@ -30,7 +34,6 @@ import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.material3.SliderDefaults
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
@@ -122,8 +125,11 @@ fun NowPlayingScreen(
|
||||
return
|
||||
}
|
||||
val dominant = rememberDominantColor(track.coverUrl)
|
||||
val dismissConnection = rememberDragDismissConnection(
|
||||
onDismiss = { navController.popBackStack() },
|
||||
)
|
||||
Scaffold(
|
||||
modifier = Modifier.fillMaxSize().nowPlayingDragDismiss(navController),
|
||||
modifier = Modifier.fillMaxSize().nestedScroll(dismissConnection),
|
||||
topBar = { NowPlayingTopBar(onClose = { navController.popBackStack() }) },
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||
containerColor = Color.Transparent,
|
||||
@@ -161,27 +167,50 @@ private const val DOMINANT_TINT_MID_STOP = 0.45f
|
||||
|
||||
/**
|
||||
* Drag-down to dismiss. Mirrors Flutter's modal-page gesture: a
|
||||
* downward drag past the threshold pops the screen, returning the
|
||||
* user to whichever shell route launched NowPlaying. Tap, swipe-up,
|
||||
* and horizontal drags pass through to the underlying content
|
||||
* (slider scrub, etc.).
|
||||
* downward over-scroll past the threshold pops the screen.
|
||||
*
|
||||
* Implementation note: the body Column uses verticalScroll, which
|
||||
* eats every vertical drag delta before a top-level
|
||||
* detectVerticalDragGestures can see it. NestedScroll is the
|
||||
* right primitive here — verticalScroll offers its over-scroll
|
||||
* deltas to the nearest ancestor connection, so we accumulate
|
||||
* downward over-scroll (available.y > 0 means the scrollable
|
||||
* couldn't consume it because it's at the top) and pop on
|
||||
* threshold. The Slider keeps its own pointerInput and is
|
||||
* unaffected.
|
||||
*/
|
||||
@Composable
|
||||
private fun Modifier.nowPlayingDragDismiss(navController: NavHostController): Modifier =
|
||||
this.pointerInput(Unit) {
|
||||
var totalDragY = 0f
|
||||
detectVerticalDragGestures(
|
||||
onDragStart = { totalDragY = 0f },
|
||||
onDragEnd = {
|
||||
if (totalDragY > DRAG_DISMISS_THRESHOLD_PX) {
|
||||
navController.popBackStack()
|
||||
private fun rememberDragDismissConnection(
|
||||
onDismiss: () -> Unit,
|
||||
thresholdPx: Float = DRAG_DISMISS_THRESHOLD_PX,
|
||||
): NestedScrollConnection = remember(onDismiss, thresholdPx) {
|
||||
object : NestedScrollConnection {
|
||||
private var accumulated = 0f
|
||||
|
||||
override fun onPostScroll(
|
||||
consumed: Offset,
|
||||
available: Offset,
|
||||
source: NestedScrollSource,
|
||||
): Offset {
|
||||
if (source != NestedScrollSource.UserInput) return Offset.Zero
|
||||
if (available.y > 0f) {
|
||||
accumulated += available.y
|
||||
if (accumulated >= thresholdPx) {
|
||||
accumulated = 0f
|
||||
onDismiss()
|
||||
}
|
||||
totalDragY = 0f
|
||||
},
|
||||
onDragCancel = { totalDragY = 0f },
|
||||
onVerticalDrag = { _, delta -> totalDragY += delta },
|
||||
)
|
||||
return Offset(0f, available.y)
|
||||
}
|
||||
if (consumed.y != 0f || available.y < 0f) accumulated = 0f
|
||||
return Offset.Zero
|
||||
}
|
||||
|
||||
override suspend fun onPreFling(available: Velocity): Velocity {
|
||||
accumulated = 0f
|
||||
return Velocity.Zero
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
|
||||
Reference in New Issue
Block a user