From 7a1b599462498f6be8faf08cbd2814a04863f3ef Mon Sep 17 00:00:00 2001 From: Ivan Iskandar <12537387+ivaniskandar@users.noreply.github.com> Date: Fri, 31 Mar 2023 20:24:44 +0700 Subject: [PATCH] Adjust SearchToolbar soft keyboard behavior (#9282) * Show soft keyboard when the text field is composed (a redo) * Clear focus on text field when soft keyboard is hidden * Request focus on text field and show soft keyboard when clear button is clicked --- .../browse/components/BrowseSourceToolbar.kt | 1 + .../kanade/presentation/components/AppBar.kt | 30 ++++----- .../presentation/core/util/Modifier.kt | 61 +++++++++++++++++++ 3 files changed, 75 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceToolbar.kt b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceToolbar.kt index ccea144a28..4e6d4585c0 100644 --- a/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceToolbar.kt +++ b/app/src/main/java/eu/kanade/presentation/browse/components/BrowseSourceToolbar.kt @@ -45,6 +45,7 @@ fun BrowseSourceToolbar( var selectingDisplayMode by remember { mutableStateOf(false) } SearchToolbar( + initialShowKeyboard = searchQuery.isNullOrEmpty(), navigateUp = navigateUp, titleContent = { AppBarTitle(title) }, searchQuery = searchQuery, diff --git a/app/src/main/java/eu/kanade/presentation/components/AppBar.kt b/app/src/main/java/eu/kanade/presentation/components/AppBar.kt index 4046e10423..4b7a69da84 100644 --- a/app/src/main/java/eu/kanade/presentation/components/AppBar.kt +++ b/app/src/main/java/eu/kanade/presentation/components/AppBar.kt @@ -23,7 +23,6 @@ import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.key @@ -45,8 +44,10 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import eu.kanade.tachiyomi.R +import tachiyomi.presentation.core.util.clearFocusOnSoftKeyboardHide import tachiyomi.presentation.core.util.runOnEnterKeyPressed import tachiyomi.presentation.core.util.secondaryItemAlpha +import tachiyomi.presentation.core.util.showSoftKeyboard const val SEARCH_DEBOUNCE_MILLIS = 250L @@ -231,9 +232,9 @@ fun SearchToolbar( scrollBehavior: TopAppBarScrollBehavior? = null, visualTransformation: VisualTransformation = VisualTransformation.None, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + initialShowKeyboard: Boolean = true, ) { val focusRequester = remember { FocusRequester() } - var searchClickCount by remember { mutableStateOf(0) } AppBar( titleContent = { @@ -255,7 +256,9 @@ fun SearchToolbar( modifier = Modifier .fillMaxWidth() .focusRequester(focusRequester) - .runOnEnterKeyPressed(action = searchAndClearFocus), + .runOnEnterKeyPressed(action = searchAndClearFocus) + .showSoftKeyboard(initialShowKeyboard) + .clearFocusOnSoftKeyboardHide(), textStyle = MaterialTheme.typography.titleMedium.copy( color = MaterialTheme.colorScheme.onBackground, fontWeight = FontWeight.Normal, @@ -294,10 +297,7 @@ fun SearchToolbar( navigateUp = if (searchQuery == null) navigateUp else onClickCloseSearch, actions = { key("search") { - val onClick = { - searchClickCount++ - onChangeSearchQuery("") - } + val onClick = { onChangeSearchQuery("") } if (!searchEnabled) { // Don't show search action @@ -306,7 +306,12 @@ fun SearchToolbar( Icon(Icons.Outlined.Search, contentDescription = stringResource(R.string.action_search)) } } else if (searchQuery.isNotEmpty()) { - IconButton(onClick) { + IconButton( + onClick = { + onClick() + focusRequester.requestFocus() + }, + ) { Icon(Icons.Outlined.Close, contentDescription = stringResource(R.string.action_reset)) } } @@ -317,15 +322,6 @@ fun SearchToolbar( isActionMode = false, scrollBehavior = scrollBehavior, ) - LaunchedEffect(searchClickCount) { - if (searchQuery == null) return@LaunchedEffect - if (searchClickCount == 0 && searchQuery.isNotEmpty()) return@LaunchedEffect - try { - focusRequester.requestFocus() - } catch (_: Throwable) { - // TextField is gone - } - } } sealed interface AppBar { diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/util/Modifier.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/util/Modifier.kt index ca538a8660..24024c055e 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/util/Modifier.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/util/Modifier.kt @@ -4,14 +4,25 @@ import androidx.compose.foundation.background import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.isImeVisible import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.composed import androidx.compose.ui.draw.alpha +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.key import androidx.compose.ui.input.key.onPreviewKeyEvent +import androidx.compose.ui.platform.LocalFocusManager import tachiyomi.presentation.core.components.material.SecondaryItemAlpha fun Modifier.selectedBackground(isSelected: Boolean): Modifier = composed { @@ -52,3 +63,53 @@ fun Modifier.runOnEnterKeyPressed(action: () -> Unit): Modifier = this.onPreview else -> false } } + +/** + * For TextField on AppBar, this modifier will request focus + * to the element the first time it's composed. + */ +fun Modifier.showSoftKeyboard(show: Boolean): Modifier = if (show) { + composed { + val focusRequester = remember { FocusRequester() } + var openKeyboard by rememberSaveable { mutableStateOf(show) } + LaunchedEffect(focusRequester) { + if (openKeyboard) { + focusRequester.requestFocus() + openKeyboard = false + } + } + + Modifier.focusRequester(focusRequester) + } +} else { + this +} + +/** + * For TextField, this modifier will clear focus when soft + * keyboard is hidden. + */ +fun Modifier.clearFocusOnSoftKeyboardHide(): Modifier = composed { + var isFocused by remember { mutableStateOf(false) } + var keyboardShowedSinceFocused by remember { mutableStateOf(false) } + if (isFocused) { + val imeVisible = WindowInsets.isImeVisible + val focusManager = LocalFocusManager.current + LaunchedEffect(imeVisible) { + if (imeVisible) { + keyboardShowedSinceFocused = true + } else if (keyboardShowedSinceFocused) { + focusManager.clearFocus() + } + } + } + + Modifier.onFocusChanged { + if (isFocused != it.isFocused) { + if (isFocused) { + keyboardShowedSinceFocused = false + } + isFocused = it.isFocused + } + } +}