Build apps faster with over 150+ styled components and screens! Check it out →

overscroll

Common

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

namedescription
overscrollEffectthe [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)
        )
    }
}
by @alexstyl