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

Until Android 11, keyboards and Androids seemed to go in opposite directions. There was no API you could query to learn if the keyboard was open or to know its size. When the keyboard popped up, the screen and views would automatically rearrange themselves without a smooth transition.

Developers used to handle everything manually with complex logic. It was time consuming and difficult. To overcome these issues, Android 11 introduces new features focused on window insets and keyboard animations.

In this tutorial, you’ll learn:

  • About window insets and the keyboard.
  • What’s new in Android 11 and what’s available in older APIs for handling keyboards.
  • Keyboard animations
  • Interacting with the keyboard in Android 11.
Note: This tutorial assumes you’re familiar with Android Studio and the basics of Android development. If you’re new to either, read through Beginning Android Development and Kotlin for Android: An Introduction before continuing.

Getting Started

Download the starter project by clicking the Download Materials button at the top or bottom of the tutorial. You’ll find two projects inside the ZIP file. Starter has the skeleton of the app you’ll build, and Final gives you something to compare your code to when you’re done.

Adding notes to Brain Dump app

It’s easy to feel overwhelmed with everything you need to do in a day: Get the kids to school, stop at the grocery store, update a section on this tutorial. If you don’t want to forget any of these items you need to write them down.

To solve this problem, you’ll build BrainDump, a note-taking app that helps you keep track of everything you need to do.

First, take some time to understand the project structure.

Understanding the Project Structure

Open the Starter project in Android Studio and wait for it to synchronize.

The directory structure of the project.

You’ll see a set of subfolders and other important files:

  • model: This is the data object used to represent a note. A note is made up of its content and the date it was created.
  • compat: Keyboard animations are only available for devices running Android 11. Therefore, you need different implementations for specific calls. RWCompat.kt is a factory responsible for loading RWCompat10.kt or RWCompat11.kt, depending on the device’s Android version.
  • ui: In this folder, you’ll find the activity, fragments and adapters you’ll use to let your users view and interact with the app’s data.
  • Utils.kt: This file contains a set of utility methods you’ll use throughout the project. Namely, you’ll use these to save and load your notes into and from shared preferences and format a note timestamp to an easily readable date.

Before diving in, spend some time learning about WindowInsets.

Understanding Window Insets

On Android, the window view could be categorised into two: the section for your application and the section for the Android OS. Window insets are the different portions of the screen that intersect with the system UI, such as the status and navigation bars and the lateral navigation sections on the new system gestures.

To avoid having user actions defined on areas already defined by the system for navigation, which would make them unusable, you can use a set of insets to locate these areas and control them according to your application’s specifications. Below are some of the available insets:

  • System window insets
  • Tappable element insets
  • Gesture insets
  • Stable insets

In this tutorial, you’ll use the system window insets. To learn more about window insets check out this Gesture Navigation Tutorial for Android.

Note: Android introduced WindowInsets in API 20. If you’re targeting a lower version you should use the AndroidX backported class WindowInsetsCompat. This is the class you’ll use throughout the tutorial.

Next, you’ll get familiar with the keyboard.

Getting to Know the Keyboard

Until now, there was no direct API you could use to access the keyboard and retrieve information about its state. Gathering this information was a job for complex calculus and inference, if not guessing.

Gone are the days when you had to manually implement everything to get any information about the keyboard’s current state. With the launch of Android 11, developers have new set of functionalities that let you animate the keyboard’s appearance or disappearance and how users interact with it.

Even better, Android backported most of these features to previous Android versions. After reading this tutorial, you can go back to your projects and remove hundreds of lines of keyboard-specifc code. :]

What’s Available on Older APIs

The following functionalities are available on all the Android versions supported by the AndroidX’s appcompat libraries.

Launching the Keyboard

You can launch the keyboard using two different APIs:

  • requestFocus: Launch the keyboard by calling this method on EditText or other components that lets users enter text.
  • windowInsetsController: With this API you manually force the keyboard to show:
    view.windowInsetsController.show(WindowInsetsCompat.Type.ime())
    

    Or the correspondent hide method, if you want to close it:

    view.windowInsetsController.hide(WindowInsetsCompat.Type.ime())
    

    On both calls, you use WindowInsetsController with the corresponding WindowInsetsCompat you want to access. In this case, the keyboard, or IME, so you can open or close it.

