Home Android & Kotlin Books Android Animations by Tutorials

9
Animate Scroll Gestures Written by Filip Babić

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

You can unlock the rest of this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

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.

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

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()
}
android:visibility="gone"

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)

Building a CoordinatorLayout screen

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

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>
<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>
<!-- 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" />
<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>
<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"
>
</androidx.core.widget.NestedScrollView>

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
transformations(BlurTransformation(requireContext()))

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.

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

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)
val scale = (1 + scrollPercent)
binding.posterContainer.scaleX = scale
binding.posterContainer.scaleY = scale

Adding custom Toolbar content

To improve the animation behavior, open fragment_details.xml.

<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>
<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" />
android:elevation="24dp"
  app:layout_constraintTop_toBottomOf="@id/posterContainer" 

Adding a fade-in animation to the container

Open MovieDetailsFragment.kt and navigate to setupScrolling().

binding.ratingContainer.alpha = scrollPercent

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.

private var originalWidth by Delegates.notNull<Int>()
private var originalHeight by Delegates.notNull<Int>()
private var originalWidth: Int = 0
private var originalHeight: Int = 0
binding.posterContainer.doOnLayout {
  originalWidth = it.width
  originalHeight = it.height
}
binding.posterContainer.scaleX = scale
binding.posterContainer.scaleY = scale
binding.posterContainer.updateLayoutParams {
  this.width = (scale * originalWidth).toInt()
  this.height = (scale * originalHeight).toInt()
}

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.

android:layout_marginTop="32dp" 

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.

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.

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.

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.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2022 Razeware LLC

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 raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.