Handle physical keyboard keys on the scaffold state that is rendered
The key event handler was installed on the activity together with the state instance it captured, so it could keep talking to a state that is no longer the one that is drawn. That happens in practice: the scaffold state is recreated when one of its rememberSaveable keys changes, which it does right after the launcher starts (the window is measured, and the state is restored after the configuration change), and the activity then still holds the handler of the previous state. Typing on the home screen set the search query and started the page transition on the detached state, so the search page never appeared, while the query showed up in the search bar. The handler is now created without the state as a key and asks for the current state through a provider, and the activity's handler is refreshed on every composition instead of only when the handler instance changes. Verified on the Titan 2: typing on the home screen opens the search page with the character inserted, further characters append, the text field stays unfocused and the on-screen keyboard stays hidden.
This commit is contained in:
@@ -34,8 +34,8 @@ android {
|
||||
applicationId = "de.mm20.launcher2"
|
||||
minSdk = libs.versions.minSdk.get().toInt()
|
||||
targetSdk = libs.versions.targetSdk.get().toInt()
|
||||
versionCode = System.getenv("VERSION_CODE_OVERRIDE")?.toIntOrNull() ?: 2026091002
|
||||
versionName = "1.40.2-typing.3"
|
||||
versionCode = System.getenv("VERSION_CODE_OVERRIDE")?.toIntOrNull() ?: 2026091003
|
||||
versionName = "1.40.2-typing.4"
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
}
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -1074,14 +1075,25 @@ internal fun LauncherScaffold(
|
||||
|
||||
// Open the search when the user starts typing on a physical keyboard. The handler is
|
||||
// installed on the activity, so it also receives key events while no text field is focused.
|
||||
val typeToSearchHandler = rememberTypeToSearchHandler(state, searchVM)
|
||||
DisposableEffect(typeToSearchHandler) {
|
||||
val launcherActivity = activity as? SharedLauncherActivity
|
||||
launcherActivity?.launcherKeyEventHandler = typeToSearchHandler
|
||||
//
|
||||
// The scaffold state is recreated when one of its keys changes - the window size after a
|
||||
// rotation, or the state that rememberSaveable restores after the activity was recreated -
|
||||
// so the handler must not keep a reference to one particular instance: it reads the current
|
||||
// state through this holder, which the composition keeps up to date, and the activity's
|
||||
// handler is refreshed on every composition. Without that, the handler can end up opening
|
||||
// the search on a state that is no longer the one that is rendered, which looks as if the
|
||||
// key press did nothing.
|
||||
val currentState = remember { mutableStateOf(state) }
|
||||
SideEffect {
|
||||
if (currentState.value !== state) currentState.value = state
|
||||
}
|
||||
val typeToSearchHandler = rememberTypeToSearchHandler({ currentState.value }, searchVM)
|
||||
SideEffect {
|
||||
(activity as? SharedLauncherActivity)?.launcherKeyEventHandler = typeToSearchHandler
|
||||
}
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
if (launcherActivity?.launcherKeyEventHandler === typeToSearchHandler) {
|
||||
launcherActivity.launcherKeyEventHandler = null
|
||||
}
|
||||
(activity as? SharedLauncherActivity)?.launcherKeyEventHandler = null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -58,6 +58,9 @@ internal fun KeyEvent.isEnter(): Boolean {
|
||||
* [SharedLauncherActivity.launcherKeyEventHandler]. It is called for every key event before the
|
||||
* event is dispatched to the view hierarchy, so it also works while no text field has focus.
|
||||
*
|
||||
* The scaffold state the handler works on is passed as a provider rather than as a value, because
|
||||
* the state is replaced when the window size changes or the launcher is recreated.
|
||||
*
|
||||
* While the search is driven by the physical keyboard, the search bar's text field is deliberately
|
||||
* left unfocused: a focused text field would make the system show the on-screen keyboard, and the
|
||||
* user is typing on a physical keyboard anyway. The handler therefore inserts and deletes the
|
||||
@@ -68,7 +71,7 @@ internal fun KeyEvent.isEnter(): Boolean {
|
||||
*/
|
||||
@Composable
|
||||
internal fun rememberTypeToSearchHandler(
|
||||
state: LauncherScaffoldState,
|
||||
stateProvider: () -> LauncherScaffoldState,
|
||||
searchVM: SearchVM,
|
||||
): (KeyEvent) -> Boolean {
|
||||
val scope = rememberCoroutineScope()
|
||||
@@ -76,8 +79,12 @@ internal fun rememberTypeToSearchHandler(
|
||||
val sheetManager = LocalBottomSheetManager.current
|
||||
val overlayManager = LocalOverlayManager.current
|
||||
|
||||
return remember(state, searchVM, activity, sheetManager, overlayManager) {
|
||||
// Deliberately not keyed on the scaffold state: the state is recreated when the window is
|
||||
// resized or the launcher is recreated, and a key must always be handled by the state that is
|
||||
// rendered at that moment. stateProvider hands out that state.
|
||||
return remember(searchVM, activity, sheetManager, overlayManager) {
|
||||
fun handle(event: KeyEvent): Boolean {
|
||||
val state = stateProvider()
|
||||
// Read the preference here rather than capturing it in the composition, so that the
|
||||
// current value is always used.
|
||||
if (!searchVM.searchOnTyping.value || state.isLocked) return false
|
||||
|
||||
Reference in New Issue
Block a user