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

9. Animate Scroll Gestures
Written by Filip Babić

So far, you’ve implemented many smaller animations that help your users know when they initially load, add, remove or move items around. Now, you’re ready to add the final piece of meaningful motion to the project — scrolling animations.

In this chapter, you’ll focus on:

  • Setting up scroll listeners.
  • Reading scroll gestures and the amount scrolled.
  • Updating the UI when scrolling.

You’ll use various APIs in this chapter, including RecyclerView.OnScrollListener, CoordinatorLayout and CollapsingToolbarLayout.

Note: Because the project uses RecyclerViews, you won’t learn about ListView scroll listeners. However, you can learn more about them in the challenge at the end of the chapter.

Scrolling animations will add a new dimension of usability to your app. Now, it’s time to jump in!

Getting Started

This chapter uses some pre-baked UI code to make its setup easier, so be sure to start building it from the starter project. It’s located in aat-materials/09-animate-scroll-gestures.

Once the project syncs, build and run. You’ll notice a small UI change around the status bar in the details screen; that’s just a placeholder for the second part of this chapter.

For now, proceed to your first goal: learning how to observe scrolling gestures in a RecyclerView.

Reading RecyclerView’s scroll state

The first thing you’ll do is add listeners to FavoriteMoviesFragment and PopularMoviesFragment — specifically, the RecyclerView lists.

To do that, you have to utilize the RecyclerView.OnScrollListener API.

Open PopularMoviesFragment.kt. Navigate to onViewCreated() and add the following piece of code inside apply() at the bottom, where you set up the popularMoviesList:

addOnScrollListener(object : RecyclerView.OnScrollListener() {
})

Here, you use addOnScrollListener() to add a new OnScrollListener to popularMoviesList. This allows you to start listening to scroll events and to observe the scroll state.

Now, add the following function inside addOnScrollListener() to start observing the scroll state:

override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
  super.onScrollStateChanged(recyclerView, newState)
}

onScrollStateChanged() notifies you any time the RecyclerView starts or stops scrolling.

You receive two parameters in the function:

  • recyclerView: The RecyclerView component you’re observing for scrolling gestures.
  • newState: The new scrolling state of the list. The options are SCROLL_STATE_IDLE, SCROLL_STATE_DRAGGING or SCROLL_STATE_SETTLING, which represent states where the list is not moving, moving or finishing a scroll animation, respectively.

Now that you have the listener set up, you’ll hook up the UI to make it change based on the scroll state.

Updating UI based on the scroll position

The observed scrolling state in newState will be used to determine what action to take to update the UI. Now that you’re observing the scrolling state, add the following code to the function:

if (newState != RecyclerView.SCROLL_STATE_IDLE) {
  binding.scrollUp.hide()
} else {
  binding.scrollUp.show()
}

Using this small piece of code, you’re reading newState and either showing or hiding a FloatingActionButton on the screen. This new button is pre-baked for you in the project. You’ll use it to give the user an option to jump to the top of the list after they’ve scrolled down. By checking if the state is SCROLL_STATE_IDLE before hiding the button, you ensure the user sees it only when they stop scrolling.

Now that you’ve added this bit of code, open fragment_popular.xml and remove the visibility attribute from the FloatingActionButton. The line to remove looks like:

android:visibility="gone"

This is a very simple button that you’ll use to scroll to the top. Now, build and run. You’ll see the following screen:

Notice that the screen how has a scroll-up FloatingActionButton. Try scrolling up or down, and you’ll see that the FloatingActionButton uses a nice animation to hide itself while you’re scrolling:

You didn’t even need to do much to create that animation; you just called show() and hide(), and they took care of everything for you!

After the scrolling stops, you proceed to call show() — and the FAB appears again. Pretty sweet!

Note: If you wanted to listen to specific scroll changes and monitor the scroll position’s changes, you could also override onScrolled().

There’s an important problem, though — so far, the button doesn’t do anything when you tap it. You’ll change that next.

