Window Insets and Keyboard Animations Tutorial for Android 11

In this tutorial, you’ll learn about Window Insets and Keyboard Animations in Android 11 and how to add these features to your android app. By Carlos Mota.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

Interacting With the Keyboard

In this section, you’ll implement a new feature. When the user scrolls up in the RecyclerView, you’ll push the keyboard at the same speed until it’s fully opened. If it’s visible, swiping down the list will have the opposite behavior, and the keyboard will close.

There are a couple of ways to achieve this. You could use any component that supports scrolling, such as ScrollView or NestedScrollView. In this, case you’ll use RecyclerView.

To open or close the keyboard while the user scrolls through the list, you need to choose a component that supports this behavior:

  • Scroll direction: Depending on if the user’s scroll direction – up or down. In this case, you want to open or close, respectively.
  • Overscroll: The list might not be moving since the user already scrolled until its limit. The user is still swiping his finger across the screen, expecting to see a corresponding action. Because of this, the component you’ll choose needs to support this behavior.
  • Detect when motion starts and stops: Maybe you want to detect when the keyboard animation should start and end. If the user scrolls a bit, you want to open or close the keyboard fully, so it’s important to understand when the movement ends.

To achieve this, you’re going to use the LinearLayoutManager, which supports all of the above functionalities. Go to RWCompat11.kt and update createLinearLayoutManager to:

@RequiresApi(Build.VERSION_CODES.R)
fun createLinearLayoutManager(context: Context, view: View): LinearLayoutManager {
  var scrolledY = 0
  var scrollToOpenKeyboard = false
 
  return object : LinearLayoutManager(context) {
    var visible = false
  }
}

In this method, a couple of fields are already declared:

  • scrolledY: Holds the distance the user dragged in pixels.
  • scrollToOpenKeyboard: True if the user is scrolling up to open the keyboard or false if they’re scrolling down to close it.
  • visible: The initial state of the keyboard when the user started this action.

For now, you’re returning an instance of the LinearLayoutManager. Nevertheless, to make all the calculations needed you’ll need to override two different methods:

  • onScrollStateChanged: Notifies when the user started scrolling through the list and when he finished.
  • scrollVerticallyById: Triggered while the user is scrolling. This lets you synchronize the keyboard animation along with the list the user is scrolling.

Observing Scroll States

Now override onScrollStateChanged. Add this method inside the LinearLayoutManager declaration:

