How to move View with Keyboard in Android using Kotlin

Last updated on: September 12, 2023

In this tutorial, I’ll walk you through how to animate a view when the keyboard appears or disappears.

The animation behaves differently at different Android API levels. For example:

• API 30 and above: The view moves synchronized with the keyboard movement (It looks awesome)

• API 21 – API 29: The view moves with the keyboard but with a small delay (It looks pretty good)

• API 20 and lower: The view snaps to the place above the keyboard, and there’s no animation (It looks kinda meh)

Here’s how they look in slow-motion:

API 30 and above
API 29 – API 21
API 20 and lower

Enabling View Movement With Keyboard

You can make the view move when the keyboard appears/disappears in two ways.

The first is to set the flag adjustPan inside the <activity ... /> , in the AndroidManifest.xml:

<!-- ... -->

<activity
android:name=".SomeActivity"
android:windowSoftInputMode="adjustPan"
android:exported="true" />

<!-- ... -->Code language: HTML, XML (xml)

The second way is to set it programmatically, which I recommend because you’ll be able to disable it when you don’t need it.

class MainActivity : AppCompatActivity() {


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        // ...

        window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
        
        // ...
        
    }
}Code language: Kotlin (kotlin)

But as you see, this flag has been deprecated since API 30, and we should use the WindowInsets instead.

The SOFT_INPUT_ADJUST_RESIZE flag is deprecated and we have to replace it with WindowInsets

So let’s do that!

To have everything organized and make the code reusable, we’re going to initialize all the interface listeners to a different class.

Go to your project, right-click and select New > Kotlin Class/File.

Create a new Kotlin Class or File

Give the name InsetsWithKeyboardCallback, choose Class, and then press Enter.

Creating the Kotlin class InsetsWithKeyboardCallback

Next, implement the OnApplyWindowInsetsListener interface and hover your mouse on the “class” word, and Implement members

Implementing the members of the OnApplyWindowInsetsListener callback in the InsetsWithKeyboadCallback class
class InsetsWithKeyboardCallback : OnApplyWindowInsetsListener {
    
    override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat {
        // Add code later ...
    }
    
}Code language: Kotlin (kotlin)

Now, in the onApplyWindowInsets method, we’re going to get the system bars insets and keyboard (IME) insets and combine their insets for the bottom padding, so when we open/close the keyboard, the view will move up/down.

class InsetsWithKeyboardCallback : OnApplyWindowInsetsListener {

    override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat {
        // System Bars' Insets
        val systemBarsInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars())
        //  System Bars' and Keyboard's insets combined
        val systemBarsIMEInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars() + WindowInsetsCompat.Type.ime())

        // We use the combined bottom inset of the System Bars and Keyboard to move the view so it doesn't get covered up by the keyboard
        v.setPadding(systemBarsInsets.left, systemBarsInsets.top, systemBarsInsets.right, systemBarsIMEInsets.bottom)
        return WindowInsetsCompat.CONSUMED
    }

}Code language: Kotlin (kotlin)

Also, when we initialize the class, we have to set fitsSystemWindows to false and let the insets do the work.

And because we need to have access to the Window of the Activity, we add it as a class parameter.

class InsetsWithKeyboardCallback(window: Window) : OnApplyWindowInsetsListener {

    init {
        WindowCompat.setDecorFitsSystemWindows(window, false)
    }

    //  onApplyWindowInsets method code ....
}Code language: Swift (swift)

Finally, we add the flag we discussed earlier to support devices running Android API Level 29 and lower

init {
    WindowCompat.setDecorFitsSystemWindows(window, false)

    // For better support for devices API 29 and lower
    if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
        @Suppress("DEPRECATION")
        window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
    }
}Code language: Kotlin (kotlin)

Now it’s time to set the listener.

In our MainActivity, we install ViewCompact.OnApplyWindowInsetsListener setOnApplyWindowInsetsListener on our root content view of our Activity and we set the listener InsetsWithKeyboardCallback

class MainActivity : AppCompatActivity() {
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        

        val root = findViewById<ConstraintLayout>(R.id.content_id)
        
        val insetsWithKeyboardCallback = InsetsWithKeyboardCallback(window)
        ViewCompat.setOnApplyWindowInsetsListener(root, insetsWithKeyboardCallback)
        
    }
}Code language: Kotlin (kotlin)
The root content view of the demo app

