overscroll
fun Modifier.overscroll(overscrollEffect: OverscrollEffect?): Modifier
Renders overscroll from the provided overscrollEffect.
This modifier attaches the provided overscrollEffect's OverscrollEffect.node to the
hierarchy, which renders the actual effect. Note that this modifier is only responsible for the
visual part of overscroll - on its own it will not handle input events. In addition to using this
modifier you also need to propagate events to the overscrollEffect, most commonly by using a
androidx.compose.foundation.gestures.scrollable.
Alternatively, you can use a higher level API such as verticalScroll or
androidx.compose.foundation.lazy.LazyColumn and provide a custom OverscrollEffect - these
components will both render and provide events to the OverscrollEffect, so you do not need to
manually render the effect with this modifier.
Parameters
| overscrollEffect | the OverscrollEffect to render |
Code Examples
OverscrollSample
@Composable
fun OverscrollSample() {
// our custom offset overscroll that offset the element it is applied to when we hit the bound
// on the scrollable container.
class OffsetOverscrollEffect(val scope: CoroutineScope) : OverscrollEffect {
private val overscrollOffset = Animatable(0f)
override fun applyToScroll(
delta: Offset,
source: NestedScrollSource,
performScroll: (Offset) -> Offset,
): Offset {
// in pre scroll we relax the overscroll if needed
// relaxation: when we are in progress of the overscroll and user scrolls in the
// different direction = substract the overscroll first
val sameDirection = sign(delta.y) == sign(overscrollOffset.value)
val consumedByPreScroll =
if (abs(overscrollOffset.value) > 0.5 && !sameDirection) {
val prevOverscrollValue = overscrollOffset.value
val newOverscrollValue = overscrollOffset.value + delta.y
if (sign(prevOverscrollValue) != sign(newOverscrollValue)) {
// sign changed, coerce to start scrolling and exit
scope.launch { overscrollOffset.snapTo(0f) }
Offset(x = 0f, y = delta.y + prevOverscrollValue)
} else {
scope.launch { overscrollOffset.snapTo(overscrollOffset.value + delta.y) }
delta.copy(x = 0f)
}
} else {
Offset.Zero
}
val leftForScroll = delta - consumedByPreScroll
val consumedByScroll = performScroll(leftForScroll)
val overscrollDelta = leftForScroll - consumedByScroll
// if it is a drag, not a fling, add the delta left to our over scroll value
if (abs(overscrollDelta.y) > 0.5 && source == NestedScrollSource.UserInput) {
scope.launch {
// multiply by 0.1 for the sake of parallax effect
overscrollOffset.snapTo(overscrollOffset.value + overscrollDelta.y * 0.1f)
}
}
return consumedByPreScroll + consumedByScroll
}
override suspend fun applyToFling(
velocity: Velocity,
performFling: suspend (Velocity) -> Velocity,
) {
val consumed = performFling(velocity)
// when the fling happens - we just gradually animate our overscroll to 0
val remaining = velocity - consumed
overscrollOffset.animateTo(
targetValue = 0f,
initialVelocity = remaining.y,
animationSpec = spring(),
)
}
override val isInProgress: Boolean
get() = overscrollOffset.value != 0f
// Create a LayoutModifierNode that offsets by overscrollOffset.value
override val node: DelegatableNode =
object : Modifier.Node(), LayoutModifierNode {
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints,
): MeasureResult {
val placeable = measurable.measure(constraints)
return layout(placeable.width, placeable.height) {
val offsetValue = IntOffset(x = 0, y = overscrollOffset.value.roundToInt())
placeable.placeRelativeWithLayer(offsetValue.x, offsetValue.y)
}
}
}
}
val offset = remember { mutableStateOf(0f) }
val scope = rememberCoroutineScope()
// Create the overscroll controller
val overscroll = remember(scope) { OffsetOverscrollEffect(scope) }
// let's build a scrollable that scroll until -512 to 512
val scrollStateRange = (-512f).rangeTo(512f)
Box(
Modifier.size(150.dp)
.scrollable(
orientation = Orientation.Vertical,
state =
rememberScrollableState { delta ->
// use the scroll data and indicate how much this element consumed.
val oldValue = offset.value
// coerce to our range
offset.value = (offset.value + delta).coerceIn(scrollStateRange)
offset.value - oldValue // indicate that we consumed what's needed
},
// pass the overscroll to the scrollable so the data is updated
overscrollEffect = overscroll,
)
.background(Color.LightGray),
contentAlignment = Alignment.Center,
) {
Text(
offset.value.roundToInt().toString(),
style = TextStyle(fontSize = 32.sp),
modifier =
Modifier
// show the overscroll only on the text, not the containers (just for fun)
.overscroll(overscroll),
)
}
}