Animating the scroll-up FAB

The point of this FAB is to allow the user to scroll up to the start of the list. To add the functionality to the button, in PopularMovieFragment.kt, add the following to scrollUp’s setOnClickListener():

  binding.popularMoviesList.smoothScrollToPosition(0)

Using smoothScrollToPosition(), you tell the RecyclerView to move to a specific list item, based on its position in the list. Since you passed in 0 as the position, tapping the scrollUp button will bring you to the top of the list.

RecyclerView has internal handlers that update the list and animate the scroll to look as smooth as possible.

Now, build and run. Scroll away from the top, then tap the FAB. You’ll see the following behavior:

When you tap the button, the list immediately starts to scroll smoothly and moves you to the top of the list. Once you reach the top of the list, the FAB animates in again; it does this because of your previous scroll implementation.

Internally, RecyclerView uses the LayoutManager API to start the scroll. This shows you how cohesive and easy to use the entire List API is!

An alternative to this animation is to call recyclerView.scrollToPosition(), which will change the list scroll state but won’t animate the change.

Note: scrollUp works well for simple scroll animations. However, if you want your animations to be more specific, to change your UI in response to specific scroll amounts or to observe pixel-perfect changes when scrolling, you can use onScrolled() instead. You can learn more in the official onScrolled() documentation.

Now that you’ve added a few simple animations to your app’s scrolling and learned how to observe scrolling changes, you’re ready for the next step: using complex layouts that enable scrolling animations and transformations out of the box.

You’ll do that using CoordinatorLayout.

Building a CoordinatorLayout screen

When it comes to beautiful and meaningful animations for scrolling screens, CoordinatorLayout is a popular solution.

CoordinatorLayout is a complex layout that developers usually pair with elements like CollapsingToolbarLayouts and FloatingActionButtons. It supports complex scrolling transitions because it can propagate the scroll values to its children.

This results in built-in animations that require very little work — while being beautiful and meaningful to users.

You’ll implement this in MovieDetailsFragment by animating the movie backdrop to have a parallax scroll animation. You’ll also read the scroll value and calculate the scroll percent to apply different UI changes to the screen.

You’ll start by implementing CollapsingToolbarLayout.

Collapsing the toolbar

To kick off the CoordinatorLayout setup, open fragment_details.xml. You’ll notice AppBarLayout and CollapsingToolbarLayout at the top of the file:

<com.google.android.material.appbar.AppBarLayout
  android:id="@+id/app_bar"
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:fitsSystemWindows="true">

  <com.google.android.material.appbar.CollapsingToolbarLayout
    android:id="@+id/toolbar_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    app:contentScrim="?attr/colorPrimary"
    app:layout_scrollFlags="scroll|exitUntilCollapsed"
    app:toolbarId="@+id/toolbar">

  </com.google.android.material.appbar.CollapsingToolbarLayout>

</com.google.android.material.appbar.AppBarLayout>

This will be the core of the layout for the scrolling animations in the CoordinatorLayout parent. AppBarLayout lets you build a top bar that reacts to scrolling gestures.

CollapsingToolbarLayout is a special type of a Toolbar that has three specific states:

  • Fully expanded: This is the default state. It represents the UI when the screen is at the very top and the user hasn’t scrolled yet.
  • Scrolled: A middle state where the user has scrolled a certain amount, but the Toolbar has neither expanded nor collapsed fully.
  • Fully collapsed: This is the state where the user has scrolled enough to fully collapse the Toolbar. This is an alternate state of the UI, which usually shows a part of the fully expanded UI.

Start by moving the ImageView with id of backdrop inside CollapsingToolbarLayout:

<com.google.android.material.appbar.CollapsingToolbarLayout
  android:id="@+id/toolbar_layout"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:fitsSystemWindows="true"
  app:contentScrim="?attr/colorPrimary"
  app:layout_scrollFlags="scroll|exitUntilCollapsed"
  app:toolbarId="@+id/toolbar">

  <ImageView
    android:id="@+id/backdrop"
    android:layout_width="match_parent"
    android:layout_height="300dp"
    android:scaleType="centerCrop" />