As I showed you at the beginning, the animation looks different on devices running different versions of the Android operating system.

In API 30 and above, the animation tracks the keyboard animation perfectly, but for the API level is 21-29, we have to write some code to mimic the system’s IME animation.

For this, we’re going to use the WindowInsetsAnimation.Callback

Now, back to the InsetsWithKeyboardCallback class, we add the WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_CONTINUE_ON_SUBTREE) after the OnApplyWindowInsetsListener, and we implement the methods the same way as we did before.

class InsetsWithKeyboardCallback(window: Window) : OnApplyWindowInsetsListener, WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_CONTINUE_ON_SUBTREE) {

    init {
        // ...
    }

    override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat {
        // ...
    }

    override fun onProgress(insets: WindowInsetsCompat, runningAnimations: MutableList<WindowInsetsAnimationCompat>): WindowInsetsCompat {
        // Add code later
    }

}Code language: Kotlin (kotlin)

We also add the onPrepare and onEnd methods.

class InsetsWithKeyboardCallback(window: Window) : OnApplyWindowInsetsListener, WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_CONTINUE_ON_SUBTREE) {

    init {
        // ...
    }

    override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat {
        // ...
    }

    override fun onPrepare(animation: WindowInsetsAnimationCompat) {
        super.onPrepare(animation)
    }

    override fun onProgress(insets: WindowInsetsCompat, runningAnimations: MutableList<WindowInsetsAnimationCompat>): WindowInsetsCompat {
        // Add code later
    }

    override fun onEnd(animation: WindowInsetsAnimationCompat) {
        super.onEnd(animation)
    }

}Code language: Kotlin (kotlin)

The onPrepare method is called when an insets animation is about to start.

In our example, this is called when the keyboard is going to appear on the screen, or when it’s about to disappear.

The onEnd method is called when an insets animation has ended.

So, in our case, it’s called when the keyboard has become fully visible on the screen, or when it has gone.

To mimic the keyboard animation, we have to change the insets between the systemBars() insets (during the animation) and systemBars() + ime() insets (after the animation has ended), and to do that we’re going to need a flag that switches between true and false in the onPrepare and onEnd methods respectively.

class InsetsWithKeyboardCallback(window: Window) : OnApplyWindowInsetsListener, WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_CONTINUE_ON_SUBTREE) {
    
    private var deferredInsets = false
    
    init {
        // ...
    }

    override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat {
        // ...
    }

    override fun onPrepare(animation: WindowInsetsAnimationCompat) {
        if (animation.typeMask and WindowInsetsCompat.Type.ime() != 0) {
            // When the IME is not visible, we defer the WindowInsetsCompat.Type.ime() insets
            deferredInsets = true
        }
    }

    override fun onProgress(insets: WindowInsetsCompat, runningAnimations: MutableList<WindowInsetsAnimationCompat>): WindowInsetsCompat {
        // Add code later
    }

    override fun onEnd(animation: WindowInsetsAnimationCompat) {
        if (deferredInsets && (animation.typeMask and WindowInsetsCompat.Type.ime()) != 0) {
            // When the IME animation has finished and the IME inset has been deferred, we reset the flag
            deferredInsets = false
        }
    }

}Code language: Kotlin (kotlin)

Next, in the onProgress method we just return the insets again because we don’t want to animate any view insets changes.

// ...

override fun onProgress(insets: WindowInsetsCompat, runningAnims: List<WindowInsetsAnimationCompat>): WindowInsetsCompat {
    return insets
}

// ...Code language: Kotlin (kotlin)

Then we return to the onApplyWindowInsets method and replace the previous code with one that returns the right insets according to the flag’s value.

class InsetsWithKeyboardCallback(window: Window) : OnApplyWindowInsetsListener, WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_CONTINUE_ON_SUBTREE) {

    private var deferredInsets = false

    init {
        // ...
    }

    override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat {
        val types = when {
            // When the deferred flag is enabled, we only use the systemBars() insets
            deferredInsets -> WindowInsetsCompat.Type.systemBars()
            // When the deferred flag is disabled, we use combination of the the systemBars() and ime() insets
            else -> WindowInsetsCompat.Type.systemBars() + WindowInsetsCompat.Type.ime()
        }

        val typeInsets = insets.getInsets(types)
        v.setPadding(typeInsets.left, typeInsets.top, typeInsets.right, typeInsets.bottom)
        return WindowInsetsCompat.CONSUMED
    }

