Skip to content

Commit e8b5df4

Browse files
committed
chore(ui): update scroll aware Modifiers to support ScrollState (sheet resignment, vertical scroll gradient)
Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
1 parent fbcb404 commit e8b5df4

2 files changed

Lines changed: 71 additions & 9 deletions

File tree

ui/core/src/main/kotlin/com/getcode/ui/core/Modifier.kt

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import androidx.compose.foundation.border
77
import androidx.compose.foundation.clickable
88
import androidx.compose.foundation.combinedClickable
99
import androidx.compose.foundation.interaction.MutableInteractionSource
10+
import androidx.compose.foundation.ScrollState
1011
import androidx.compose.foundation.lazy.LazyListState
1112
import androidx.compose.foundation.lazy.grid.LazyGridState
1213
import androidx.compose.material.ripple
@@ -393,6 +394,34 @@ fun Modifier.verticalScrollStateGradient(
393394
}
394395
}
395396

397+
fun Modifier.verticalScrollStateGradient(
398+
scrollState: ScrollState,
399+
color: Color = Color.Unspecified,
400+
showAtStart: Boolean = true,
401+
showAtStartAlways: Boolean = false,
402+
showAtEnd: Boolean = true,
403+
showAtEndAlways: Boolean = false,
404+
isLongGradient: Boolean = false,
405+
): Modifier = composed {
406+
val backgroundColor = color.takeOrElse { CodeTheme.colors.background }
407+
val gradientSizePx =
408+
with(LocalDensity.current) { gradientSize.toPx() } * if (isLongGradient) 1.5f else 1f
409+
this
410+
.addIf((showAtStart && scrollState.value > 0) || showAtStartAlways) {
411+
Modifier.drawWithGradient(
412+
color = backgroundColor,
413+
startY = { gradientSizePx },
414+
endY = { 0f },
415+
)
416+
}
417+
.addIf((showAtEnd && scrollState.value < scrollState.maxValue) || showAtEndAlways) {
418+
Modifier.drawWithGradient(
419+
color = backgroundColor,
420+
startY = { size.height - gradientSizePx },
421+
)
422+
}
423+
}
424+
396425
fun LazyListState.isScrolledToEnd(): Boolean {
397426
val lastItem = layoutInfo.visibleItemsInfo.lastOrNull()
398427
return lastItem == null ||

ui/navigation/src/main/kotlin/com/getcode/ui/utils/SheetResignmentModifierNode.kt

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.getcode.ui.utils
22

3+
import androidx.compose.foundation.ScrollState
34
import androidx.compose.foundation.lazy.LazyListState
45
import androidx.compose.runtime.Composable
56
import androidx.compose.runtime.derivedStateOf
@@ -56,15 +57,12 @@ internal class SheetResignmentConnection(
5657
}
5758

5859
private class SheetResignmentModifierNode(
59-
private val listState: LazyListState,
60+
private val isAtTopProvider: () -> Boolean,
6061
private val connection: SheetResignmentConnection,
6162
private val autoResetDelayMs: Long,
6263
) : Modifier.Node(), DelegatableNode {
6364

64-
private val isAtTop by derivedStateOf {
65-
listState.firstVisibleItemIndex == 0 &&
66-
listState.firstVisibleItemScrollOffset == 0
67-
}
65+
private val isAtTop by derivedStateOf { isAtTopProvider() }
6866

6967
private var observeJob: Job? = null
7068

@@ -96,18 +94,17 @@ private class SheetResignmentModifierNode(
9694
}
9795

9896
private data class SheetResignmentElement(
99-
val listState: LazyListState,
97+
val isAtTopProvider: () -> Boolean,
10098
val connection: SheetResignmentConnection,
10199
val autoResetDelayMs: Long,
102100
) : ModifierNodeElement<SheetResignmentModifierNode>() {
103101

104-
override fun create() = SheetResignmentModifierNode(listState, connection, autoResetDelayMs)
102+
override fun create() = SheetResignmentModifierNode(isAtTopProvider, connection, autoResetDelayMs)
105103

106104
override fun update(node: SheetResignmentModifierNode) = Unit
107105

108106
override fun InspectorInfo.inspectableProperties() {
109107
name = "sheetResignmentBehavior"
110-
properties["listState"] = listState
111108
properties["autoResetDelayMs"] = autoResetDelayMs
112109
}
113110
}
@@ -135,5 +132,41 @@ fun Modifier.sheetResignmentBehavior(
135132
}
136133
return this
137134
.nestedScroll(connection)
138-
.then(SheetResignmentElement(listState, connection, autoResetDelayMs))
135+
.then(SheetResignmentElement(
136+
isAtTopProvider = {
137+
listState.firstVisibleItemIndex == 0 &&
138+
listState.firstVisibleItemScrollOffset == 0
139+
},
140+
connection = connection,
141+
autoResetDelayMs = autoResetDelayMs,
142+
))
143+
}
144+
145+
/**
146+
* Prevents a [ModalBottomSheet] from starting dismiss on the first downward drag after the
147+
* internal [ScrollState] reaches the top. The first drag triggers overscroll bounce only;
148+
* a subsequent downward drag (or after [autoResetDelayMs]) will dismiss normally.
149+
*
150+
* Requires [LocalSheetGesturesState] to be provided, or pass [setGesturesEnabled] explicitly.
151+
*
152+
* @param scrollState the [ScrollState] of the scrollable Column inside the sheet
153+
* @param setGesturesEnabled callback to toggle sheet gesture handling (e.g. `sheetState.gesturesEnabled`)
154+
* @param autoResetDelayMs time after which a paused-then-drag will re-enable dismiss; 0 to disable
155+
*/
156+
@Composable
157+
fun Modifier.sheetResignmentBehavior(
158+
scrollState: ScrollState,
159+
setGesturesEnabled: (Boolean) -> Unit = LocalSheetGesturesState.current,
160+
autoResetDelayMs: Long = 700L,
161+
): Modifier {
162+
val connection = remember(setGesturesEnabled, autoResetDelayMs) {
163+
SheetResignmentConnection(setGesturesEnabled, autoResetDelayMs)
164+
}
165+
return this
166+
.nestedScroll(connection)
167+
.then(SheetResignmentElement(
168+
isAtTopProvider = { scrollState.value == 0 },
169+
connection = connection,
170+
autoResetDelayMs = autoResetDelayMs,
171+
))
139172
}

0 commit comments

Comments
 (0)