</com.google.android.material.appbar.CollapsingToolbarLayout>

Also, make sure to completely remove the MaterialCardView with the id of surface; you won’t need it anymore. The code to remove looks like:

<!-- Remove this element -->
<com.google.android.material.card.MaterialCardView
  android:id="@+id/surface"
  android:layout_width="match_parent"
  android:layout_height="600dp"
  app:cardCornerRadius="12dp"
  app:layout_constraintBottom_toBottomOf="parent"
  app:layout_constraintEnd_toEndOf="parent"
  app:layout_constraintStart_toStartOf="parent"
  app:layout_constraintTop_toBottomOf="@id/backdrop" />

Now that you have backdrop as the expanded part of the CollapsingToolbarLayout, you need to add a regular Toolbar to represent the collapsed state.

Add the following element beneath the ImageView with the id of backdrop:

<Toolbar
  android:id="@+id/toolbar"
  android:layout_width="match_parent"
  android:layout_height="?attr/actionBarSize"
  app:layout_collapseMode="pin"/>
</com.google.android.material.appbar.CollapsingToolbarLayout>

By adding Toolbar, you can represent some content — usually a title and action buttons — when the user scrolls away.

Notice the layout_collapseMode attributes on both the backdrop and the toolbar. These attributes describe how the elements should scroll and collapse in CoordinatorLayout.

There are three types of collapseModes:

  • parallax: Use this collapse mode images. It’s a well-known effect when the image moves inside the component as you scroll, causing a parallel scroll effect. It will create a nice and cool animation.

  • pin: Use this collapse mode whenever you have elements that stick around when the CollapsingToolbarLayout fully collapses after a scroll. For example, you used it to keep the Toolbar around when the user scrolls away from the top.

  • none: The default collapse mode, which makes the item go away completely as you scroll away from the top.

Finally, make sure to change the ScrollView to a NestedScrollView. Replace the opening ScrollView tag with:

<androidx.core.widget.NestedScrollView
  android:id="@+id/scrollView"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
>

then replace the close tag, </ScrollView>, with:

</androidx.core.widget.NestedScrollView>

This is important; without it, CoordinatorLayout cannot process the scrolling and collapsing gestures. Also, notice the layout_behavior attribute — it helps CoordinatorLayout place the scrolling content of the screen under AppBarLayout, no matter how big the app bar is.

It’s important to add this because CoordinatorLayout doesn’t support ordered item placement. The way it works is similar to FrameLayout. It’s time to render the UI!

Rendering the UI with Coordinator Layout

Now that you’ve set up the basic CoordinatorLayout and its children, open MovieDetailsFragment.kt add the following to the start of renderUi():

binding.toolbar.title = movie.title

Here, you’re adding a title to the toolbar, which is based on the movie.title. Next, remove the line of code:

transformations(BlurTransformation(requireContext()))

By removing the blur transformation, you’ll show the title when the Toolbar collapses, and the backdrop will be clear to represent the movie image.

Now, build and run. Open the details screen to see the following behavior:

As you scroll through the details screen, the Toolbar collapses and the image moves with the scrolling gesture. Finally, when you reach the top, the image and the Toolbar crossfade so the movie title is visible.

If you try scrolling back, you’ll see the animation reverse itself. This is a simple yet satisfying animation that you built without writing any animation code! How cool is that?

This animation is great for all screens where you have a lot of options or information to show to the users. You can show some information in the Toolbar, which you can collapse to show other UI components when the user scrolls away.

You can also react when the Toolbar scrolls or collapses. You’ll add that feature next!

Reading CollapsingToolbarLayout scroll state

Now that you’ve added the basic CollapsingToolbarLayout implementation, it’s time to improve the user experience and add more meaningful motion to the screen.

