Skip to content
Kapow
Esc
↑↓navigate↵open⌘Jpreview
On this page

Progress effects

Draw an effect at a progress you control, such as a scroll position, a slider or a hinge angle.

A progress effect has no clock of its own. You give it a progress from 0 to 1 and it draws that moment of the effect. Modifier.progressEffect(effect) { progress } takes the effect and a lambda that returns the progress. Values outside 0 to 1 are clamped.

The effects are shine, glow, iris, fill and trace:

Effect At 0 At 1
ProgressEffect.shine() the band is before the element the band is past the element
ProgressEffect.glow() no halo full halo
ProgressEffect.iris() nothing is revealed the whole element is revealed
ProgressEffect.fill() no liquid the whole element is tinted
ProgressEffect.trace() no outline the closed outline

Curve and smoothing

Two wrappers change how the progress reaches the effect. Both return a new ProgressEffect.

  • .curve(easing) shapes the progress with an easing before the effect draws it, so a linear scroll can start slowly.
  • .smoothed(dampingRatio, stiffness) follows the progress with a spring instead of jumping to it. A progress that changes in steps, such as a download in chunks, then moves smoothly. The spring runs in the draw phase and does not recompose.
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.jantursky.kapow.ProgressEffect
import com.jantursky.kapow.progressEffect

@Composable
fun DownloadFill(progress: Float) {
    Box(
        Modifier
            .size(120.dp)
            .progressEffect(ProgressEffect.fill().curve(FastOutSlowInEasing).smoothed()) { progress },
    )
}

The sample passes a captured Float because the progress changes in steps and the spring smooths it. For a value that changes every frame, read state inside the lambda as described below.

Driven by scroll

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.jantursky.kapow.ProgressEffect
import com.jantursky.kapow.progressEffect

@Composable
fun ScrollShine() {
    val scroll = rememberScrollState()
    Column(Modifier.fillMaxSize().verticalScroll(scroll)) {
        Box(
            Modifier
                .height(120.dp)
                .progressEffect(ProgressEffect.shine()) {
                    if (scroll.maxValue == 0) 0f else scroll.value / scroll.maxValue.toFloat()
                },
        )
        Box(Modifier.height(2000.dp))
    }
}

The lambda is read while drawing

Kapow calls your lambda in the draw phase. If it reads Compose state, such as scroll.value above or an Animatable, the effect redraws when the state changes and nothing recomposes. Do not compute the progress in the composable body and pass a captured Float: that would recompose on every change and the effect would lag a frame behind.

A new lambda on a recomposition updates the node in place; it does not rebuild the effect.

Hinge

The kapow-crease artifact builds on this: Modifier.hingeEffect feeds the hinge angle of a foldable into a progress effect. See Hinge effects.

Reduced motion

Progress effects follow the progress you give them, so Kapow does not animate them on its own. If the progress itself moves a lot (a fast scroll, say), check isReducedMotion() and decide what to show. .reducedMotion(ReducedMotionMode.Nothing) makes one effect draw nothing under reduced motion; the other modes change nothing. See Reduced motion.

Was this page helpful?