Last active
May 13, 2025 13:34
-
-
Save bmc08gt/9ed7c1ccae28294d2c7b6ec5125cf790 to your computer and use it in GitHub Desktop.
KMP Software Keyboard Controller
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import androidx.compose.runtime.Composable | |
import androidx.compose.runtime.getValue | |
import androidx.compose.runtime.mutableStateOf | |
import androidx.compose.runtime.setValue | |
import androidx.compose.ui.platform.SoftwareKeyboardController | |
import kotlinx.coroutines.CoroutineScope | |
import kotlinx.coroutines.delay | |
import kotlinx.coroutines.launch | |
public abstract class KeyboardController( | |
private val coroutineScope: CoroutineScope, | |
private val softwareController: SoftwareKeyboardController? | |
) { | |
public open var isVisible: Boolean by mutableStateOf(false) | |
protected set | |
public fun show() { | |
softwareController?.show() | |
} | |
public fun hide() { | |
softwareController?.hide() | |
} | |
public fun hideIfVisible(block: () -> Unit = { }) { | |
coroutineScope.launch { | |
if (isVisible) { | |
hide() | |
delay(300) | |
} | |
block() | |
} | |
} | |
} | |
@Composable | |
public expect fun rememberKeyboardController(): KeyboardController |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import android.annotation.SuppressLint | |
import android.view.View | |
import android.view.ViewTreeObserver | |
import androidx.compose.runtime.Composable | |
import androidx.compose.runtime.DisposableEffect | |
import androidx.compose.runtime.getValue | |
import androidx.compose.runtime.mutableStateOf | |
import androidx.compose.runtime.remember | |
import androidx.compose.runtime.rememberCoroutineScope | |
import androidx.compose.runtime.setValue | |
import androidx.compose.ui.platform.LocalSoftwareKeyboardController | |
import androidx.compose.ui.platform.LocalView | |
import androidx.compose.ui.platform.SoftwareKeyboardController | |
import androidx.core.view.ViewCompat | |
import androidx.core.view.WindowInsetsCompat | |
import kotlinx.coroutines.CoroutineScope | |
private class AndroidKeyboardController( | |
private val view: View, | |
coroutineScope: CoroutineScope, | |
softwareKeyboardController: SoftwareKeyboardController? | |
): KeyboardController(coroutineScope, softwareKeyboardController) { | |
override var isVisible by mutableStateOf(false) | |
// Internal setup for visibility tracking | |
@SuppressLint("ComposableNaming") | |
@Composable | |
fun setupVisibilityTracking() { | |
val viewTreeObserver = view.viewTreeObserver | |
DisposableEffect(viewTreeObserver) { | |
val listener = ViewTreeObserver.OnGlobalLayoutListener { | |
isVisible = ViewCompat.getRootWindowInsets(view) | |
?.isVisible(WindowInsetsCompat.Type.ime()) ?: false | |
} | |
viewTreeObserver.addOnGlobalLayoutListener(listener) | |
onDispose { viewTreeObserver.removeOnGlobalLayoutListener(listener) } | |
} | |
} | |
} | |
@Composable | |
public actual fun rememberKeyboardController(): KeyboardController { | |
val view = LocalView.current | |
val softwareController = LocalSoftwareKeyboardController.current | |
val composeScope = rememberCoroutineScope() | |
val keyboardController = remember(view, softwareController) { | |
AndroidKeyboardController(view, composeScope, softwareController) | |
} | |
// Trigger visibility tracking | |
keyboardController.setupVisibilityTracking() | |
return keyboardController | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import androidx.compose.runtime.Composable | |
import androidx.compose.runtime.DisposableEffect | |
import androidx.compose.runtime.getValue | |
import androidx.compose.runtime.mutableStateOf | |
import androidx.compose.runtime.remember | |
import androidx.compose.runtime.rememberCoroutineScope | |
import androidx.compose.runtime.setValue | |
import androidx.compose.ui.interop.LocalUIViewController | |
import androidx.compose.ui.platform.LocalSoftwareKeyboardController | |
import androidx.compose.ui.platform.SoftwareKeyboardController | |
import kotlinx.coroutines.CoroutineScope | |
import platform.Foundation.NSNotificationCenter | |
import platform.UIKit.UIKeyboardWillHideNotification | |
import platform.UIKit.UIKeyboardWillShowNotification | |
private class KeyboardObserver( | |
private val onStateChanged: (Boolean) -> Unit, | |
) { | |
fun watch() { | |
NSNotificationCenter.defaultCenter.addObserverForName( | |
name = UIKeyboardWillHideNotification, | |
`object` = null, | |
queue = null, | |
) { | |
onStateChanged(false) | |
} | |
NSNotificationCenter.defaultCenter.addObserverForName( | |
name = UIKeyboardWillShowNotification, | |
`object` = null, | |
queue = null, | |
) { | |
onStateChanged(true) | |
} | |
} | |
fun stop() { | |
NSNotificationCenter.defaultCenter.removeObserver(this) | |
} | |
} | |
private class IOSKeyboardController( | |
coroutineScope: CoroutineScope, | |
softwareKeyboardController: SoftwareKeyboardController? | |
) : KeyboardController(coroutineScope, softwareKeyboardController) { | |
override var isVisible by mutableStateOf(false) | |
// Internal setup for visibility tracking | |
@Composable | |
fun setupVisibilityTracking() { | |
val keyboardObserver = remember { KeyboardObserver { isVisible = it } } | |
DisposableEffect(LocalUIViewController.current) { | |
keyboardObserver.watch() | |
onDispose { | |
keyboardObserver.stop() | |
} | |
} | |
} | |
} | |
@Composable | |
public actual fun rememberKeyboardController(): KeyboardController { | |
val softwareController = LocalSoftwareKeyboardController.current | |
val composeScope = rememberCoroutineScope() | |
val keyboardController = remember(softwareController) { | |
IOSKeyboardController(composeScope, softwareController) | |
} | |
// Trigger visibility tracking | |
keyboardController.setupVisibilityTracking() | |
return keyboardController | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Composable | |
fun SomeScreen(goBack: () -> Unit)) { | |
val keyboard = rememberKeyboardController() | |
Button(onClick = { keyboard.hideIfVisible(goBack) }) { | |
Text(text = "Exit") | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment