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.
Distributed via JitPack.
In your settings.gradle.kts:
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven("https://jitpack.io")
}
}In your app/library build.gradle.kts:
implementation("com.github.makzimi:scroll-effect-lazy:0.1.0")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.
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.
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.
Same as LazyColumn / LazyRow. All the usual params go straight through. The only extra thing is:
scrollEffect: (ScrollEffectScope.(item: ScrollingItemData) -> Unit)? = nullIf it's null, we don't do anything fancy, just call the normal composable.
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,
)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) |
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 |
How floppy you want the rubber.
| Variant | Feel | Gap size |
|---|---|---|
Hard |
Stiff, barely there | Small |
Normal |
Just right | Medium |
Loose |
Soft, really obvious | Big |