2

My app is meant to collect data. But it is only necessary for it to collect the data while the keyboard is visible. It is not sufficient to only collect data while the user is typing, so I definitely need to know if the keyboard is visible or not.

I know, similar questions were posted before (How to check visibility of software keyboard in Android?), but the last answer with a serious number of upvotes is from 2012, and I guess a lot of things happened with Android since then.

So, can I detect if the keyboard is open/visible?

keinabel
  • 1,002
  • 3
  • 15
  • 33
  • as I mentioned, the last "useful" answer is 4years old and a lot has changed with android since then. I am looking for an up-to-date answer to this issue – keinabel Oct 17 '16 at 13:52

5 Answers5

5

Create a class

package com.dubaipolice.app.utils;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Rect;
import android.util.TypedValue;
import android.view.View;
import android.view.ViewTreeObserver;

import java.util.LinkedList;
import java.util.List;

/**
 * Created by dev101 on 1/13/15.
 */
public class SoftKeyboardStateHelper implements ViewTreeObserver.OnGlobalLayoutListener {

    float LIMIT = 100;

    public interface SoftKeyboardStateListener {
        void onSoftKeyboardOpened(int keyboardHeightInPx);

        void onSoftKeyboardClosed();
    }

    private final List<SoftKeyboardStateListener> listeners = new LinkedList<SoftKeyboardStateListener>();
    private final View activityRootView;
    private int lastSoftKeyboardHeightInPx;
    private boolean isSoftKeyboardOpened;

    public SoftKeyboardStateHelper(Context context, View activityRootView) {
        this(activityRootView, false);
        Resources r = context.getResources();
        LIMIT = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 100, r.getDisplayMetrics());
    }

    public SoftKeyboardStateHelper(View activityRootView, boolean isSoftKeyboardOpened) {
        this.activityRootView = activityRootView;
        this.isSoftKeyboardOpened = isSoftKeyboardOpened;
        activityRootView.getViewTreeObserver().addOnGlobalLayoutListener(this);
    }

    @Override
    public void onGlobalLayout() {
        final Rect r = new Rect();
        //r will be populated with the coordinates of your view that area still visible.
        activityRootView.getWindowVisibleDisplayFrame(r);

        final int heightDiff = activityRootView.getRootView().getHeight() - (r.bottom - r.top);
        if (!isSoftKeyboardOpened && heightDiff > LIMIT) { // if more than 100 pixels, its probably a keyboard...
            isSoftKeyboardOpened = true;
            notifyOnSoftKeyboardOpened(heightDiff);
        } else if (isSoftKeyboardOpened && heightDiff < LIMIT) {
            isSoftKeyboardOpened = false;
            notifyOnSoftKeyboardClosed();
        }
    }

    public void setIsSoftKeyboardOpened(boolean isSoftKeyboardOpened) {
        this.isSoftKeyboardOpened = isSoftKeyboardOpened;
    }

    public boolean isSoftKeyboardOpened() {
        return isSoftKeyboardOpened;
    }

    /**
     * Default value is zero (0)
     *
     * @return last saved keyboard height in px
     */
    public int getLastSoftKeyboardHeightInPx() {
        return lastSoftKeyboardHeightInPx;
    }

    public void addSoftKeyboardStateListener(SoftKeyboardStateListener listener) {
        listeners.add(listener);
    }

    public void removeSoftKeyboardStateListener(SoftKeyboardStateListener listener) {
        listeners.remove(listener);
    }

    private void notifyOnSoftKeyboardOpened(int keyboardHeightInPx) {
        this.lastSoftKeyboardHeightInPx = keyboardHeightInPx;

        for (SoftKeyboardStateListener listener : listeners) {
            if (listener != null) {
                listener.onSoftKeyboardOpened(keyboardHeightInPx);
            }
        }
    }

    private void notifyOnSoftKeyboardClosed() {
        for (SoftKeyboardStateListener listener : listeners) {
            if (listener != null) {
                listener.onSoftKeyboardClosed();
            }
        }
    }
}

Then in your activity's onCreate, add following lines

final SoftKeyboardStateHelper softKeyboardStateHelper = new SoftKeyboardStateHelper(context, findViewById(R.id.parent));
        softKeyboardStateHelper.addSoftKeyboardStateListener(softKeyboardStateListener);

where R.id.parent is the id of your activity's parent layout and softKeyboardStateListener is defined as follows

SoftKeyboardStateHelper.SoftKeyboardStateListener softKeyboardStateListener = new SoftKeyboardStateHelper.SoftKeyboardStateListener() {
        @Override
        public void onSoftKeyboardOpened(int keyboardHeightInPx) {

        }

        @Override
        public void onSoftKeyboardClosed() {

        }
    };
4

We have currently Android N now and still no direct ways to detect if keyboard is opened or not. There're only work around solutions available like the ones checking the screen size. However there're not fullproof and some times give false signals for example on screen rotation or going into multi-window mode on Android N.