Note:You can sufficiently use these APIs to control your keyboard in place of the InputMethodManager API.

Checking if the Keyboard is Visible

To see if the keyboard is open, you can call:

view.rootWindowInsets.isVisible(WindowInsetsCompat.Type.ime())

WindowInsetsCompat.Type.ime() defines the type of insets you need to access to see if the keyboard is open.

Getting the Keyboard Height

You guessed it! Once again you can rely on WindowInsetsCompat to get the keyboard height. Simply call:

view.rootWindowInsets.getInsets(WindowInsetsCompat.Type.ime()).bottom

This bottom value is directly related to keyboard visibility. If the keyboard is visible, it returns its height. If not, the value is 0.

Now that you’ve reviewed what the old APIs can do, it’s time to see what’s new in Android 11.

What’s New in Android 11

Along with these new methods, Android 11 also introduced a set of functionalities focused on keyboard animations and how your views interact with them.

Before diving in, add some notes to the list. Notice how unsynchronized opening the keyboard is:

Opening and closing the keyboard without animations

Preparing Your App for Keyboard Animations

To animate your keyboard, or IME, and the surrounding UI, you need to set your app as fullscreen because the IME is part of the system UI. Prior to Android 11, you could do this via:

window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
  View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
  View.SYSTEM_UI_FLAG_FULLSCREEN

However, this API is now deprecated. That’s a good thing, since it was always tricky to find the right combination of flags. In it’s place, Android backported a single method to the previous Android versions via WindowCompat that lets you achieve the same behavior.

Go to MainActivity.kt, and before calling super.onCreate(savedInstanceState), add:

WindowCompat.setDecorFitsSystemWindows(window, !isAtLeastAndroid11())

Android studio prompts you for two imports. So, import androidx.core.view.WindowCompat and com.raywenderlich.android.braindump.isAtLeastAndroid11.

Here, the second parameter, !isAtLeastAndroid11(), defines whether the app will handle the system windows. If the device runs Android 11 or newer, this value will be false so the app can define the keyboard animations. On lower versions, since these functionalities aren’t available, the value will be true so the system controls them.

To understand these differences, compile and run the on a device with a version lower than Android 11.

Brain dump app running on Android 10 where the app doesn't handle the system bars

Everything seems perfect! What if you run it on a device with Android 11?

Brain dump app running on Android 11 where the app handles the system bars

As you can see, the UI now overlaps the system bars. This overlap happens because your app asked to occupy the entire screen. At the same time, it said it would take care of the system windows, which it didn’t.

So, time to do that. :]

Handling System Windows

setDecorFitsSystemWindows is only set for Android versions 11 or higher. So, open RWCompat11.kt and update setUiWindowInsets :

//1
private var posTop = 0
private var posBottom = 0
 
fun setUiWindowInsets() {
  //2
  ViewCompat.setOnApplyWindowInsetsListener(container) { _, insets ->
    //3
    if (posBottom == 0) {
      posTop = insets.getInsets(WindowInsetsCompat.Type.systemBars()).top
      posBottom = insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom
    }
    //4
    container.updateLayoutParams<ViewGroup.MarginLayoutParams> {
      updateMargins(
        top = posTop,
        bottom = posBottom)
    }
 
    insets
 }
}

When prompted for imports, use the following:

import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updateLayoutParams
import androidx.core.view.updateMargins

setUiWindowInsets is already declared, so you need to add its content.

Here’s a step-by-step breakdown of this logic:

  1. You declare these two fields globally because you’ll use them later in a method that handles the keyboard animations.
  2. To guarantee compatibility with previous Android versions, you give precedence to the appcompat APIs.
  3. On the first run, both fields are empty, so they need to be set. Since the input bar should be on top of the navigation UI and the toolbar should be under the status bar, you’ll need to check the margins of these two insets and update the container margins accordingly.
  4. The container received corresponds to the activity’s root view, based on the values defined earlier. You use them to update the view bottom and top margin. With this, no component is overlaid.
