overscroll
Modifier in Compose Foundation
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.
Last updated:
Installation
dependencies {
implementation("androidx.compose.foundation:foundation:1.8.0-beta01")
}
Overloads
@Suppress("DEPRECATION_ERROR")
fun Modifier.overscroll(overscrollEffect: OverscrollEffect?): Modifier
Parameters
name | description |
---|---|
overscrollEffect | the [OverscrollEffect] to render |
Code Example
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)
)
}
}