Transitions
Masked and particle enter and exit transitions for AnimatedVisibility and AnimatedContent, and why they are applied with a modifier.
Kapow has sixteen transitions. Eleven are masks that work in both directions: iris, clock, wipe, glare, blinds, dissolve, curtain, shapeIris, checker, tear and blob. pop and anvil are enter-only. poof, shatter and disintegrate are exit-only.
Why not enter = KapowEnter.iris()?
You would expect to write AnimatedVisibility(visible, enter = iris()). That is not possible. EnterTransition and ExitTransition are sealed classes in Compose, and their contents are internal data the animation system reads. No library can add a new kind.
Compose does expose one public hook. Inside AnimatedVisibility or AnimatedContent, the scope has a transition: Transition<EnterExitState>, and anything animated on that transition is waited for before the content leaves composition. Kapow animates its own progress on that transition. So the transition is a modifier on the content, not an argument of the container.
Modifier.kapowTransition
Pass the scope as the first argument, and set the stock transitions to None.
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.foundation.background
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.graphics.Color
import androidx.compose.ui.unit.dp
import com.jantursky.kapow.KapowEnter
import com.jantursky.kapow.KapowExit
import com.jantursky.kapow.kapowTransition
@Composable
fun Toast(visible: Boolean) {
AnimatedVisibility(visible, enter = EnterTransition.None, exit = ExitTransition.None) {
Box(
Modifier
.kapowTransition(this, enter = KapowEnter.iris(), exit = KapowExit.poof())
.size(160.dp, 56.dp)
.background(Color.DarkGray),
)
}
}
Both enter and exit are optional and default to None. Because the animation lives on the scope’s transition, the container keeps the content in composition until your exit has finished. A 400 ms poof keeps the content for 400 ms.
Mask transitions run the same motion in reverse on exit: an iris that grew on enter shrinks on exit. KapowExit.iris() and KapowEnter.iris() take the same parameters.
KapowAnimatedVisibility
For the common case, KapowAnimatedVisibility wraps AnimatedVisibility and applies the modifier for you.
import androidx.compose.foundation.background
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.graphics.Color
import androidx.compose.ui.unit.dp
import com.jantursky.kapow.KapowAnimatedVisibility
import com.jantursky.kapow.KapowEnter
import com.jantursky.kapow.KapowExit
@Composable
fun Dialog(visible: Boolean) {
KapowAnimatedVisibility(
visible = visible,
enter = KapowEnter.pop(),
exit = KapowExit.poof(),
) {
Box(Modifier.size(200.dp).background(Color.DarkGray))
}
}
AnimatedContent
AnimatedContentScope is an AnimatedVisibilityScope, so the modifier works there too. Use EnterTransition.None togetherWith ExitTransition.None and let Kapow do the work.
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.jantursky.kapow.KapowEnter
import com.jantursky.kapow.KapowExit
import com.jantursky.kapow.kapowTransition
@Composable
fun Pager(page: Int) {
AnimatedContent(
targetState = page,
transitionSpec = { EnterTransition.None togetherWith ExitTransition.None },
) { target ->
BasicText(
text = "Page $target",
modifier = Modifier.kapowTransition(this, enter = KapowEnter.wipe(), exit = KapowExit.wipe()),
)
}
}
Combining with stock transitions
Kapow runs next to Compose’s own transitions. Give the container a stock transition and add Kapow’s on the content, or use the stockEnter and stockExit parameters of KapowAnimatedVisibility.
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.background
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.graphics.Color
import androidx.compose.ui.unit.dp
import com.jantursky.kapow.KapowAnimatedVisibility
import com.jantursky.kapow.KapowEnter
import com.jantursky.kapow.KapowExit
@Composable
fun Banner(visible: Boolean) {
KapowAnimatedVisibility(
visible = visible,
enter = KapowEnter.glare(),
exit = KapowExit.glare(),
stockEnter = fadeIn() + expandVertically(),
stockExit = fadeOut() + shrinkVertically(),
) {
Box(Modifier.size(280.dp, 64.dp).background(Color.DarkGray))
}
}
The stock transitions and Kapow’s keep their own durations. The container waits for the longest one.
Durations
Each factory has a duration. Masks default to 450 ms, poof to 400 ms, pop to 1200 ms and anvil to 1400 ms. pop and anvil include their particles, so they run longer. tear defaults to 500 ms; dissolve, shapeIris, checker and blob to 600 ms; shatter to 900 ms; and disintegrate to 1500 ms.
If visibility flips back before a transition has finished, Compose interrupts it with a short spring instead of the transition’s own timing, as it does for stock transitions. The mask or movement then completes quickly from where it was.
Delay, easing and reduced motion
Every KapowEnter and KapowExit takes three more wrappers. Each returns a new transition.
| Wrapper | What it does |
|---|---|
.delay(delay) |
Starts the transition delay after the content becomes visible (enter) or is asked to go (exit). On enter the content stays hidden until then. On exit it stays fully shown. |
.easing(easing) |
Shapes the progress with easing instead of the transition’s own curve. Transitions with their own motion, such as anvil, pop, poof, shatter and disintegrate, stretch their whole timeline with it. |
.reducedMotion(mode) |
Chooses the reduced-motion behavior. See Reduced motion. |
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.foundation.background
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.graphics.Color
import androidx.compose.ui.unit.dp
import com.jantursky.kapow.KapowAnimatedVisibility
import com.jantursky.kapow.KapowEnter
import com.jantursky.kapow.KapowExit
import com.jantursky.kapow.ReducedMotionMode
import kotlin.time.Duration.Companion.milliseconds
@Composable
fun Sheet(visible: Boolean) {
KapowAnimatedVisibility(
visible = visible,
enter = KapowEnter.curtain().delay(100.milliseconds).easing(FastOutSlowInEasing),
exit = KapowExit.curtain().reducedMotion(ReducedMotionMode.Nothing),
) {
Box(Modifier.size(200.dp).background(Color.DarkGray))
}
}
Stagger
KapowStagger.delay(index) gives each item of a list a delay that grows with its position. Pass it to .delay(...) on the enter and exit of each row so the list fills in and empties one row after another. See Stagger.
Feedback
Both kapowTransition and KapowAnimatedVisibility take feedback = KapowFeedbackEvent.... The event is sent when the content starts to enter or exit. See Feedback.
Reduced motion
With reduced motion every Kapow transition becomes a fade over the same duration or shorter. .reducedMotion(...) changes that: Default and Fade fade, while Nothing and EndState show or remove the content at once, without a delay. See Reduced motion.
Particles in transitions
poof, anvil, pop and disintegrate emit particles. They draw through a KapowParticleLayer when there is one above the content. Without one, they draw around the content, and the container’s own bounds can clip them.