Skip to content

makzimi/scroll-effect-lazy

Repository files navigation

Scroll Effect Lazy

Maven Central License: MIT

Small Compose lib that makes LazyColumn / LazyRow feel a bit rubbery. Scroll fast and the items at the front lag behind a little, like they're on a stretchy band. Nothing weird happens to layout or measuring, it's all just graphicsLayer under the hood.

Timeline_1x_24fps

Install

Published to Maven Central, so no extra repositories are needed beyond the usual mavenCentral().

Kotlin Multiplatform — add it to commonMain:

kotlin {
    sourceSets {
        commonMain.dependencies {
            implementation("io.github.makzimi:scroll-effects:0.2.0")
        }
    }
}

Android-only (or any single-platform module):

dependencies {
    implementation("io.github.makzimi:scroll-effects:0.2.0")
}

Targets: Android, iOS, desktop (JVM), and web (Wasm/JS).


Usage

Swap your LazyColumn / LazyRow for the wrapper, toss in a scrollEffect lambda, done:

ScrollEffectLazyColumn(
    scrollEffect = { item -> elastic(item, ElasticStrength.Normal) }
) {
    items(myList) { MyCard(it) }
}

Don't want the effect? Pass null or just leave it out. You're back to a plain lazy list, no overhead.


Custom effects

The built-in elastic() is just one thing you can do. The scrollEffect lambda runs per visible item on every frame, and this inside it is a GraphicsLayerScope, so you can do whatever you want with it.

ScrollEffectLazyColumn(
    // lambda runs per-item, every frame
    scrollEffect = { item ->
        // use the prebuilt rubber band
        elastic(item)

        // or roll your own with anything from item / item.velocity
        // (it's a GraphicsLayerScope, so all the usual stuff is here)
        alpha = 1f - item.velocity.stretch * 0.3f
        rotationZ = item.velocity.direction * 2f
    }
) {
    items(myList) { MyCard(it) }
}

You can also mix: call elastic(item) for the base feel and then layer your own tweaks on top.


Why a whole new composable and not just a Modifier?

Short answer: the effect has to touch every item, not the list as a whole. To pull that off we have to sit between you and LazyListScope and wrap each item in its own graphicsLayer. A modifier on the list can't do that, and making you glue a modifier onto every item by hand would be a pain. The lambda also runs on a little custom scope (ScrollEffectScope) that bundles GraphicsLayerScope with list-aware stuff and shortcuts like elastic(), which a regular modifier just can't give you.


API reference

ScrollEffectLazyColumn / ScrollEffectLazyRow

Same as LazyColumn / LazyRow. All the usual params go straight through. The only extra thing is:

scrollEffect: (ScrollEffectScope.(item: ScrollingItemData) -> Unit)? = null

If it's null, we don't do anything fancy, just call the normal composable.


ScrollEffectScope

Whatever this is inside your scrollEffect lambda. It's a GraphicsLayerScope, so all the usual toys are there (alpha, scaleX, rotationZ, ...), plus this one shortcut:

fun elastic(
    item: ScrollingItemData,
    strength: ElasticStrength = ElasticStrength.Normal,
    maxTranslationDp: Float = 150f,
)

ScrollingItemData

One of these lands in your lambda for every visible item on every frame.

Property Type What it is
index Int Which spot this item sits in
velocity ScrollVelocity What the list's doing right now
distanceFromDragged Int How far from the item under your finger (0 means that one, positive means further down, negative means further back)

ScrollVelocity

Quick snapshot of how the list is moving. The built-in elastic() only looks at lagPx, the rest are there so you have stuff to work with when you write your own effects.

Property Type What it is
lagPx Float Signed lag in pixels. This is what elastic() uses to do its thing
rawPxPerSec Float Straight-from-VelocityTracker speed, px/s
stretch Float Same thing squashed into [0, 1] with tanh
direction Int +1 = forward, -1 = back, 0 = not moving
isDragging Boolean Finger's still on the screen
isFlinging Boolean Finger's off, list's still sliding

ElasticStrength

How floppy you want the rubber.

Variant Feel Gap size
Hard Stiff, barely there Small
Normal Just right Medium
Loose Soft, really obvious Big

Running the samples

The repo ships a sample app that shows the effect across platforms. The UI is shared in sample-common; each platform just hosts it.

Platform How to run
Android Open in Android Studio and run the sample-android app, or ./gradlew :sample-android:installDebug
iOS Open iosApp/iosApp.xcodeproj in Xcode and run. To run on a device, set your Apple Team ID in iosApp/Configuration/Config.xcconfig
Desktop (JVM) ./gradlew :sample-common:run
Web (Wasm/JS) ./gradlew :sample-common:wasmJsBrowserDevelopmentRun, then open http://localhost:8080

Web needs a browser with WebAssembly GC support (Chrome/Edge 119+, Firefox 120+, Safari 18+).

Toggle the Off / On switch at the top of any list screen to compare a plain list with the elastic effect.


License

MIT License

Copyright (c) 2026 Maxim Kachinkin

Released under the MIT License — do what you want, just keep the notice.

About

Add elastic stretch effect to LazyColumn and LazyRow

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages