Change effects
How a change effect decides to play, and what equality, isEnabled, cooldown, delay, times, speed and reducedMotion do.
A change effect plays once each time a value you watch changes. You attach it with Modifier.changeEffect(value, effect).
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.jantursky.kapow.ChangeEffect
import com.jantursky.kapow.changeEffect
@Composable
fun Score(score: Int) {
BasicText(
text = "$score",
modifier = Modifier.changeEffect(score, ChangeEffect.pulse()),
)
}
The effects are shine, pulse, glow, spray, rise, spin, ripple, flash, burst, speedLines, sparkle, fireworks, confettiPop, hearts, trace, strike, stamp and glitch. The effects overview has one line on each, and Tuning effects covers the options they share.
When it plays
Kapow keeps the last value it saw on the element. Each time the modifier is applied with a new value:
- First composition is silent. The value you start with is only recorded.
- Equal values do nothing. The comparison is
!=. Replacing3with3, or a list with another list that is equal, plays nothing. - A different value plays the effect once.
The value can be anything: an Int, a Boolean, an enum, a data class. If you want to replay for the same data, for example a “ping” button, watch a counter that you increase on each tap instead.
import androidx.compose.foundation.clickable
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import com.jantursky.kapow.ChangeEffect
import com.jantursky.kapow.changeEffect
@Composable
fun PingButton() {
var pings by remember { mutableIntStateOf(0) }
BasicText(
text = "Ping",
modifier = Modifier
.clickable { pings++ }
.changeEffect(pings, ChangeEffect.glow()),
)
}
isEnabled
isEnabled defaults to true. While it is false, changes are recorded but play nothing. Turning it back on does not replay old changes. Use it to stay quiet during a bulk update or while the screen is not visible.
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.jantursky.kapow.ChangeEffect
import com.jantursky.kapow.changeEffect
@Composable
fun Total(total: Int, isLoading: Boolean) {
BasicText(
text = "$total",
modifier = Modifier.changeEffect(total, ChangeEffect.shine(), isEnabled = !isLoading),
)
}
Cooldown
Some effects ignore a change that arrives too soon after the last one they accepted. The cooldown is part of the effect, not a parameter:
| Effect | Cooldown |
|---|---|
shine |
half of its duration |
pulse |
0.2 s for each pulse after the first (count - 1) |
trace |
half of its duration |
glow, spray, rise, spin, ripple, flash, burst, speedLines, sparkle, fireworks, confettiPop, hearts, strike, stamp, glitch |
none |
.cooldown(...) replaces the effect’s own cooldown, as the next section shows. With the default 1 s shine, two changes 0.2 s apart play one sweep. The effects without a cooldown play on every change: rise floats two labels for two quick changes, glow gets stronger up to a limit, and spin adds speed.
Delaying an effect
.delay() wraps any change effect so it starts a fixed time after the change is accepted. Delays add up if you nest them. The cooldown of the wrapped effect still applies.
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.jantursky.kapow.ChangeEffect
import com.jantursky.kapow.changeEffect
import kotlin.time.Duration.Companion.milliseconds
@Composable
fun Reward(coins: Int) {
BasicText(
text = "Coins: $coins",
modifier = Modifier.changeEffect(coins, ChangeEffect.spray().delay(300.milliseconds)),
)
}
Cooldown, times, speed and reduced motion
Four more wrappers work on any change effect. Each returns a new ChangeEffect, and they can be chained in any order.
| Wrapper | What it does |
|---|---|
.cooldown(duration) |
Ignores a change that arrives sooner than duration after the last one that played. It replaces the effect’s own cooldown, so .cooldown(Duration.ZERO) turns it off. |
.times(count, gap) |
Plays the effect count times for each change, gap apart. Changes that arrive while the series runs are ignored. |
.speed(multiplier) |
Plays the effect at multiplier times the pace: 2 takes half as long, 0.5 twice as long. Repeated calls multiply. |
.reducedMotion(mode) |
Chooses what the effect does under reduced motion. See Reduced motion. |
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.jantursky.kapow.ChangeEffect
import com.jantursky.kapow.ReducedMotionMode
import com.jantursky.kapow.changeEffect
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
@Composable
fun Likes(likes: Int) {
BasicText(
text = "$likes",
modifier = Modifier.changeEffect(
likes,
ChangeEffect.pulse()
.cooldown(2.seconds)
.times(count = 2, gap = 150.milliseconds)
.speed(1.5f)
.reducedMotion(ReducedMotionMode.Nothing),
),
)
}
A count or a multiplier that is not positive, or a negative duration, throws when you build the effect.
Feedback
Pass feedback = KapowFeedbackEvent.ImpactLight to ask for a haptic or a sound when the effect plays. It is also called under reduced motion. See Feedback.
No recomposition per frame
Only the value change recomposes. The animation runs inside the modifier node and invalidates drawing, not composition. Performance has the details.