More modern, Kotlin solution, based on Flow and Coroutines:
- 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()
}
Create a singleton instance with whatever dependency injection library you use.
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)
}
}
- 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()
}