The idea is to change the UI state as you scroll, making the transition a bit nicer. As the user scrolls, you’ll increase the size of the poster using scaleX and scaleY. This will give users a nicer look at the movie poster, while transitioning them into the movie’s details.

To do that, you first need to read CollapsingToolbarLayout’s scroll state.

Open MovieDetailsFragment.kt. Now, add the following code inside setupScrolling():

binding.appBar.addOnOffsetChangedListener(AppBarLayout.OnOffsetChangedListener { appBarLayout, verticalOffset ->
    // Update UI
  })

addOnOffsetChangedListener() allows you to bind a listener to the appBar, which gives you updates whenever the vertical scroll offset changes. This means you can react to user scroll gestures and tell how much the appBar has collapsed.

You pass in an AppBarLayout.OnOffsetChangedListener implementation, which gives you access to two properties:

  • appBarLayout: The appBar that’s being scrolled and collapsed.
  • verticalOffset: How far the appBar has already collapsed, in pixels.

Using these two properties, you can figure out what percentage of the CollapsingToolbarLayout has collapsed and react by applying different UI changes to the rest of your UI.

You’ll do that next.

Calculating how much the toolbar has collapsed

To calculate the collapse percentage, add the following code inside the listener you just added:

val scrollRange = appBarLayout.totalScrollRange.toFloat()
val scrollPercent = abs(verticalOffset / scrollRange)

Using appBarLayout, you can calculate the totalScrollRange of the CollapsingToolbarLayout — in other words, how much you have to scroll to fully collapse the Toolbar. Once you have that, you can calculate the scrollPercent by dividing the verticalOffset by the scrollRange.

Next, below the code you just added, add the logic to increase the scale of the movie poster:

val scale = (1 + scrollPercent)
binding.posterContainer.scaleX = scale
binding.posterContainer.scaleY = scale

By adding up 1 and scrollPercent, you’ll change the scale of the movie poster from 1.0 to 2.0, making it twice the scale when you fully collapse the toolbar. Once you apply that scale to the posterContainer, it’ll change in size as you scroll.

As you scroll down to the movie details, the poster scales up. This gives the details screen a nice look and feel — you get to see the poster while you browse through the information. Then, as you go back, the poster scales down and the toolbar expands, giving you a nice transition effect.

Your animations are really starting to shape up, but there are still a few points where you can polish them even more. For example, when the poster scales, it covers up the movie rating.

To fix this, you’ll move the movie rating to the header when the toolbar collapses.

Adding custom Toolbar content

To improve the animation behavior, open fragment_details.xml.

Head to the CollapsingToolbarLayout element and add the following code underneath the Toolbar:

<RelativeLayout
  android:id="@+id/ratingContainer"
  android:layout_width="match_parent"
  android:alpha="0"
  android:layout_height="?attr/actionBarSize"
  android:layout_gravity="bottom|end|center_vertical"
  app:layout_collapseMode="pin"
  app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">

</RelativeLayout>

Here, you used a small RelativeLayout to represent the rating information in the collapsing toolbar.

Now, remove the movieRating and ratingValue elements from the XML file. Then, add the following code within the ratingContainer:

<RatingBar
  android:id="@+id/movieRating"
  style="@style/Widget.AppCompat.RatingBar.Small"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:layout_centerVertical="true"
  android:layout_margin="16dp"
  android:layout_toStartOf="@id/ratingValue"
  android:elevation="4dp"
  android:isIndicator="true"
  android:numStars="5"
  android:progressTint="@color/colorRating"
  android:rating="3.5"
  android:scaleX="1.5"
  android:scaleY="1.5" />

<TextView
  android:id="@+id/ratingValue"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:layout_alignParentEnd="true"
  android:layout_centerVertical="true"
  android:layout_marginStart="16dp"
  android:layout_marginEnd="16dp"
  android:elevation="4dp"
  android:fontFamily="@font/rubik_one"
  android:letterSpacing="-0.1"
  android:textColor="?colorOnPrimary"
  android:textSize="32sp"
  android:textStyle="bold"
  tools:text="4.2" />

