From 30580bbcf406420e62cc6339e3fbc7ff7ffed394 Mon Sep 17 00:00:00 2001 From: usuiat Date: Wed, 20 Sep 2023 23:02:26 +0900 Subject: [PATCH 1/4] Create NestedScrollDispatcher object in TimeTableGrid. --- .../confsched2023/sessions/section/TimetableGrid.kt | 12 ++++++------ .../confsched2023/sessions/section/TimetableSheet.kt | 7 ------- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/feature/sessions/src/main/java/io/github/droidkaigi/confsched2023/sessions/section/TimetableGrid.kt b/feature/sessions/src/main/java/io/github/droidkaigi/confsched2023/sessions/section/TimetableGrid.kt index e4a059235..092500790 100644 --- a/feature/sessions/src/main/java/io/github/droidkaigi/confsched2023/sessions/section/TimetableGrid.kt +++ b/feature/sessions/src/main/java/io/github/droidkaigi/confsched2023/sessions/section/TimetableGrid.kt @@ -35,8 +35,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollDispatcher import androidx.compose.ui.input.nestedscroll.NestedScrollSource +import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.pointer.PointerInputChange import androidx.compose.ui.input.pointer.PointerInputScope import androidx.compose.ui.input.pointer.pointerInput @@ -84,14 +86,12 @@ data class TimetableGridUiState(val timetable: Timetable) @Composable fun TimetableGrid( uiState: TimetableGridUiState, - nestedScrollDispatcher: NestedScrollDispatcher, onTimetableItemClick: (TimetableItem) -> Unit, modifier: Modifier = Modifier, contentPadding: PaddingValues = PaddingValues(), ) { TimetableGrid( timetable = uiState.timetable, - nestedScrollDispatcher = nestedScrollDispatcher, onTimetableItemClick = onTimetableItemClick, modifier = modifier, contentPadding = contentPadding, @@ -101,7 +101,6 @@ fun TimetableGrid( @Composable fun TimetableGrid( timetable: Timetable, - nestedScrollDispatcher: NestedScrollDispatcher, onTimetableItemClick: (TimetableItem) -> Unit, modifier: Modifier = Modifier, contentPadding: PaddingValues = PaddingValues(), @@ -133,7 +132,6 @@ fun TimetableGrid( TimetableGrid( timetable = timetable, timetableState = timetableGridState, - nestedScrollDispatcher = nestedScrollDispatcher, modifier = modifier, contentPadding = PaddingValues( top = 16.dp + contentPadding.calculateTopPadding(), @@ -157,7 +155,6 @@ fun TimetableGrid( fun TimetableGrid( timetable: Timetable, timetableState: TimetableState, - nestedScrollDispatcher: NestedScrollDispatcher, modifier: Modifier = Modifier, contentPadding: PaddingValues = PaddingValues(), content: @Composable (TimetableItem, Int) -> Unit, @@ -186,10 +183,14 @@ fun TimetableGrid( content(timetableItemWithFavorite.timetableItem, itemHeightPx) } + val nestedScrollConnection = remember { object : NestedScrollConnection {} } + val nestedScrollDispatcher = remember { NestedScrollDispatcher() } + LazyLayout( modifier = modifier .focusGroup() .clipToBounds() + .nestedScroll(nestedScrollConnection, nestedScrollDispatcher) .drawBehind { timetableScreen.timeHorizontalLines.value.forEach { drawLine( @@ -322,7 +323,6 @@ fun TimetableGrid( fun TimetablePreview() { TimetableGrid( timetable = Timetable.fake(), - nestedScrollDispatcher = remember { NestedScrollDispatcher() }, onTimetableItemClick = {}, modifier = Modifier.fillMaxSize(), ) diff --git a/feature/sessions/src/main/java/io/github/droidkaigi/confsched2023/sessions/section/TimetableSheet.kt b/feature/sessions/src/main/java/io/github/droidkaigi/confsched2023/sessions/section/TimetableSheet.kt index f2e208f73..711dd18fc 100644 --- a/feature/sessions/src/main/java/io/github/droidkaigi/confsched2023/sessions/section/TimetableSheet.kt +++ b/feature/sessions/src/main/java/io/github/droidkaigi/confsched2023/sessions/section/TimetableSheet.kt @@ -20,7 +20,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.input.nestedscroll.NestedScrollConnection -import androidx.compose.ui.input.nestedscroll.NestedScrollDispatcher import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalLayoutDirection @@ -128,17 +127,11 @@ fun TimetableSheet( } is GridTimetable -> { - val nestedScrollDispatcher = remember { NestedScrollDispatcher() } TimetableGrid( uiState = requireNotNull(uiState.timetableGridUiState[selectedDay]), - nestedScrollDispatcher = nestedScrollDispatcher, onTimetableItemClick = onTimetableItemClick, modifier = Modifier .fillMaxSize() - .nestedScroll( - timetableSheetContentScrollState.nestedScrollConnection, - nestedScrollDispatcher, - ) .weight(1f), contentPadding = PaddingValues( bottom = contentPadding.calculateBottomPadding(), From b786a70889910f1a67f3e755b377a4405e11fd54 Mon Sep 17 00:00:00 2001 From: usuiat Date: Sat, 23 Sep 2023 22:54:08 +0900 Subject: [PATCH 2/4] Implement fling for nested scroll of TimetableGrid. --- .../sessions/section/TimetableGrid.kt | 38 +++++++++++++++---- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/feature/sessions/src/main/java/io/github/droidkaigi/confsched2023/sessions/section/TimetableGrid.kt b/feature/sessions/src/main/java/io/github/droidkaigi/confsched2023/sessions/section/TimetableGrid.kt index 092500790..a4d501718 100644 --- a/feature/sessions/src/main/java/io/github/droidkaigi/confsched2023/sessions/section/TimetableGrid.kt +++ b/feature/sessions/src/main/java/io/github/droidkaigi/confsched2023/sessions/section/TimetableGrid.kt @@ -1,6 +1,8 @@ package io.github.droidkaigi.confsched2023.sessions.section import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.AnimationState +import androidx.compose.animation.core.animateDecay import androidx.compose.animation.core.exponentialDecay import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.focusGroup @@ -246,7 +248,7 @@ fun TimetableGrid( }, onDragEnd = { coroutineScope.launch { - scrollState.flingIfPossible() + scrollState.flingIfPossible(nestedScrollDispatcher) } }, ) @@ -510,7 +512,7 @@ class ScreenScrollState( } } - suspend fun flingIfPossible() = coroutineScope { + suspend fun flingIfPossible(nestedScrollDispatcher: NestedScrollDispatcher) = coroutineScope { val velocity = velocityTracker.calculateVelocity() launch { _scrollX.animateDecay( @@ -518,11 +520,33 @@ class ScreenScrollState( exponentialDecay(), ) } - launch { - _scrollY.animateDecay( - velocity.y / 2f, - exponentialDecay(), - ) + + var lastValue = 0f + AnimationState( + initialValue = 0f, + initialVelocity = velocity.y, + ).animateDecay( + exponentialDecay() + ) { + launch { + val delta = Offset(0f, value - lastValue) + lastValue = value + val preConsumed = nestedScrollDispatcher.dispatchPreScroll( + available = delta, + source = NestedScrollSource.Fling, + ) + + val weAvailable = delta - preConsumed + val previousY = _scrollY.value + _scrollY.snapTo(_scrollY.value + weAvailable.y) + val weConsumed = Offset(0f, _scrollY.value - previousY) + + nestedScrollDispatcher.dispatchPostScroll( + consumed = preConsumed + weConsumed, + available = weAvailable - weConsumed, + source = NestedScrollSource.Fling, + ) + } } } From 98e0c88f7ee3f4a01552faecd77361be6425c995 Mon Sep 17 00:00:00 2001 From: usuiat Date: Sat, 23 Sep 2023 23:56:51 +0900 Subject: [PATCH 3/4] Format codes. --- .../droidkaigi/confsched2023/sessions/section/TimetableGrid.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/feature/sessions/src/main/java/io/github/droidkaigi/confsched2023/sessions/section/TimetableGrid.kt b/feature/sessions/src/main/java/io/github/droidkaigi/confsched2023/sessions/section/TimetableGrid.kt index a4d501718..891654ea9 100644 --- a/feature/sessions/src/main/java/io/github/droidkaigi/confsched2023/sessions/section/TimetableGrid.kt +++ b/feature/sessions/src/main/java/io/github/droidkaigi/confsched2023/sessions/section/TimetableGrid.kt @@ -526,7 +526,7 @@ class ScreenScrollState( initialValue = 0f, initialVelocity = velocity.y, ).animateDecay( - exponentialDecay() + exponentialDecay(), ) { launch { val delta = Offset(0f, value - lastValue) From 3f69c5258d333a299a09ce44bae23e230c559e4b Mon Sep 17 00:00:00 2001 From: usuiat Date: Wed, 27 Sep 2023 00:32:15 +0900 Subject: [PATCH 4/4] Fix unexpected fling behaviors. - Use a position relative to the root component to calculate velocity. - Reset VelocityTracker when drag gesture starts. - Cancel fling animation when another drag gesture starts. - Don't do scroll process when the position does not change. --- .../sessions/section/TimetableGrid.kt | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/feature/sessions/src/main/java/io/github/droidkaigi/confsched2023/sessions/section/TimetableGrid.kt b/feature/sessions/src/main/java/io/github/droidkaigi/confsched2023/sessions/section/TimetableGrid.kt index 891654ea9..b97917b62 100644 --- a/feature/sessions/src/main/java/io/github/droidkaigi/confsched2023/sessions/section/TimetableGrid.kt +++ b/feature/sessions/src/main/java/io/github/droidkaigi/confsched2023/sessions/section/TimetableGrid.kt @@ -47,6 +47,8 @@ import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.positionChange import androidx.compose.ui.input.pointer.util.VelocityTracker import androidx.compose.ui.layout.Placeable +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.layout.positionInRoot import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.semantics.ScrollAxisRange @@ -228,8 +230,14 @@ fun TimetableGrid( } }, ) + .onGloballyPositioned { coordinates -> + timetableState.screenScrollState.componentPositionInRoot = coordinates.positionInRoot() + } .pointerInput(Unit) { detectDragGestures( + onDragStart = { + scrollState.resetTracking() + }, onDrag = { change, dragAmount -> if (timetableScreen.enableHorizontalScroll(dragAmount.x)) { if (change.positionChange() != Offset.Zero) change.consume() @@ -482,6 +490,8 @@ class ScreenScrollState( private val velocityTracker = VelocityTracker() private val _scrollX = Animatable(initialScrollX) private val _scrollY = Animatable(initialScrollY) + var componentPositionInRoot = Offset.Zero + private var cancelFling = false val scrollX: Float get() = _scrollX.value @@ -499,9 +509,11 @@ class ScreenScrollState( timeMillis: Long, position: Offset, ) { + cancelFling = true if (scrollX.isNaN().not() && scrollY.isNaN().not()) { coroutineScope { - velocityTracker.addPosition(timeMillis = timeMillis, position = position) + val positionInRoot = position + componentPositionInRoot + velocityTracker.addPosition(timeMillis = timeMillis, position = positionInRoot) launch { _scrollX.snapTo(scrollX) } @@ -513,6 +525,7 @@ class ScreenScrollState( } suspend fun flingIfPossible(nestedScrollDispatcher: NestedScrollDispatcher) = coroutineScope { + cancelFling = false val velocity = velocityTracker.calculateVelocity() launch { _scrollX.animateDecay( @@ -546,6 +559,10 @@ class ScreenScrollState( available = weAvailable - weConsumed, source = NestedScrollSource.Fling, ) + + if (cancelFling) { + this@animateDecay.cancelAnimation() + } } } } @@ -696,6 +713,9 @@ private class TimetableScreen( position: Offset, nestedScrollDispatcher: NestedScrollDispatcher, ) { + // If the position does not change, VelocityTracker malfunctions. Therefore return here. + if (dragAmount == Offset.Zero) return + val parentConsumed = nestedScrollDispatcher.dispatchPreScroll( available = dragAmount, source = NestedScrollSource.Drag,