    override fun onPrepare(animation: WindowInsetsAnimationCompat) {
        // ...
    }

    override fun onProgress(insets: WindowInsetsCompat, runningAnimations: MutableList<WindowInsetsAnimationCompat>): WindowInsetsCompat {
        // ...
    }

    override fun onEnd(animation: WindowInsetsAnimationCompat) {
        // ...
    }

}Code language: Kotlin (kotlin)

Lastly, we must dispatch the insets manually because the normal dispatch will happen too late and make a visual flicker.

So we have to store the view and latest insets in onApplyWindowInsets method, the and then in onEnd, dispatch them.

class InsetsWithKeyboardCallback(window: Window) : OnApplyWindowInsetsListener, WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_CONTINUE_ON_SUBTREE) {

    private var deferredInsets = false
    private var view: View? = null
    private var lastWindowInsets: WindowInsetsCompat? = null

    init {
        // ...
    }

    override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat {
        view = v
        lastWindowInsets = insets
        
        // ...
        
        return WindowInsetsCompat.CONSUMED
    }

    override fun onPrepare(animation: WindowInsetsAnimationCompat) {
        // ...
    }

    override fun onProgress(insets: WindowInsetsCompat, runningAnimations: MutableList<WindowInsetsAnimationCompat>): WindowInsetsCompat {
        // ...
    }

    override fun onEnd(animation: WindowInsetsAnimationCompat) {
        if (deferredInsets && (animation.typeMask and WindowInsetsCompat.Type.ime()) != 0) {
            
            // ...
            
            // We dispatch insets manually because if we let the normal dispatch cycle handle it, this will happen too late and cause a visual flicker
            // So we dispatch the latest WindowInsets to the view
            if (lastWindowInsets != null && view != null) {
                ViewCompat.dispatchApplyWindowInsets(view!!, lastWindowInsets!!)
            }
        }
    }

}Code language: Kotlin (kotlin)

In the end, the file will look like this:

class InsetsWithKeyboardCallback(window: Window) : OnApplyWindowInsetsListener, WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_CONTINUE_ON_SUBTREE) {

    private var deferredInsets = false
    private var view: View? = null
    private var lastWindowInsets: WindowInsetsCompat? = null

    init {
        WindowCompat.setDecorFitsSystemWindows(window, false)

        // For better support for devices API 29 and lower
        if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
            @Suppress("DEPRECATION")
            window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
        }
    }

    override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat {
        view = v
        lastWindowInsets = insets
        val types = when {
            // When the deferred flag is enabled, we only use the systemBars() insets
            deferredInsets -> WindowInsetsCompat.Type.systemBars()
            // When the deferred flag is disabled, we use combination of the the systemBars() and ime() insets
            else -> WindowInsetsCompat.Type.systemBars() + WindowInsetsCompat.Type.ime()
        }

        val typeInsets = insets.getInsets(types)
        v.setPadding(typeInsets.left, typeInsets.top, typeInsets.right, typeInsets.bottom)
        return WindowInsetsCompat.CONSUMED
    }

    override fun onPrepare(animation: WindowInsetsAnimationCompat) {
        if (animation.typeMask and WindowInsetsCompat.Type.ime() != 0) {
            // When the IME is not visible, we defer the WindowInsetsCompat.Type.ime() insets
            deferredInsets = true
        }
    }

    override fun onProgress(insets: WindowInsetsCompat, runningAnimations: MutableList<WindowInsetsAnimationCompat>): WindowInsetsCompat {
        return insets
    }

    override fun onEnd(animation: WindowInsetsAnimationCompat) {
        if (deferredInsets && (animation.typeMask and WindowInsetsCompat.Type.ime()) != 0) {
            // When the IME animation has finished and the IME inset has been deferred, we reset the flag
            deferredInsets = false

            // We dispatch insets manually because if we let the normal dispatch cycle handle it, this will happen too late and cause a visual flicker
            // So we dispatch the latest WindowInsets to the view
            if (lastWindowInsets != null && view != null) {
                ViewCompat.dispatchApplyWindowInsets(view!!, lastWindowInsets!!)
            }
        }
    }

}Code language: Kotlin (kotlin)