override fun onScrollStateChanged(state: Int) {
  //1
  if (state == AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {
    //2
    visible = view.rootWindowInsets?.isVisible(WindowInsetsCompat.Type.ime()) == true
    //3
    if (visible) {
      scrolledY = view.rootWindowInsets?.getInsets(WindowInsetsCompat.Type.ime())!!.bottom
    }
    //4
    createWindowInsetsAnimation()
    //5
  } else if (state == AbsListView.OnScrollListener.SCROLL_STATE_IDLE) {
    //6
    scrolledY = 0
    animationController?.finish(scrollToOpenKeyboard)
  }
 
  super.onScrollStateChanged(state)
}

If Android studio prompts for imports, import android.widget.AbsListView. Ignore the missing methods for now.

Here’s what this code does:

  1. When the user initially touches the list they trigger onScrollStateChanged with the value SCROLL_STATE_TOUCH_SCROLL.
  2. To understand if the keyboard is going to close or open, you need to save its initial state. If it’s open, the corresponding action is to close the keyboard. Conversely, if it’s closed, the corresponding action will open it.
  3. If it’s already visible you need to initialize scrolledY with the IME’s current bottom position, otherwise part of the UI will be covered. This value will be important later since it also influences the value of scrollToOpenKeyboard which defines if the keyboard’s final action is to open or close.
  4. In this method, you define the animation controller listener that’s used in the animation.
  5. After the user finishes the action it’s time to clean up any used resource and finish the animation.
  6. If the keyboard isn’t entirely visible, because the user scrolled a small portion of the screen, this call is responsible for finishing this action.

In this code block, you saw there’s a call to a method you haven’t added: createWindowInsetsAnimation. In the same class, after the declaration of createLinearLayoutManager add:

@RequiresApi(Build.VERSION_CODES.R)
private fun createWindowInsetsAnimation() {
  view.windowInsetsController?.controlWindowInsetsAnimation(
    WindowInsetsCompat.Type.ime(), //types
    -1,                            //durationMillis
    LinearInterpolator(),          //interpolator
    CancellationSignal(),          //cancellationSignal
    animationControlListener       //listener
 )
}

Take note of the import statements to use:
import android.os.CancellationSignal
import android.view.animation.LinearInterpolator

This adds a controller to the inset you want to animate. This method receives the following arguments:

  • types: The types of inset your app wants to control. In this case, since it’s the keyboard you’re going to set it as ime.
  • durationMillis: The duration of the animation. Since the keyboard is going to animate while the user drags the list, which depends on an arbitrary action, you disable the animation by setting it to -1.
  • interpolator: The interpolator used for the animation. In this case, you’re going to use LinearInterpolator.
  • cancellationSignal: Used to cancel the animation and return to the previous state. Since in this scenario the behavior selected is to finish the animation you won’t use this.
  • listener: The animation controller listener that’s called when the windows are ready to animate or the operation cancels or finishes.

Handling Animations

Now that you defined the controlWindowInsetsAnimation you’ll need to declare the animationControlListener used in this method. At the top of RWCompat11 class, just before setUiWindowInsets, add:

 
private val animationControlListener: WindowInsetsAnimationControlListener by lazy {
  @RequiresApi(Build.VERSION_CODES.R)
  object : WindowInsetsAnimationControlListener {
 
    override fun onReady(
      controller: WindowInsetsAnimationController,
      types: Int
    ) {
      animationController = controller
    }
 
    override fun onFinished(controller: WindowInsetsAnimationController) {
      animationController = null
    }
 
    override fun onCancelled(controller: WindowInsetsAnimationController?) {
      animationController = null
    }
  }
}

When the keyboard is ready to animate, you call onReady with the animationController you’ll use to pull or pop the keyboard to or from the screen. Here, the animationController updates with this new reference to use on LinearLayoutManager methods. If the action is either canceled or finished, it cleans all resources, since they’re no longer necessary.

Before going to the last method, declare the animationController field just before the animationControlListener:

private var animationController: WindowInsetsAnimationController? = null

Finally, go back to createLinearLayoutManager. You’ve already declared onScrollStateChanged where the keyboard animation is set up and finished. Now it’s time to create the animation itself.

Override the scrollVerticallyBy:

override fun scrollVerticallyBy(dy: Int, recycler: Recycler, state: State): Int {
  //1
  scrollToOpenKeyboard = scrolledY < scrolledY + dy
  //2
  scrolledY += dy
  //3
  if (scrolledY < 0) {
    scrolledY = 0
  }
  //4
  animationController?.setInsetsAndAlpha(
    Insets.of(0, 0, 0, scrolledY),
    1f,
    0f
  )
 
  return super.scrollVerticallyBy(dy, recycler, state)
}

When prompted for imports, import androidx.recyclerview.widget.RecyclerView.* and android.graphics.Insets.

In the code above:

  1. Since the user can scroll up and down the list you can't rely on the keyboard's initial visibility state. To know if it should open or close the keyboard, scrollToOpenKeyboard calculates the user's swipe direction based on scrolledY and dy. If the last scroll was upwards to the beginning of the list the keyboard will show, otherwise it will hide.
  2. dy contains the distance from scrollVerticallyBy event. To know the total distance scrolled you have to add this reference to a variable set inside the LinearLayoutManager scope: scrolledY.
  3. In case the scrolledY is negative, the value will be set to 0 since it's not possible to move the keyboard to a negative value.
  4. Finally, setInsetsAndAlpha defines the movement that needs to occur on the IME window. In this case, you only need to define the bottom value so all the other values are set to 0. The 1f corresponds to the value set for the alpha property, which is set to the maximum to avoid having any transparency. 0f is the animation progress.

Now that you've defined everything, it's time to compile and run the app!

Opening and closing the keyboard by scrolling through the notes list

Beautiful, right? :]