1

As you may have heard, there is no direct way. However, by checking if the screen size has been changed, you can generally find this out by using:

protected void onSizeChanged(int xNew, int yNew, int xOld, int yOld) {
    super.onSizeChanged(xNew, yNew, xOld, yOld);

    if (yOld > yNew) {
        //Do Stuff Here
    }
}

Hope I Helped :D

Ishan Manchanda
  • 459
  • 4
  • 19
1

I faced the same problem earlier. After a lot of trial and error and following other people suggestion, I came up with my implementation that works well for me. Here is the link.

Francis Nduba Numbi
  • 2,499
  • 1
  • 11
  • 22
0

More modern, Kotlin solution, based on Flow and Coroutines:

  1. Create KeyboardUtil class.
    import android.app.Activity
    import android.graphics.Rect
    import android.view.View
    import android.view.ViewTreeObserver
    import kotlinx.coroutines.CoroutineScope
    import kotlinx.coroutines.flow.MutableStateFlow
    import kotlinx.coroutines.flow.SharingStarted
    import kotlinx.coroutines.flow.combine
    import kotlinx.coroutines.flow.stateIn
    import kotlinx.coroutines.flow.update

    interface KeyboardUtil {
        val keyboardOpen: StateFlow<Boolean?> // true = open, false = closed, null = uninitialised
        fun isKeyboardOpen() = keyboardOpen.value ?: false // For easily accessing the state in a synchronous way.
        fun registerKeyboardListener(activity: Activity)
        fun deregisterKeyboardListener(activity: Activity)
    }
    
    class KeyboardUtilImpl(
        scope: CoroutineScope,
    ) : KeyboardUtil, ViewTreeObserver.OnGlobalLayoutListener {
        private val currentHeight = MutableStateFlow(0)
        private val maxHeight = MutableStateFlow(0)
        private var _activity: Activity? = null
    
        override val keyboardOpen = currentHeight.combine(maxHeight) { current, previous ->
            if (current <= 0) return@combine null
            // Keyboard opening is determined by measuring the height of the layout
            // and comparing it to its previous state.
            current < previous
        }.stateIn(scope, SharingStarted.WhileSubscribed(5000L), null)
    
        private fun updateLayoutHeight(height: Int) {
            if (currentHeight.value > maxHeight.value) {
                maxHeight.update { currentHeight.value }
            }
            currentHeight.update { height }
        }
    
        override fun onGlobalLayout() {
            val layoutHeight = _activity.measureLayoutHeight()
            updateLayoutHeight(layoutHeight)
        }
    
        override fun registerKeyboardListener(activity: Activity) {
            _activity = activity
            activity.getRootView()
                ?.viewTreeObserver
                ?.addOnGlobalLayoutListener(this)
        }
    
        override fun deregisterKeyboardListener(activity: Activity) {
            _activity = null
            activity.getRootView()
                ?.viewTreeObserver
                ?.removeOnGlobalLayoutListener(this)
        }
    }
    
    private fun Activity.getRootView(): View? =
        findViewById<View>(android.R.id.content)?.rootView
    
    private fun Activity?.measureLayoutHeight(): Int {
        this ?: return 0
        val visibleBounds = Rect()
        val rootView = getRootView() ?: return 0
        rootView.getWindowVisibleDisplayFrame(visibleBounds)
        return visibleBounds.height()
    }

  1. Create a singleton instance with whatever dependency injection library you use.

  2. Inject into activity (with your chosen library) and register/deregister onResume/onPause:

    import androidx.lifecycle.Lifecycle
    import androidx.lifecycle.lifecycleScope
    import androidx.lifecycle.repeatOnLifecycle
    import kotlinx.coroutines.launch
    // import KeyboardUtil
    
    class SomeActivity {
        
        // Inject keyboardUtil
        
        override fun onResume() {
            super.onResume()
            keyboardUtil.registerKeyboardListener(activity)
        }
    
        override fun onPause() {
            super.onPause()
            keyboardUtil.deregisterKeyboardListener(activity)
        }
    }
  1. Observe in activity:
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                // Keyboard changes relevant only in the foreground
                keyboardUtil.keyboardOpen.collect { isOpen ->
                    isOpen ?: return@collect
                    when (isOpen) {
                        true -> {}
                        false -> {}
                    }
                }
            }
        }
    }

Or in Compose:

    import androidx.compose.runtime.Composable
    import androidx.compose.runtime.getValue
    import androidx.lifecycle.compose.collectAsStateWithLifecycle
    // import KeyboardUtil
    
    @Composable
    fun SomeView(
        keyboardUtil: KeyboardUtil,
    ) {
        val isKeyboardOpen by keyboardUtil.keyboardOpen.collectAsStateWithLifecycle()
    }
Serge
  • 359
  • 2
  • 13