Skillversitas

Animation and Transitions

Kivy's animation system moves widget properties over time: an Animation is a set of property targets with a duration and a timing function, and it runs on the render loop, easing the values frame by frame. The uses are the standard motion vocabulary - a button that scales on press, a panel that slides in, a label that fades, a screen transition - and the system is compact enough that a full interaction is a few lines. This chapter covers the Animation object, the timing functions, chaining with ParallelAnimation and SequentialAnimation, the built-in widget animations, and the screen transitions that tie it to navigation.

The Animation Object

from kivy.animation import Animation
from kivy.uix.button import Button

btn = Button(text="Tap", size_hint=(0.4, 0.2))

# an Animation is {property: target_value, duration, ...}
pop = Animation(size=(btn.width * 1.15, btn.height * 1.15),
                duration=0.1)
pop_back = Animation(size=(btn.width, btn.height), duration=0.1)

def on_press(instance):
    # chain: grow fast, then shrink back
    pop.bind(on_complete=lambda *a: pop_back.start(instance))
    pop.start(instance)

btn.bind(on_press=on_press)

# fade: opacity is a property, so it animates like any other
fade = Animation(opacity=0, duration=0.4)
fade.bind(on_complete=lambda *a: fade_back.start(btn))
fade_back = Animation(opacity=1, duration=0.4)

The shape: Animation(prop=value, duration=..., ...) names the target properties and how long the move takes, and .start(widget) begins it. The timing is eased by a animation timing function (the kivy.animation.in_out_sine family), so the motion is not linear - the default ease is usually what you want. The complete event (on_complete) is the hook for chaining: grow, then on complete, shrink back. Because opacity, pos, size, rotation, and scale are all properties, they all animate with the same object - one mechanism for the whole motion vocabulary. The two-value pattern (animate out, bind on_complete to animate back) is the basis of every pulse, slide-in, and fade-through in the toolkit.

Composing: Parallel and Sequential

from kivy.animation import Animation, ParallelAnimation, SequentialAnimation

# parallel: run several property animations AT THE SAME TIME
slide_in = ParallelAnimation(
    Animation(pos_x=parent.x + 200, duration=0.3),
    Animation(opacity=1, duration=0.3),
)

# sequential: run one AFTER the other
bounce = SequentialAnimation(
    Animation(y=widget.y + 40, duration=0.15),
    Animation(y=widget.y, duration=0.15),
)

# the | operator is shorthand for sequential, & for parallel:
# slide_in = Animation(pos_x=..., duration=0.3) & Animation(opacity=1, duration=0.3)
# bounce   = Animation(y=..., duration=0.15) | Animation(y=..., duration=0.15)

slide_in.start(panel)
bounce.start(ball)

Two combinators build compound motion. ParallelAnimation (or the & operator) runs its children simultaneously - a slide and a fade at once is a single parallel animation. SequentialAnimation (or the | operator) runs them in order - the bounce is up then down as one object. This is how a "drop-in" is expressed (parallel: y from above and opacity in) and a "pulse" (sequential: scale up, scale down). The operators keep the expressions compact and readable, and both combinators support the same on_complete chaining, so a complex sequence - slide in, pause, highlight - is a tree of animations with a complete hook at the end. The rule: compose with the combinators rather than hand-chaining on_complete callbacks, because the tree is cancellable as a unit and the timing is declared, not implicit in the callback order.

The Built-in Widget Animations

Button:
    # the standard press animation: scale down while held
    animation_in: Animation(scale=0.9, duration=0.1)
    animation_out: Animation(scale=1, duration=0.1)

# most interactive widgets ship these; the properties are:
#   animation_in  - plays on press / focus
#   animation_out - plays on release / blur
# and the widget's normal state is the "rest" state between them

The interactive widgets (Button, CheckBox, the toggle family) carry animation_in and animation_out properties that play on press and release - the press-down scale is the default, and it is customizable per widget by setting those two properties in the rule. This is the "feel" layer: the default animations are the reason a Kivy button feels responsive, and tuning them (a faster in, a softer out) is how you match the app's motion language. The pattern generalizes - any widget that exposes an animation_* property is inviting you to define its motion, and the rest of the UI (the custom panels, the screens) supplies its own animations with the same Animation objects.

Screen Transitions

from kivy.uix.screenmanager import ScreenManager, Screen, SlideTransition
from kivy.lang import Builder

Builder.load_string("""
ScreenManager:
    SlideTransition()
    ListScreen:
    DetailScreen:
""")

class AppScreens(ScreenManager):
    def __init__(self, **kw):
        super().__init__(**kw)
        # the transition is how one screen leaves and the next enters
        # SlideTransition, FadeTransition, RiseInTransition, NoTransition
        pass

The ScreenManager (the next chapter's subject) takes a TransitionBase as its transition - the animation that plays when the current screen is swapped for the next. The built-ins are SlideTransition (the default horizontal slide), FadeTransition, RiseInTransition, Rotate3DTransition, and NoTransition - each a pre-built Animation over the two screens' positions and opacity. Setting the transition is one line in the ScreenManager rule, and it is the motion language of the whole app's navigation: the same slide on every screen change reads as a consistent, deliberate UX. A custom transition is a TransitionBase subclass with the same Animation tools, for when the app's motion identity needs something the built-ins do not cover.

The Working Rules

  • Animate properties, not pixels. The system eases property values; pos, size, opacity, scale, rotation are the motion vocabulary, and they are all properties.
  • Compose with the combinators. & for simultaneous, | for sequential - the tree is cancellable and the timing is declared.
  • Keep it short. UI motion in the 0.1-0.4s range reads as responsive; longer feels laggy. The press animations are ~0.1s, the screen transitions ~0.3s.
  • Clean up on completion. An on_complete that starts the return animation is the pattern; a fire-and-forget animation that leaves the property at its target is usually the bug (the button stuck at 1.15x).
  • Respect the reduce-motion preference where your users have one - the transitions are the place to swap to NoTransition.

Summary

An Animation is {property: target, duration, timing} started with .start(widget), and because the motion properties (pos, size, opacity, scale, rotation) are all properties, one object drives the whole vocabulary. Chain with on_complete, compose with ParallelAnimation/& (simultaneous) and SequentialAnimation/| (in order), and use the widgets' built-in animation_in/animation_out for the press feel. At the navigation level, the ScreenManager takes a transition (SlideTransition, FadeTransition, a custom TransitionBase) that is the app's motion identity for screen changes. Keep it short (0.1-0.4s), always animate back to the rest state, and compose rather than hand-chain - and the motion layer becomes a deliberate, consistent part of the UI instead of an afterthought.