Chapters

Hide chapters

Android Animations by Tutorials

First Edition · Android 12 · Kotlin 1.5 · Android Studio Artic Fox

Section II: Screen Transitions

Section 2: 3 chapters
Show chapters Hide chapters

10. Jetpack Compose Animations
Written by Prateek Prasad

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

So far in this book, you’ve worked on animating views and screens based on the UI toolkit. However, now that Jetpack Compose is gaining in popularity, more and more apps will start migrating to it, so it’s a good idea to know how to animate those apps.

Jetpack Compose offers a host of modern features when building UIs, and it makes things like state management a lot simpler. In this chapter, you’ll learn about animations in Jetpack Compose.

Setting up the project

Open the starter project for this chapter in Android Studio. Build and run. You’ll notice that everything looks the same as in the previous chapter.

The difference lies in the project’s code.

Expand the project structure, and you’ll notice a new package named ui:

The UI package contains three packages:

  • components: Contains the components built using Jetpack Compose.
  • screen: Contains the screens built using Jetpack Compose and the components mentioned above.
  • theme: Contains the color and typography definition used to build the theme for the app.

The rest of the core architecture of the app is still the same, down to the UI scaffold. The three screens of your app still use fragments, but the fragments host composable functions instead of inflating an XML.

With that out of the way, you’ll now dive in and add some sweet animations to this app.

Animating visibility changes

When you open a movie’s details in the app’s current form, you’ll notice that the Cast section snaps into existence as soon as it’s done loading — which feels quite janky.


val visibleState = remember {
  MutableTransitionState(initialState = false).apply {
    targetState = true
  }
}
AnimatedVisibility(
  visibleState = visibileState,
  enter = fadeIn()
) {
  LazyRow(contentPadding = PaddingValues(end = 24.dp)) {
    items(it) {
      CastItem(it.profilePath)
    }
  }
}
@ExperimentalAnimationApi
@Composable
fun CastRow(cast: List<Cast>?) {
	...
}

Adding a slide-in animation

To make your animation a little more exciting, you’ll now introduce a slide-in animation when the cast row appears.

enter = fadeIn() + slideInVertically()

Animating content sizes

In the app, a few of the movies have lengthy overviews. Unfortunately, these movies’ overviews take up so much space that they push the cast row and the Add to Favorites button off the screen.

Hiding and showing long text

Create a new file named Overview.kt in the components package and create a new composable function, Overview, that takes in a movie object.

@Composable
fun Overview(movie: Movie) {
}
Overview(movie = movie)
@Composable
fun Overview(movie: Movie) {
    Text(
      text = movie.overview,
      style = MaterialTheme.typography.body2,
      textAlign = TextAlign.Start,
      modifier = Modifier.padding(horizontal = 16.dp),
    )
}
var overviewExpanded by remember { mutableStateOf(false) }
//1
if (movie.overview.length > 200) {
  Text(
    //2
    text = if (overviewExpanded) "READ LESS" else "READ MORE",
    style = MaterialTheme.typography.overline,
    modifier = Modifier
      .padding(24.dp)
      .clickable {
        //3
        overviewExpanded = !overviewExpanded
      },
  )
}

Text(
  text = movie.overview,
  style = MaterialTheme.typography.body2,
  textAlign = TextAlign.Start,
  modifier = Modifier.padding(horizontal = 16.dp),
  maxLines = if (overviewExpanded) Int.MAX_VALUE else 4
)

Animating the change in the text

Wrap both Text composables inside a Column, as shown below, to animate the text:

@Composable
fun Overview(movie: Movie) {
  var overviewExpanded by remember { mutableStateOf(false) }

  Column(
    modifier = Modifier.animateContentSize(),
    verticalArrangement = Arrangement.Top,
    horizontalAlignment = Alignment.CenterHorizontally
  ) {
    Text(
      text = movie.overview,
      style = MaterialTheme.typography.body2,
      textAlign = TextAlign.Start,
      modifier = Modifier.padding(horizontal = 16.dp),
      maxLines = if (overviewExpanded) Int.MAX_VALUE else 4
    )
    if (movie.overview.length > 200) {
      Text(
        text = if (overviewExpanded) "READ LESS" 
               else "READ MORE",
        style = MaterialTheme.typography.overline,
        modifier = Modifier
          .padding(24.dp)
          .clickable {
            overviewExpanded = !overviewExpanded
          },
      )
    }
  }
}

