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.