Here’s what happens when we open the keyboard:

We tap on a text field, and before the keyboard shows up, it calls the onPrepare method. Here we check if the keyboard is not visible, in this case, it is true, so we set the differedInsets flag to true.

Then the onApplyWindowInsets method gets called and sets the systemBars() insets until the end of the animation.

Next, after the keyboard has appeared fully on the screen, the onEnd method gets called and we set the flag back to false and we dispatch the insets to the view manually.

Finally, the onApplyWindowInsets method is called again and sets the insets back to the combination of the systemBars() and ime() insets.

Now, we return to the Activity where we set the animation listener as well:

class MainActivity : AppCompatActivity() {
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        

        val root = findViewById<ConstraintLayout>(R.id.content_id)
        
        val insetsWithKeyboardCallback = InsetsWithKeyboardCallback(window)
        ViewCompat.setOnApplyWindowInsetsListener(root, insetsWithKeyboardCallback)
        ViewCompat.setWindowInsetsAnimationCallback(root, insetsWithKeyboardCallback)
    }
}Code language: Kotlin (kotlin)

Making View Movement Smoother

Now, let’s make the view movement smoother when the keyboard appears/disappears.

We’ll do this by using the WindowInsetsAnimation.Callback again.

Create a new Kotlin Class file as we did before, and name it InsetsWithKeyboardAnimationCallback.

Creating the Kotlin class InsetsWithKeyboardAnimationCallback and adding code that will help to move the view with the keyboard smoother

Then, we add WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_STOP) and the parameter view, which is the view that will move (In our example, is the “Login” button), and implement the members.

Then, we add WindowInsetsAnimationCompat.setCallback() with DISPATCH_MODE_STOP as the dispatch mode and a View parameter (the view that will move) and implement the callback methods.

We also add the onEnd method

class InsetsWithKeyboardAnimationCallback(private val view: View) : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_STOP) {

    override fun onProgress(insets: WindowInsetsCompat, runningAnimations: MutableList<WindowInsetsAnimationCompat>): WindowInsetsCompat {
        // Add code later
    }

    override fun onEnd(animation: WindowInsetsAnimationCompat) {
        super.onEnd(animation)
    }
}Code language: Kotlin (kotlin)

In the onProgress method, we get the ime and system bars insets, we calculate the difference and we apply the results as a translation to the view

class InsetsWithKeyboardAnimationCallback(private val view: View) : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_STOP) {

    override fun onProgress(insets: WindowInsetsCompat, runningAnimations: MutableList<WindowInsetsAnimationCompat>): WindowInsetsCompat {
        val imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime())
        val systemInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars())
        
        
        val diff = Insets.subtract(imeInsets, systemInsets).let {
            Insets.max(it, Insets.NONE)
        }
        
        view.translationX = (diff.left - diff.right).toFloat()
        view.translationY = (diff.top - diff.bottom).toFloat()

        return insets
    }

    override fun onEnd(animation: WindowInsetsAnimationCompat) {
        // ...
    }
}Code language: Kotlin (kotlin)

Lastly, in the onEnd method, we reset the view’s translation values after the animation has ended

class InsetsWithKeyboardAnimationCallback(private val view: View) : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_STOP) {

    override fun onProgress(insets: WindowInsetsCompat, runningAnimations: MutableList<WindowInsetsAnimationCompat>): WindowInsetsCompat {
        //...
    }

    override fun onEnd(animation: WindowInsetsAnimationCompat) {
        // We reset the translation values after the animation has finished
        view.translationX = 0f
        view.translationY = 0f
    }
}Code language: Kotlin (kotlin)

Now, back to the MainActivity, we set the listener for the login button

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // ...

        val loginButton = findViewById<Button>(R.id.login_button)

        val insetsWithKeyboardAnimationCallback = InsetsWithKeyboardAnimationCallback(loginButton)
        ViewCompat.setWindowInsetsAnimationCallback(loginButton, insetsWithKeyboardAnimationCallback)

    }
}Code language: Kotlin (kotlin)
You can find the final project here

If you have any questionsplease feel free to leave a comment below

Subscribe
Notify of
guest
10 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments