1, the preface

In some projects, we often need to create a custom input box that pops up on the soft keyboard and returns to the same box when closed (as in the illustration below, the chat and comment screens in various apps look like this). In this process, elements of the interface other than the input field are not affected, such as the background image in the renderings will not be moved up or compressed. But in practice, it is found that the soft keyboard often covers the input box when it pops up, which leads to the incomplete display of the input box. What can be done about it?

2. Analysis of ideas

2.1 Obtaining the height of the soft keyboard

A common idea on the web is this: place a View under the input box, when the soft keyboard pops up, get the height of the soft keyboard, and then dynamically set the View height to the same as the soft keyboard in the code, so that the input box is on top of it. Visually, it looks like being jacked up by a soft keyboard.

The difficulty of this idea is to accurately obtain the dynamic height of the soft keyboard. Android doesn’t provide an API for directly measuring the height of the soft keyboard. Fortunately, we can save the country by curving the height: The height of the soft keyboard is essentially the height of the screen minus the height of the visible area above the soft keyboard.

Soft keyboard height = Screen height – Visible area height

In addition, status bar and virtual navigation bar height also need to be considered, so we can get the following calculation formula:

Soft keyboard height = screen height – visible area height – top status bar height – bottom navigation bar height

But there are two caveats:

  1. When the Activity is full-screen, there is no status bar, so you don’t have to deduct the height.

  2. In landscape mode, the virtual status bar is on the side, so you don’t have to subtract its height.

Finally, our formula can be modified as:

Soft keyboard height = Screen height – Visible area height – Top status bar height (not full screen) – Bottom navigation bar height (portrait)

The screen height, status bar height, and navigation bar height in this formula are all available through the Android API, so now the challenge is to get the exact dynamic height of the visible area.

2.2 Obtaining the height of the Visible Area

Accurately capture the dynamic height of the visible area, what is accurate, what is dynamic? To be accurate, we must accurately obtain objects in the visible area. To be dynamic, we must listen for height changes in the visible area, i.e. :

  1. Obtain visible area (accurately corresponding);

  2. Listen for height changes in the visible area (corresponding to dynamics).

Start with the first step, in the View class provides a method for us getWindowVisibleDisplayFrame (), where it can get a View Window (Window) of the visible area (note: is the visible area of the Window, not the View of the visible area!) . It needs to pass in a Rect object. From the Rect object, we can get information about the visible area, such as the distance between the top of the visible area and the top of the parent layout and the distance between the bottom of the visible area and the top of the parent layout, and subtract the two to get the height of the visible area.

So which View is used to get the visible area? Is the layout or control on the current Activity or Fragment? The answer is no. Because the Activity (or Fragment) is in the same window as the soft keyboard, that is, the soft keyboard is also in the window’s visible area, whether the soft keyboard is up or closed, the size of the visible area remains unchanged!

In that case, we need another window. Is there a way to create a View that isn’t part of the window that the soft keyboard is in? Sure, Dialog and PopupWindow can do this. We need this View to be always there for listening, so PopupWindow is definitely the best option.

After the first step to solve, the next step is to monitor the visible region of the change is simpler, can through inheritance interface ViewTreeObserver. OnGlobalLayoutListener, in onGlobalLayout () to monitor.

3. Code practice

Now that I’ve got my thoughts straight, it’s code time. Create a KeyboardStatusWatcher class, inheritance in PopupWindow and ViewTreeObserver OnGlobalLayoutListener interface:

