Animations & Transitions

A well-placed animation tells the user something happened without them having to think about it — a card that fades in feels like it arrived gently rather than snapping into existence, a photo that glides from one screen to the next feels like the same object rather than two unrelated images. Android gives you several layers of animation API, from a one-line convenience call all the way up to fully custom motion. This lesson walks through each, using TrailLog's trail detail screen and photo list as the running example.

ViewPropertyAnimator: The Simplest Way to Animate a View

For animating a View's own built-in properties — alpha, position, scale, rotation — ViewPropertyAnimator, reached through view.animate(), is the simplest and most efficient option. It batches multiple property changes into a single underlying animation pass. Here's TrailLog fading in a trail's detail card once its data has finished loading:

Fading in the trail detail card

fun showTrailDetailCard() {
    detailCard.alpha = 0f
    detailCard.visibility = View.VISIBLE
    detailCard.animate()
        .alpha(1f)
        .setDuration(300)
        .start()
}

The card starts fully transparent, is made visible, and then animate().alpha(1f) smoothly raises its opacity to fully opaque over 300 milliseconds. You can chain .translationY(), .scaleX()/.scaleY(), or .rotation() onto the same call to combine several effects into one animation.

ObjectAnimator and ValueAnimator: More Control

ViewPropertyAnimator only covers a View's own properties. When you need to animate something else entirely — a custom property on your own class, a color, a value you'll use to drive a manual invalidate() redraw — reach for ObjectAnimator (animates a named property on any object with the right getter/setter) or the lower-level ValueAnimator (just produces a stream of interpolated values you apply yourself):

Animating the difficulty badge's background color

val colorAnimator = ObjectAnimator.ofArgb(
    difficultyBadge,
    "badgeColor",
    Color.parseColor("#9E9E9E"),
    Color.parseColor("#4CAF50")
)
colorAnimator.duration = 400
colorAnimator.start()

This targets the badgeColor property on the custom TrailDifficultyBadge View from the previous lesson, interpolating between two ARGB colors and requiring the View to expose a matching setter that calls invalidate() to redraw on each step.

XML-Defined Animations in res/anim/

Simple, reusable animations can also live declaratively as XML resources under res/anim/, which keeps the animation's shape out of your Kotlin code entirely:

res/anim/fade_in.xml

<alpha xmlns:android="http://schemas.android.com/apk/res/android"
    android:fromAlpha="0.0"
    android:toAlpha="1.0"
    android:duration=300 />

Loading and starting it from Kotlin

val fadeIn = AnimationUtils.loadAnimation(context, R.anim.fade_in)
detailCard.startAnimation(fadeIn)

This produces the same visual result as the ViewPropertyAnimator example above — which approach to reach for is mostly a matter of taste, though XML animations are handy when designers hand you a spec that maps cleanly onto a resource file, or when the same animation is reused across many screens.

Shared Element Transitions: Carrying a View Between Screens

One of Android's most delightful, commonly-seen UI patterns is the shared element transition: tap a trail's thumbnail in a list, and its photo appears to smoothly grow into place as the detail screen's hero image, rather than the list simply vanishing and a new screen popping in. Both screens declare the same transitionName on the shared View, and the navigation call tells the system to animate between them:

Marking the shared photo in both layouts

<ImageView
    android:id="@+id/trailPhoto"
    android:transitionName="trail_photo_transition"
    android:layout_width="match_parent"
    android:layout_height="200dp" />

Launching the detail screen with the shared element

val options = ActivityOptionsCompat.makeSceneTransitionAnimation(
    this, trailPhotoView, "trail_photo_transition"
)
startActivity(intent, options.toBundle())

The Navigation Component wraps this same idea for Fragment-to-Fragment transitions — see Navigation Component for how shared elements plug into that setup specifically.

MotionLayout: For Complex, Multi-State Animated Layouts

ConstraintLayout's more powerful sibling, MotionLayout, is built for layouts that need to animate between several distinct states — think a collapsing header that also fades its title, resizes its image, and moves a floating action button, all in one coordinated motion driven by scroll position or a tap. It's a genuinely deep topic on its own with its own XML scene format; this lesson only asks you to know it exists and what kind of problem it solves, not to use it yet.

A Word Against Over-Animating

Every animation on this page has a job: the fade tells the user content has finished loading, the shared element transition confirms which photo they tapped. An animation that exists purely for decoration — a long bounce on every button press, a spinning icon nobody asked for — adds latency to the very interaction it's supposed to make feel nice. Keep durations short (roughly 150–300ms for most UI feedback), give the user a way to skip or interrupt long animations, and cut anything that doesn't clarify a transition or add real polish.

Note: These snippets are correct, real Kotlin/Android code, but this website's own project doesn't run a live Android Studio build, so none of the motion described here actually plays on this page.
Next up: Motion is one kind of polish; how your app looks in light and dark system themes is another. Theming & Dark Mode covers that next.