Note: You need to calculate postTop and postBottom inside the setOnApplyWindowInsetsListener. Otherwise when you’re querying systemBars insets you might receive 0 as top and bottom margins. There’s no guarantee the views will be ready outside this listener.

Now that you rearranged the UI to be within the screen limits, hit compile and run the app.

Brain dump app running on Android 11 with the UI adapted to the system windows

Everything fits perfectly – well done! :]

Note: Want to know more about window insets and gesture navigation? Check out this Gesture Navigation Tutorial for Android.

Now that the UI fits its window, it’s time to animate the keyboard.

Animating the Keyboard

You’ll use the WindowInsetsAnimationCallback to animate the keyboard.

This API is only available on Android 11 and higher. So in RWCompat11.kt, update animateKeyboardDisplay as follows:

//1
@RequiresApi(Build.VERSION_CODES.R)
fun animateKeyboardDisplay() {
  //2
  val cb = object : WindowInsetsAnimation.Callback(DISPATCH_MODE_STOP) {
    //3
    override fun onProgress(insets: WindowInsets, animations: MutableList<WindowInsetsAnimation>): WindowInsets {
      //4
      posBottom = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom +
        insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom
      //5
      container.updateLayoutParams<ViewGroup.MarginLayoutParams> {
        updateMargins(
          top = posTop,
          bottom = posBottom)
      }
 
      return insets
    }
  }
  //6
  container.setWindowInsetsAnimationCallback(cb)
}

Here’s a logic breakdown:

  1. setWindowInsetsAnimationCallback is only available on Android R. Although RWCompat11.kt only contains code that targets this API, it’s good practice to add this annotation to notify other programmers they need to check if the device supports this call.
  2. You can use two modes here: DISPATCH_MODE_CONTINUE_ON_SUBTREE and DISPATCH_MODE_STOP. In this scenario, you used the latter since the animation occurs at the parent level. And also there’s no need to propagate this event into other levels of the view hierarchy.
  3. In this use case, you used onProgress to update the UI. There are a few methods available that can be useful in other scenarios. More on this shortly.
  4. Every time there’s a change on WindowInsetsCompat, onProgress is called and you need to update the root view margins. This guarantees the UI updates seamlessly with the animation.

    To recalculate this margin, you need to get the systemBars bottom margin and add it to the current size of the IME. Otherwise, your UI will be under the system navigation bar or the keyboard.

    If the user opens the keyboard, this sum will increase until the animation finishes. If the user closes the keyboard, the sum will decrease until the final result is the value of the systemBars bottom.

    Position in the screen of the system bars and the IME

  5. With these new values you update the margins, so the UI will synchronize with the keyboard animation.
  6. After defining the callback it’s important to set it. Otherwise, nothing will happen.

With everything defined, play a bit with these new animations. Compile and run the app. See how smoothly the keyboard opens and closes.

Opening and closing the keyboard with the newly added animations

WindowInsets Animation Lifecycle

The other methods available in setWindowInsetsAnimationCallback are:

  • onPrepare: Lets you record any specific configuration before the animation takes place. For instance, you can record the initial coordinates of a view.
  • onStart: Similar to the previous method, you can use it to save any value that’s going to change later. You can also use it to trigger any other behavior related to this event when the animation starts.
  • onProgress: This event is triggered multiple times as the keyboard is displayed or dismissed from the screen. It’s called every time the WindowInsetsCompat changes.
  • onEnd: This event triggers after the animation ends. You can use it to clean up any allocated resources or make any UI view reflect this new state.

Now you’ve seen how to make your UI smoothly adapt to any keyboard change. Take a look at which additional events could cause the same trigger.

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? :]

Where to Go From Here?

Congratulations! You learned how to create a seamless animation when launching the keyboard.

You can download the completed project files by clicking the Download Materials button at the top or bottom of the tutorial.

Android 10 and 11 have many new features you can use to empower your app! For a fun challenge, try the Bubbles tutorial. Consider learning about augmented reality apps in ARCore with Kotlin. Does your app deal with files? Don't forget to make it ready for Scoped Storage.

If you have any questions or comments, please join the discussion below.