class KeyboardStatusWatcher(
    private val activity: FragmentActivity,
    private val lifecycleOwner: LifecycleOwner,
    private val listener: (isKeyboardShowed: Boolean, keyboardHeight: Int) - >Unit
) : PopupWindow(activity), ViewTreeObserver.OnGlobalLayoutListener {

    private val rootView by lazy { activity.window.decorView.rootView }

    private val TAG = "Keyboard-Tag"

    /** * Height of visible area */
    private var visibleHeight = 0

    /** * Whether the soft keyboard displays */
    var isKeyboardShowed = false
        private set

    /** * The height of the last pop-up soft keyboard */
    var keyboardHeight = 0
        private set

    /** * PopupWindow layout */
    private val popupView by lazy {
        FrameLayout(activity).also {
            it.layoutParams = FrameLayout.LayoutParams(
                ViewGroup.LayoutParams.WRAP_CONTENT,
                ViewGroup.LayoutParams.MATCH_PARENT
            )
            // Listen for layout size changes
            it.viewTreeObserver.addOnGlobalLayoutListener(this)}}init {
        // Initialize PopupWindow
        contentView = popupView
        // The PopupWindow should be resized when the soft keyboard pops up
        softInputMode =
            WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE or
                WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE
        inputMethodMode = INPUT_METHOD_NEEDED
        // Set the width to 0 to avoid blocking the interface
        width = 0
        height = ViewGroup.LayoutParams.MATCH_PARENT
        setBackgroundDrawable(ColorDrawable(0))
        rootView.post { showAtLocation(rootView, Gravity.NO_GRAVITY, 0.0)}// popupWindow must be closed during activity destruction or Fragment onDestroyView to avoid memory leaks
        lifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver {
            override fun onDestroy(owner: LifecycleOwner) {
                super.onDestroy(owner)
                dismiss()
            }
        })
    }

    /** * listen for layout size changes */
    override fun onGlobalLayout(a) {
        val rect = Rect()
        // Get the current visible region
        popupView.getWindowVisibleDisplayFrame(rect)
        if (visibleHeight == (rect.bottom - rect.top)) {
            // Do not execute the following code when the height of the visible area is unchanged to avoid repeated listening
            return
        } else {
            visibleHeight = (rect.bottom - rect.top)
        }
        // Roughly calculate the height change, which will be corrected later according to the status bar and navigation bar
        val heightDiff = rootView.height - visibleHeight
        // If the window height changes by more than 1/3 of the screen, it will be treated as a soft keyboard pop-up
        if (heightDiff > activity.screenHeight / 3) {
            isKeyboardShowed = true
            // Subtracts the height of the status bar when not in full screen
            keyboardHeight =
                if (activity.isFullScreen) heightDiff else heightDiff - activity.statusBarHeight
            // Subtract the height of the navigation bar when it is displayed, but the navigation bar is on the side when it is landscape, so it is not necessary to subtract the height
            if (activity.hasNavBar && activity.isNavBarShowed && activity.isPortrait) {
                keyboardHeight -= activity.navBarHeight
            }
        } else {
            // The keyboard height is 0 when the soft keyboard is hidden
            isKeyboardShowed = false
            keyboardHeight = 0
        }
        listener.invoke(isKeyboardShowed, keyboardHeight)
    }
}
Copy the code

The code is to follow the previous thinking analysis written, notes are more detailed, but more analysis. Just pay attention to the interaction of the soft keyboard while PopupWindow exists. PopupWindow belongs to a different window from the soft keyboard, and the soft keyboard will be overwritten by PopupWindow by default (you can verify this by modifying the code above to set the PopupWindow color and width not to 0), so that the PopupWindow height does not change. You can’t get the surveillance done. So we need to set the softInputMode and inputMethodMode properties to adjust the PopupWindow height as the soft keyboard pops up and down.

Then take a quick look at the MainActivity layout:

<androidx.constraintlayout.widget.ConstraintLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/clRoot"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@drawable/watermelon"
    tools:context=".MainActivity">

    <View
        android:id="@+id/vKeyboardBg"
        android:layout_width="match_parent"
        android:layout_height="60dp"
        android:background="@android:color/white"
        app:layout_constraintBottom_toBottomOf="parent" />

    <androidx.appcompat.widget.AppCompatEditText
        android:imeOptions="flagNoExtractUi"
        android:id="@+id/editText"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_marginHorizontal="15dp"
        android:layout_marginVertical="8dp"
        android:background="@drawable/shape_edit_bg"
        android:hint="Please enter"
        android:paddingHorizontal="10dp"
        app:layout_constraintBottom_toBottomOf="@id/vEditBg"
        app:layout_constraintTop_toTopOf="@id/vEditBg" />

</androidx.constraintlayout.widget.ConstraintLayout>
Copy the code

Android :imeOptions=”flagNoExtractUi” in EditText, otherwise the style will change in landscape.

Also, don’t forget to Activity in the manifest file with android: windowSoftInputMode = “adjustNothing | stateHidden”, otherwise the soft keyboard pop-up layout will move up the whole.

Last, of course, is called in the Activity:

        KeyboardStatusWatcher(this.this) { isKeyboardShowed: Boolean, keyboardHeight: Int ->
            vKeyboardBg.updateLayoutParams<ConstraintLayout.LayoutParams> {
                bottomMargin = keyboardHeight
            }
            Log.d("Tag"."isShowed = $isKeyboardShowed,keyboardHeight = $keyboardHeight")}}Copy the code

4. Project address

This concludes the article. The project address is: Gitee.

There is another shortcoming of the project: the requirements are met, but the user experience is much worse than that of wechat. The input box of wechat is very smooth when the soft keyboard pops up and down, without any flicker.

If you have a better implementation method or other criticism suggestions, feel free to leave a comment and communicate with me.

5. Refer to the article

Android EditText Landscape display issues – Simple book

Android dynamically obtains the height of the soft keyboard and listens for the display or hiding of the soft keyboard. – the nuggets

Android viewing area for window size: getWindowVisibleDisplayFrame _ccpat column – CSDN blog ()

Windows mechanism of Android comprehensive parsing _ A fairy ape -CSDN blog