Animating state changes

In the details screen of any movie, tapping the Add to Favorites button will bring up a circular progress bar that displays while the operation is in progress:

Giving the button a state

To use updateTransition(), the button needs a state of its own. Open AddToFavoritesButton.kt and add the following enum to the top of the file:

enum class ButtonState {
  IDLE, PRESSED
}
//1
val buttonState = remember { mutableStateOf(ButtonState.IDLE) }

//2
val transition = updateTransition(buttonState.value, "Button Transition")

//3
val width = transition.animateDp(label = "Button width animation") { state ->
  when (state) {
    ButtonState.IDLE -> 250.dp
    ButtonState.PRESSED -> 56.dp
  }
}

Toggling the button’s state

Now that you’ve set up a state for the button, you need to add a mechanism for toggling that state. To do that, you’ll use contentState, which MovieDetails observes and passes down as a property.

buttonState.value = if (contentState is Events.Loading) {
  ButtonState.PRESSED
} else ButtonState.IDLE

Animating the button

First, get rid of the check that renders theCircularProgressIndicator when contentState is Loading. Remove the if statement starting with the following including the else portion, all the way to the } for the else:

if (contentState is Events.Loading) {
      CircularProgressIndicator(
        modifier = Modifier.padding(top = 8.dp),
        strokeWidth = 2.5.dp,
        color = Color.Black
      )
    } else {
    ...
    }
Button(
  modifier = Modifier
    .size(250.dp, 56.dp),
  shape = RoundedCornerShape(32.dp),
  colors = ButtonDefaults.buttonColors(
    backgroundColor = MaterialTheme.colors.secondary
  ),
  onClick = { onFavoriteButtonClick(movie) },
) {
  Row(verticalAlignment = Alignment.CenterVertically) {
    if (buttonState.value == ButtonState.PRESSED) {
      CircularProgressIndicator(
        modifier = Modifier.padding(top = 8.dp),
        strokeWidth = 2.5.dp,
        color = Color.Black
      )
    } else {
      Icon(
          imageVector = if (movie.isFavorite) {
              Icons.Default.Favorite
          } else {
              Icons.Default.FavoriteBorder
          },
          contentDescription = null
      )
      Spacer(modifier = Modifier.width(16.dp))
      Text(
          text = if (movie.isFavorite) {
              stringResource(
                  id = R.string.remove_from_favorites
              )
          } else {
              stringResource(
                  id = R.string.add_to_favorites
              )
          },
          style = MaterialTheme.typography.button,
          maxLines = 1
      )
    }
  }
}

Changing the size of the button

There’s one final thing to sort out before the animation is ready: To make the button shrink and grow, you need to use the width property you created earlier.

Button(
  modifier = Modifier
    .size(width.value, 56.dp),
  shape = RoundedCornerShape(32.dp),
  colors = ButtonDefaults.buttonColors(
    backgroundColor = MaterialTheme.colors.secondary
  ),
  onClick = { onFavoriteButtonClick(movie) },
) {

	...
}

Challenge: Animating the button color

You animated the button width using the animateDp() extension available for the Transition in the button animation.

Key points

  • Jetpack Compose introduces a comparatively simple set of APIs to add animations to your app.
  • AnimatedVisibility lets you animate the visibility changes of a composable.
  • AnimatedVisibility is still an experimental API at the time of this writing, so composables using this need the @ExperimentalAnimationApi annotation.
  • To animate content size changes, use animateContentSize() on the parent container of a composable.
  • To trigger based on state changes in your app, use updateTransition().
  • Transition has several convenient extensions. For example, animateDp and animateSize let you animate properties of a composable across state changes.

Where to go from here?

This chapter provided an introduction to the Jetpack Compose animations API. While you covered some of the simple use cases, you barely scratched the surface of what Jetpack Compose offers for animations.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now