nestedScroll
Common
Modifier in Compose Ui
Modify element to make it participate in the nested scrolling hierarchy.
There are two ways to participate in the nested scroll: as a scrolling child by dispatching scrolling events via [NestedScrollDispatcher] to the nested scroll chain; and as a member of nested scroll chain by providing [NestedScrollConnection], which will be called when another nested scrolling child below dispatches scrolling events.
It's mandatory to participate as a [NestedScrollConnection] in the chain, but dispatching scrolling events is optional since there are cases where an element wants to participate in nested scrolling without being directly scrollable.
Last updated:
Installation
dependencies {
implementation("androidx.compose.ui:ui:1.8.0-alpha04")
}
Overloads
fun Modifier.nestedScroll(
connection: NestedScrollConnection,
dispatcher: NestedScrollDispatcher? = null
): Modifier
Parameters
name | description |
---|---|
connection | connection to the nested scroll system to participate in the event chaining, receiving events when scrollable descendant is being scrolled. |
dispatcher | object to be attached to the nested scroll system on which dispatch* methods can be called to notify ancestors within nested scroll system about scrolling happening |
Code Examples
NestedScrollConnectionSample
@Composable
fun NestedScrollConnectionSample() {
// here we use LazyColumn that has build-in nested scroll, but we want to act like a
// parent for this LazyColumn and participate in its nested scroll.
// Let's make a collapsing toolbar for LazyColumn
val toolbarHeight = 48.dp
val toolbarHeightPx = with(LocalDensity.current) { toolbarHeight.roundToPx().toFloat() }
// our offset to collapse toolbar
val toolbarOffsetHeightPx = remember { mutableStateOf(0f) }
// now, let's create connection to the nested scroll system and listen to the scroll
// happening inside child LazyColumn
val nestedScrollConnection = remember {
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
// try to consume before LazyColumn to collapse toolbar if needed, hence pre-scroll
val delta = available.y
val newOffset = toolbarOffsetHeightPx.value + delta
toolbarOffsetHeightPx.value = newOffset.coerceIn(-toolbarHeightPx, 0f)
// here's the catch: let's pretend we consumed 0 in any case, since we want
// LazyColumn to scroll anyway for good UX
// We're basically watching scroll without taking it
return Offset.Zero
}
}
}
Box(
Modifier.fillMaxSize()
// attach as a parent to the nested scroll system
.nestedScroll(nestedScrollConnection)
) {
// our list with build in nested scroll support that will notify us about its scroll
LazyColumn(contentPadding = PaddingValues(top = toolbarHeight)) {
items(100) { index ->
Text("I'm item $index", modifier = Modifier.fillMaxWidth().padding(16.dp))
}
}
TopAppBar(
modifier =
Modifier.height(toolbarHeight).offset {
IntOffset(x = 0, y = toolbarOffsetHeightPx.value.roundToInt())
},
title = { Text("toolbar offset is ${toolbarOffsetHeightPx.value}") }
)
}
}
NestedScrollDispatcherSample
@Composable
fun NestedScrollDispatcherSample() {
// Let's take Modifier.draggable (which doesn't have nested scroll build in, unlike Modifier
// .scrollable) and add nested scroll support our component that contains draggable
// this will be a generic components that will work inside other nested scroll components.
// put it inside LazyColumn or / Modifier.verticalScroll to see how they will interact
// first, state and it's bounds
val basicState = remember { mutableStateOf(0f) }
val minBound = -100f
val maxBound = 100f
// lambda to update state and return amount consumed
val onNewDelta: (Float) -> Float = { delta ->
val oldState = basicState.value
val newState = (basicState.value + delta).coerceIn(minBound, maxBound)
basicState.value = newState
newState - oldState
}
// create a dispatcher to dispatch nested scroll events (participate like a nested scroll child)
val nestedScrollDispatcher = remember { NestedScrollDispatcher() }
// create nested scroll connection to react to nested scroll events (participate like a parent)
val nestedScrollConnection = remember {
object : NestedScrollConnection {
override fun onPostScroll(
consumed: Offset,
available: Offset,
source: NestedScrollSource
): Offset {
// we have no fling, so we're interested in the regular post scroll cycle
// let's try to consume what's left if we need and return the amount consumed
val vertical = available.y
val weConsumed = onNewDelta(vertical)
return Offset(x = 0f, y = weConsumed)
}
}
}
Box(
Modifier.size(100.dp)
.background(Color.LightGray)
// attach ourselves to nested scroll system
.nestedScroll(connection = nestedScrollConnection, dispatcher = nestedScrollDispatcher)
.draggable(
orientation = Orientation.Vertical,
state =
rememberDraggableState { delta ->
// here's regular drag. Let's be good citizens and ask parents first if they
// want to pre consume (it's a nested scroll contract)
val parentsConsumed =
nestedScrollDispatcher.dispatchPreScroll(
available = Offset(x = 0f, y = delta),
source = NestedScrollSource.UserInput
)
// adjust what's available to us since might have consumed smth
val adjustedAvailable = delta - parentsConsumed.y
// we consume
val weConsumed = onNewDelta(adjustedAvailable)
// dispatch as a post scroll what's left after pre-scroll and our
// consumption
val totalConsumed = Offset(x = 0f, y = weConsumed) + parentsConsumed
val left = adjustedAvailable - weConsumed
nestedScrollDispatcher.dispatchPostScroll(
consumed = totalConsumed,
available = Offset(x = 0f, y = left),
source = NestedScrollSource.UserInput
)
// we won't dispatch pre/post fling events as we have no flinging here, but
// the
// idea is very similar:
// 1. dispatch pre fling, asking parents to pre consume
// 2. fling (while dispatching scroll events like above for any fling tick)
// 3. dispatch post fling, allowing parent to react to velocity left
}
)
) {
Text("State: ${basicState.value.roundToInt()}", modifier = Modifier.align(Alignment.Center))
}
}