Now that the keyboard is beautifully synchronized with the views, let’s see how interacting with the surrounding views can also modify the keyboard visibility.
In this episode you’ll use: LinearLayoutManager, WindowInsetsController, WindowInsetsAnimationController. API’s to 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.
Open RWCompat11.kt and search for the createLinearLayoutManager function.
@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
}
}
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.
To make all the calculations needed to update the RecyclerView position while opening/closing the keyboard 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.
override fun onScrollStateChanged(state: Int) {
if (state == AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {
visible = view.rootWindowInsets?.isVisible(WindowInsetsCompat.Type.ime()) == true
if (visible) {
scrolledY = view.rootWindowInsets?.getInsets(WindowInsetsCompat.Type.ime())!!.bottom
}
createWindowInsetsAnimation()
} else if (state == AbsListView.OnScrollListener.SCROLL_STATE_IDLE) {
scrolledY = 0
animationController?.finish(scrollToOpenKeyboard)
}
super.onScrollStateChanged(state)
}
When the user initially touches the list it’s trigger onScrollStateChanged with the value SCROLL_STATE_TOUCH_SCROLL. 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.
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. In this method, you define the animation controller listener that’s used in the animation.
After the user finishes the action it’s time to clean up any used resource and finish the animation. 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.
override fun scrollVerticallyBy(dy: Int, recycler: Recycler, state: State): Int {
scrollToOpenKeyboard = scrolledY < scrolledY + dy
scrolledY += dy
val navBar = view.rootWindowInsets?.getInsets(WindowInsetsCompat.Type.navigationBars())!!
if (scrolledY < navBar.bottom) {
scrolledY = navBar.bottom
}
animationController?.setInsetsAndAlpha(
Insets.of(0, 0, 0, scrolledY),
1f,
0f
)
return super.scrollVerticallyBy(dy, recycler, state)
}
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.
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.
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.
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.
@RequiresApi(Build.VERSION_CODES.R)
private fun createWindowInsetsAnimation() {
view.windowInsetsController?.controlWindowInsetsAnimation(
WindowInsetsCompat.Type.ime(), //types
-1, //durationMillis
LinearInterpolator(), //interpolator
CancellationSignal(), //cancellationSignal
animationControlListener //listener
)
}
The animateController.setInsetsAndAlpha 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.
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, onReady with the animationController is called, and it will be used to pull or pop the keyboard to or from the screen.
Congratulations! You learned how to create a seamless animation when launching the keyboard. Build & run the app, and play with the list to see the keyboard opening while you’re scrolling up and close it when your scrolling down.
Don’t forget to add the episode name to the list: 06: Animate Surrounding Views. In the next episode we will revisit the same concepts, but this time using Jetpack Compose!
See you in the next episode!