komikku/app/src/main/java/eu/kanade/presentation/components/SourceSearchBox.kt
dhanuarf b24b14c73d
fix(sources-search): search box visibility glitch when scrolling (#1278)
* fix(sources-screen): search box visibility glitch when scrolling

* Refactor to a reusable function

* Apply to MigrateSourceScreen

* Fix warning

* suggestions

* Revert "Fix warning"

This reverts commit b1c97bcf04b9e34b3eb314fd54c45be953d2f451.

* Add comment

---------

Co-authored-by: Cuong-Tran <16017808+cuong-tran@users.noreply.github.com>
2025-11-28 14:21:41 +07:00

217 lines
7.9 KiB
Kotlin

package eu.kanade.presentation.components
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusManager
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.platform.SoftwareKeyboardController
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.clearFocusOnSoftKeyboardHide
import tachiyomi.presentation.core.util.isItemScrollingUp
import tachiyomi.presentation.core.util.runOnEnterKeyPressed
import tachiyomi.presentation.core.util.secondaryItemAlpha
@Composable
fun AnimatedFloatingSearchBox(
listState: LazyListState,
searchQuery: String?,
onChangeSearchQuery: (String?) -> Unit,
modifier: Modifier = Modifier,
placeholderText: String? = null,
focusManager: FocusManager = LocalFocusManager.current,
focusRequester: FocusRequester = remember { FocusRequester() },
keyboardController: SoftwareKeyboardController? = LocalSoftwareKeyboardController.current,
) {
AnimatedVisibility(
visible = listState.isItemScrollingUp(),
enter = expandVertically(),
exit = shrinkVertically(),
modifier = modifier,
) {
SourcesSearchBox(
searchQuery = searchQuery,
onChangeSearchQuery = onChangeSearchQuery,
placeholderText = placeholderText,
focusManager = focusManager,
focusRequester = focusRequester,
keyboardController = keyboardController,
)
}
}
@Composable
fun SourcesSearchBox(
searchQuery: String?,
onChangeSearchQuery: (String?) -> Unit,
modifier: Modifier = Modifier,
placeholderText: String? = null,
focusManager: FocusManager = LocalFocusManager.current,
focusRequester: FocusRequester = remember { FocusRequester() },
keyboardController: SoftwareKeyboardController? = LocalSoftwareKeyboardController.current,
) {
val searchAndClearFocus: () -> Unit = f@{
if (searchQuery.isNullOrBlank()) return@f
focusManager.clearFocus()
keyboardController?.hide()
}
val onClickClearSearch: () -> Unit = {
onChangeSearchQuery("")
focusRequester.requestFocus()
keyboardController?.show()
}
val onClickCloseSearch: () -> Unit = {
onChangeSearchQuery("")
focusManager.clearFocus()
keyboardController?.hide()
}
var isFocused by remember { mutableStateOf(false) }
BasicTextField(
value = searchQuery ?: "",
onValueChange = onChangeSearchQuery,
modifier = modifier
.fillMaxWidth()
.focusRequester(focusRequester)
.onFocusChanged { isFocused = it.isFocused }
.runOnEnterKeyPressed(action = searchAndClearFocus)
.clearFocusOnSoftKeyboardHide(),
enabled = true,
textStyle = MaterialTheme.typography.bodyMedium.copy(
color = MaterialTheme.colorScheme.onBackground,
),
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
keyboardActions = KeyboardActions(onSearch = { searchAndClearFocus() }),
singleLine = true,
cursorBrush = SolidColor(MaterialTheme.colorScheme.onBackground),
decorationBox = { innerTextField ->
TextFieldDefaults.DecorationBox(
value = searchQuery ?: "",
innerTextField = innerTextField,
enabled = true,
singleLine = true,
visualTransformation = VisualTransformation.None,
interactionSource = remember { MutableInteractionSource() },
placeholder = {
Text(
modifier = Modifier.secondaryItemAlpha(),
text = (placeholderText ?: stringResource(MR.strings.action_search_hint)),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.bodyMedium,
)
},
leadingIcon = {
SearchBoxLeadingIcon(
isSearching = isFocused || !searchQuery.isNullOrBlank(),
onClickCloseSearch = onClickCloseSearch,
)
},
trailingIcon = {
SearchBoxTrailingIcon(
isEmpty = searchQuery.isNullOrEmpty(),
onClickClearSearch = onClickClearSearch,
)
},
shape = RoundedCornerShape(24.dp),
colors = TextFieldDefaults.colors(
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
errorIndicatorColor = Color.Transparent,
cursorColor = MaterialTheme.colorScheme.onBackground,
),
contentPadding = PaddingValues(MaterialTheme.padding.small),
)
},
)
}
@Composable
fun SearchBoxLeadingIcon(
isSearching: Boolean,
modifier: Modifier = Modifier,
onClickCloseSearch: () -> Unit = {},
) {
if (isSearching) {
IconButton(
modifier = modifier,
onClick = onClickCloseSearch,
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Close",
)
}
} else {
Icon(
modifier = modifier,
imageVector = Icons.Filled.Search,
contentDescription = "Search",
)
}
}
@Composable
fun SearchBoxTrailingIcon(
isEmpty: Boolean,
modifier: Modifier = Modifier,
onClickClearSearch: () -> Unit = {},
) {
if (!isEmpty) {
IconButton(
modifier = modifier,
onClick = onClickClearSearch,
) {
Icon(
imageVector = Icons.Filled.Close,
contentDescription = "Clear",
)
}
}
}
@Preview
@Composable
fun PreviewClearFocusOnKeyboardDismissExample() {
SourcesSearchBox(
searchQuery = "Hello World",
onChangeSearchQuery = {},
)
}