diff --git a/app/src/main/kotlin/androidx/compose/material3/SearchBarInputField.kt b/app/src/main/kotlin/androidx/compose/material3/SearchBarInputField.kt deleted file mode 100644 index 312e3811b..000000000 --- a/app/src/main/kotlin/androidx/compose/material3/SearchBarInputField.kt +++ /dev/null @@ -1,191 +0,0 @@ -package androidx.compose.material3 - -import androidx.compose.animation.animateColorAsState -import androidx.compose.foundation.ScrollState -import androidx.compose.foundation.interaction.Interaction -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.interaction.collectIsFocusedAsState -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.offset -import androidx.compose.foundation.layout.sizeIn -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.text.BasicTextField -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.foundation.text.input.InputTransformation -import androidx.compose.foundation.text.input.OutputTransformation -import androidx.compose.foundation.text.input.TextFieldLineLimits -import androidx.compose.foundation.text.input.TextFieldState -import androidx.compose.material3.SearchBarDefaults.InputFieldHeight -import androidx.compose.material3.SearchBarDefaults.inputFieldColors -import androidx.compose.material3.SearchBarDefaults.inputFieldShape -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawWithCache -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.focus.onFocusChanged -import androidx.compose.ui.graphics.ColorProducer -import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.graphics.drawOutline -import androidx.compose.ui.graphics.takeOrElse -import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.unit.dp -import kotlinx.coroutines.delay - -/** - * A text field to input a query in a search bar. - * - * @param state [TextFieldState] that holds the internal editing state of the input field. - * @param onSearch the callback to be invoked when the input service triggers the - * [ImeAction.Search] action. The current query in the [state] comes as a parameter of the - * callback. - * @param expanded whether the search bar is expanded and showing search results. - * @param onExpandedChange the callback to be invoked when the search bar's expanded state is - * changed. - * @param modifier the [Modifier] to be applied to this input field. - * @param enabled the enabled state of this input field. When `false`, this component will not - * respond to user input, and it will appear visually disabled and disabled to accessibility - * services. - * @param readOnly controls the editable state of the input field. When `true`, the field cannot - * be modified. However, a user can focus it and copy text from it. - * @param textStyle the style to be applied to the input text. Defaults to [LocalTextStyle]. - * @param placeholder the placeholder to be displayed when the input text is empty. - * @param leadingIcon the leading icon to be displayed at the start of the input field. - * @param trailingIcon the trailing icon to be displayed at the end of the input field. - * @param prefix the optional prefix to be displayed before the input text. - * @param suffix the optional suffix to be displayed after the input text. - * @param inputTransformation optional [InputTransformation] that will be used to transform - * changes to the [TextFieldState] made by the user. The transformation will be applied to - * changes made by hardware and software keyboard events, pasting or dropping text, - * accessibility services, and tests. The transformation will _not_ be applied when changing - * the [state] programmatically, or when the transformation is changed. If the transformation - * is changed on an existing text field, it will be applied to the next user edit. The - * transformation will not immediately affect the current [state]. - * @param outputTransformation optional [OutputTransformation] that transforms how the contents - * of the text field are presented. - * @param scrollState scroll state that manages the horizontal scroll of the input field. - * @param shape the shape of the input field. - * @param colors [TextFieldColors] that will be used to resolve the colors used for this input - * field in different states. See [SearchBarDefaults.inputFieldColors]. - * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and - * emitting [Interaction]s for this input field. You can use this to change the search bar's - * appearance or preview the search bar in different states. Note that if `null` is provided, - * interactions will still happen internally. - */ -@ExperimentalMaterial3Api -@Composable -fun SearchBarInputField( - state: TextFieldState, - onSearch: (String) -> Unit, - expanded: Boolean, - onExpandedChange: (Boolean) -> Unit, - modifier: Modifier = Modifier, - enabled: Boolean = true, - readOnly: Boolean = false, - textStyle: TextStyle = LocalTextStyle.current, - placeholder: @Composable (() -> Unit)? = null, - leadingIcon: @Composable (() -> Unit)? = null, - trailingIcon: @Composable (() -> Unit)? = null, - prefix: @Composable (() -> Unit)? = null, - suffix: @Composable (() -> Unit)? = null, - inputTransformation: InputTransformation? = null, - outputTransformation: OutputTransformation? = null, - scrollState: ScrollState = rememberScrollState(), - shape: Shape = inputFieldShape, - colors: TextFieldColors = inputFieldColors(), - interactionSource: MutableInteractionSource? = null, -) { - @Suppress("NAME_SHADOWING") - val interactionSource = interactionSource ?: remember { MutableInteractionSource() } - - val focused = interactionSource.collectIsFocusedAsState().value - val focusRequester = remember { FocusRequester() } - val focusManager = LocalFocusManager.current - - val textColor = textStyle.color.takeOrElse { - if (focused) colors.focusedTextColor else colors.unfocusedTextColor - } - val mergedTextStyle = textStyle.merge(TextStyle(color = textColor)) - - BasicTextField( - state = state, - modifier = modifier - .sizeIn( - minWidth = SearchBarMinWidth, - maxWidth = SearchBarMaxWidth, - minHeight = InputFieldHeight, - ) - .focusRequester(focusRequester) - .onFocusChanged { if (it.isFocused) onExpandedChange(true) } - .fillMaxWidth(), - enabled = enabled, - readOnly = readOnly, - lineLimits = TextFieldLineLimits.SingleLine, - textStyle = mergedTextStyle, - cursorBrush = SolidColor(colors.cursorColor), - keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), - onKeyboardAction = { onSearch(state.text.toString()) }, - interactionSource = interactionSource, - inputTransformation = inputTransformation, - outputTransformation = outputTransformation, - scrollState = scrollState, - decorator = TextFieldDefaults.decorator( - state = state, - enabled = enabled, - lineLimits = TextFieldLineLimits.SingleLine, - outputTransformation = outputTransformation, - interactionSource = interactionSource, - placeholder = placeholder, - leadingIcon = leadingIcon?.let { leading -> - { Box(Modifier.offset(x = SearchBarIconOffsetX)) { leading() } } - }, - trailingIcon = trailingIcon?.let { trailing -> - { Box(Modifier.offset(x = -SearchBarIconOffsetX)) { trailing() } } - }, - prefix = prefix, - suffix = suffix, - colors = colors, - contentPadding = TextFieldDefaults.contentPaddingWithoutLabel(), - container = { - val containerColor = - animateColorAsState( - targetValue = if (focused) colors.focusedContainerColor else colors.unfocusedContainerColor, - animationSpec = MaterialTheme.motionScheme.fastEffectsSpec(), - label = "ContainerColor", - ) - Box(Modifier.textFieldBackground(containerColor::value, shape)) - }, - ), - ) - - val shouldClearFocus = !expanded && focused - LaunchedEffect(expanded) { - if (shouldClearFocus) { - // Not strictly needed according to the motion spec, but since the animation - // already has a delay, this works around b/261632544. - delay(AnimationDelayMillis.toLong()) - focusManager.clearFocus() - } - } -} - -private fun Modifier.textFieldBackground( - color: ColorProducer, - shape: Shape, -) = drawWithCache { - val outline = shape.createOutline(size, layoutDirection, this) - onDrawBehind { drawOutline(outline, color = color()) } -} - -private val SearchBarMinWidth = 360.dp -private val SearchBarMaxWidth = 720.dp -private val SearchBarIconOffsetX = 4.dp - -@Suppress("ktlint:standard:property-naming") -private const val AnimationDelayMillis = 100 diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/SearchBarScreen.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/SearchBarScreen.kt index bad4f5d79..8bf1a20cb 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/SearchBarScreen.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/SearchBarScreen.kt @@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.offset @@ -40,7 +41,7 @@ import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.Scaffold import androidx.compose.material3.SearchBar import androidx.compose.material3.SearchBarDefaults -import androidx.compose.material3.SearchBarInputField +import androidx.compose.material3.SearchBarDefaults.InputField import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -57,6 +58,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.coerceAtMost import androidx.compose.ui.unit.dp import com.hippo.ehviewer.EhApplication.Companion.searchDatabase import com.hippo.ehviewer.R @@ -215,7 +217,7 @@ fun SearchBarScreen( modifier = Modifier.align(Alignment.TopCenter).thenIf(!expanded) { offset { IntOffset(0, searchBarOffsetY()) } } .windowInsetsPadding(WindowInsets.navigationBars.only(WindowInsetsSides.Horizontal)), inputField = { - SearchBarInputField( + InputField( state = searchFieldState, onSearch = { hideSearchView() @@ -223,7 +225,7 @@ fun SearchBarScreen( }, expanded = expanded, onExpandedChange = onExpandedChange, - modifier = Modifier.widthIn(max = maxWidth - SearchBarHorizontalPadding * 2), + modifier = Modifier.widthIn(max = (maxWidth - SearchBarHorizontalPadding * 2).coerceAtMost(M3SearchBarMaxWidth)).fillMaxWidth(), placeholder = { val contentActive by activeState.state val text = title.takeUnless { expanded || contentActive } ?: searchFieldHint @@ -319,3 +321,4 @@ fun wrapTagKeyword(keyword: String, translate: Boolean = false): String = if (ke private val WhitespaceRegex = Regex("\\s+") private val SearchBarHorizontalPadding = 16.dp +private val M3SearchBarMaxWidth = 720.dp