These two elements are similar to what you used before, just written in a way that works with RelativeLayout.

Now, also make sure to update the elevation in posterContainer to 24dp so it looks like:

android:elevation="24dp"

This increases the elevation of posterContainer. Then update app:layout_constraintTop_toBottomOf in overviewHeader to @id/posterContainer:

  app:layout_constraintTop_toBottomOf="@id/posterContainer" 

This will fix the constraints after you moved ratingValue and movieRating into ratingContainer.

Now that you’ve fixed the constraints and moved the rating information to the collapsible header, it’s time to make the container fade in as you scroll.

Adding a fade-in animation to the container

Open MovieDetailsFragment.kt and navigate to setupScrolling().

Add the following code at the end of the offset listener:

binding.ratingContainer.alpha = scrollPercent

Using this code, you’ll slowly fade in the rating information. This will prevent the rating information from covering up the image when the Toolbar is expanded.

Now, build and run. You’ll see the movie rating slowly fade in as you scroll and collapse the toolbar.

In the meantime, the poster will scale up and take over more space so you can still look at the image of the movie when you scroll.

Improving the poster scale animation

Now that you’ve set up the rating, it’s time to make the poster scale animation even nicer. Right now, it covers the rest of the UI as it scales up. Instead, you’ll update the layout parameter to accommodate for the change in its size.

Find the following properties at the top of the class:

private var originalWidth by Delegates.notNull<Int>()
private var originalHeight by Delegates.notNull<Int>()

and replace them with:

private var originalWidth: Int = 0
private var originalHeight: Int = 0

These two properties represent posterContainer‘s original width and height. You’ll use them to remember what the original size was and to scale up from there as you scroll.

Now, add the following code at the bottom of onViewCreated():

binding.posterContainer.doOnLayout {
  originalWidth = it.width
  originalHeight = it.height
}

Using doOnLayout(), you tell the View to perform an action when the system lays out the movie poster and draws it. This way, you can get the correct size of the container. You’ll use that value to scale the container as you scroll.

Next, to apply the animation to posterContainer as you scroll and collapse the Toolbar, remove the following lines from setupScrolling():

binding.posterContainer.scaleX = scale
binding.posterContainer.scaleY = scale

and replace them with the following:

binding.posterContainer.updateLayoutParams {
  this.width = (scale * originalWidth).toInt()
  this.height = (scale * originalHeight).toInt()
}

Instead of applying a scaleX or scaleY animation, you now change its width and height. You used updateLayoutParams() and the originalWidth and originalHeight properties to do this.

This function lets you update and change the parameters that calculate the size of the View. Using scale * originalWidth and setting the scale to (1 + scrollPercent), you change the size of the posterContainer from a scale of 1.0 to a scale of 2.0. This is similar to what you did before when calculating the scale.

The difference is that, this time, the size changes instead of the scale. The result is that the View won’t cover up other elements.

Removing the empty space.

Build and run. At this point, you might notice that there’s quite a bit of empty space at the top of the poster. You’ll fix this next.

Open fragment_details.xml and change the posterContainer’s marginTop to 32dp:

android:layout_marginTop="32dp" 

With this change, you’ll see there’s not so much empty space at the top of the poster.

Build and run one final time. You’ll see the poster increase in size.

Congratulations, everything works smoothly now!

This is now a much better animation. There’s no item overlap, and everything looks cohesive and clean! If you start scrolling back and expanding the Toolbar, posterContainer will reduce in size and slowly go back to the 1.0 scale — its original size.

The animation will look like this:

Pretty cool… and all with just a few lines of code! You’re now ready to apply beautiful and simple animations to lists and scrollable screens in your apps. :]

Challenges

Challenge 1: Build a ListView and its scroll listener

In this challenge, you’ll learn how to use the ListView API to achieve the same scrolling behavior as with the RecyclerView.

You need to implement a ListView and an adapter as a BaseAdapter. Once you do that, make sure to fill the adapter with the movie data and to connect an OnScrollListener to it. That performs the same FAB show-and-hide logic.

Check out the challenge projects to find the solution!

Challenge 2: Use scroll listeners to show and hide the FAB

Try to implement extra logic for the FloatingActionButton to show it only when the list is scrolled away from the top.

To do this, use RecyclerView.LayoutManager.findFirstVisibleItemPosition() to find what the first visible item is. Then, pair it up with onScrollStateChanged() to check if the user is scrolling or if they’re at the top of the list, to hide the FAB.

If you check the challenge project, you’ll find the ListView implementation. It’s based off the first challenge, but the logic is the same!

Key points

  • When using ScrollViews, you can control scrolling gestures and animations using the ListView’s OnScrollListener, the RecyclerView.OnScrollListener, a CoordinatorLayout and a View.OnScrollChangeListener.
  • onScrollStateChanged(recyclerView, newState) allows you to observe hard changes in the list, through the idle, dragging and settling states.
  • Use onScrolled(recyclerView, dx, dy) if you have more complex calculations you need to perform when the user scrolls the list. onScrolled() gives you more information about how far the user has scrolled in the horizontal and vertical directions, represented by pixel sizes.
  • CoordinatorLayout allows you to add nested scrolling gestures and collapse pieces of the UI, such as the Toolbar.
  • Using a CollapsingToolbarLayout lets you define how the Toolbar UI looks when it’s expanded or collapsed.
  • Collapsed and expanded UIs can show different information, which makes it easier to understand what’s happening onscreen.
  • Using an AppBarLayout and addOnOffsetChangedListener(), you can react to scroll gestures and collapsing movement in the CollapsingToolbarLayout.
  • AppBarLayout.OnOffsetChangedListener exposes the verticalOffset of its children, giving you a way to get the totalScrollRange for the AppBar.
  • Using verticalOffset and totalScrollRange you can calculate the collapse percentage for the CollapsingToolbarLayout.
  • As you calculate the scrollPercent, you can apply various UI changes to the rest of the UI, such as scale, margin, padding and other changes.
  • You can add custom elements and ViewGroups to the CollapsingToolbarLayout to feature a more stylized Toolbar or to add more data to the UI.
  • The Toolbar can do more than just show a title. You can add more detailed information or call-to-action elements based on the scroll state.
  • Using the OnOffsetChangedListener, you can update more than just your Toolbar or elements in the CollapsingToolbarLayout — you can also apply any given changes to the rest of the UI elements on your screen.

Where to go from here?

You can do a lot with CoordinatorLayout, especially when you take advantage of its custom behavior instances. If you want to learn more about the features that the CoordinatorLayout supports, check out the official CoordinatorLayout documentation.

You can also check out the CoordinatorLayout.Behavior documentation if you need to customize what happens to elements and how they move around when you scroll.

Finally, make sure to check out the official Android Developers’ blog post about intercepting events with the CoordinatorLayout.

Now that you’ve learned all about various scrolling and list item animations, you’re ready to serve data to your users with a bit of style! List and scrolling animations are the most common type of animations in apps, because a core feature of most apps is presenting specific types of data and allowing different actions based on that data.

This can be anything from creating, reading, updating and deleting data — also known as CRUD operations — or simply browsing through catered content such as movies, user and news posts, images and videos.

Whatever your use case, your users are likely to scroll through a bunch of data and apply some operations to it, so be sure to arm your apps with meaningful scrolling and list item animations to make the experience more enjoyable.

If you’re looking for inspiration for what you can do with list items and scrolling, be sure to check out Dribble. It offers many examples of animations and meaningful motion, especially in modern apps!

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.
© 2026 Kodeco Inc.