| /* |
| * Copyright 2022 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package androidx.compose.material3 |
| |
| import androidx.activity.compose.BackHandler |
| import androidx.compose.animation.AnimatedVisibility |
| import androidx.compose.animation.EnterTransition |
| import androidx.compose.animation.ExitTransition |
| import androidx.compose.animation.core.CubicBezierEasing |
| import androidx.compose.animation.core.FiniteAnimationSpec |
| import androidx.compose.animation.core.animateFloatAsState |
| import androidx.compose.animation.core.tween |
| import androidx.compose.animation.expandVertically |
| import androidx.compose.animation.fadeIn |
| import androidx.compose.animation.fadeOut |
| import androidx.compose.animation.shrinkVertically |
| import androidx.compose.foundation.interaction.Interaction |
| import androidx.compose.foundation.interaction.MutableInteractionSource |
| import androidx.compose.foundation.layout.Box |
| import androidx.compose.foundation.layout.Column |
| import androidx.compose.foundation.layout.ColumnScope |
| import androidx.compose.foundation.layout.PaddingValues |
| import androidx.compose.foundation.layout.WindowInsets |
| import androidx.compose.foundation.layout.asPaddingValues |
| import androidx.compose.foundation.layout.consumeWindowInsets |
| import androidx.compose.foundation.layout.exclude |
| import androidx.compose.foundation.layout.fillMaxWidth |
| import androidx.compose.foundation.layout.height |
| import androidx.compose.foundation.layout.heightIn |
| import androidx.compose.foundation.layout.offset |
| import androidx.compose.foundation.layout.onConsumedWindowInsetsChanged |
| import androidx.compose.foundation.layout.padding |
| import androidx.compose.foundation.layout.statusBars |
| import androidx.compose.foundation.layout.width |
| import androidx.compose.foundation.shape.GenericShape |
| import androidx.compose.foundation.text.BasicTextField |
| import androidx.compose.foundation.text.KeyboardActions |
| import androidx.compose.foundation.text.KeyboardOptions |
| import androidx.compose.foundation.text.selection.LocalTextSelectionColors |
| import androidx.compose.foundation.text.selection.TextSelectionColors |
| import androidx.compose.material3.SearchBarDefaults.InputFieldHeight |
| import androidx.compose.material3.tokens.FilledTextFieldTokens |
| import androidx.compose.material3.tokens.MotionTokens |
| import androidx.compose.material3.tokens.SearchBarTokens |
| import androidx.compose.material3.tokens.SearchViewTokens |
| import androidx.compose.runtime.Composable |
| import androidx.compose.runtime.Immutable |
| import androidx.compose.runtime.LaunchedEffect |
| import androidx.compose.runtime.Stable |
| import androidx.compose.runtime.State |
| import androidx.compose.runtime.derivedStateOf |
| import androidx.compose.runtime.getValue |
| import androidx.compose.runtime.mutableStateOf |
| import androidx.compose.runtime.remember |
| import androidx.compose.runtime.setValue |
| import androidx.compose.runtime.structuralEqualityPolicy |
| import androidx.compose.ui.Modifier |
| import androidx.compose.ui.focus.FocusRequester |
| import androidx.compose.ui.focus.focusRequester |
| import androidx.compose.ui.focus.onFocusChanged |
| import androidx.compose.ui.geometry.CornerRadius |
| import androidx.compose.ui.geometry.RoundRect |
| import androidx.compose.ui.geometry.toRect |
| import androidx.compose.ui.graphics.Color |
| import androidx.compose.ui.graphics.Shape |
| import androidx.compose.ui.graphics.SolidColor |
| import androidx.compose.ui.graphics.graphicsLayer |
| import androidx.compose.ui.graphics.takeOrElse |
| import androidx.compose.ui.layout.layout |
| import androidx.compose.ui.platform.LocalConfiguration |
| import androidx.compose.ui.platform.LocalDensity |
| import androidx.compose.ui.platform.LocalFocusManager |
| import androidx.compose.ui.semantics.contentDescription |
| import androidx.compose.ui.semantics.onClick |
| import androidx.compose.ui.semantics.semantics |
| import androidx.compose.ui.semantics.stateDescription |
| import androidx.compose.ui.text.TextStyle |
| import androidx.compose.ui.text.input.ImeAction |
| import androidx.compose.ui.text.input.VisualTransformation |
| import androidx.compose.ui.unit.Constraints |
| import androidx.compose.ui.unit.Density |
| import androidx.compose.ui.unit.Dp |
| import androidx.compose.ui.unit.IntSize |
| import androidx.compose.ui.unit.LayoutDirection |
| import androidx.compose.ui.unit.dp |
| import androidx.compose.ui.unit.lerp |
| import androidx.compose.ui.unit.offset |
| import androidx.compose.ui.util.lerp |
| import androidx.compose.ui.zIndex |
| import kotlin.math.max |
| import kotlin.math.min |
| import kotlinx.coroutines.delay |
| |
| /** |
| * <a href="https://m3.material.io/components/search/overview" class="external" target="_blank">Material Design search</a>. |
| * |
| * A search bar represents a floating search field that allows users to enter a keyword or phrase |
| * and get relevant information. It can be used as a way to navigate through an app via search |
| * queries. |
| * |
| * An active search bar expands into a search "view" and can be used to display dynamic suggestions. |
| * |
| * ![Search bar image](https://developer.android.com/images/reference/androidx/compose/material3/search-bar.png) |
| * |
| * A [SearchBar] expands to occupy the entirety of its allowed size when active. For full-screen |
| * behavior as specified by Material guidelines, parent layouts of the [SearchBar] must not pass |
| * any [Constraints] that limit its size, and the host activity should set |
| * `WindowCompat.setDecorFitsSystemWindows(window, false)`. |
| * |
| * If this expansion behavior is undesirable, for example on large tablet screens, [DockedSearchBar] |
| * can be used instead. |
| * |
| * An example looks like: |
| * @sample androidx.compose.material3.samples.SearchBarSample |
| * |
| * @param query the query text to be shown in the search bar's input field |
| * @param onQueryChange the callback to be invoked when the input service updates the query. An |
| * updated text comes as a parameter of the callback. |
| * @param onSearch the callback to be invoked when the input service triggers the [ImeAction.Search] |
| * action. The current [query] comes as a parameter of the callback. |
| * @param active whether this search bar is active |
| * @param onActiveChange the callback to be invoked when this search bar's active state is changed |
| * @param modifier the [Modifier] to be applied to this search bar |
| * @param enabled controls the enabled state of this search bar. When `false`, this component will |
| * not respond to user input, and it will appear visually disabled and disabled to accessibility |
| * services. |
| * @param placeholder the placeholder to be displayed when the search bar's [query] is empty. |
| * @param leadingIcon the leading icon to be displayed at the beginning of the search bar container |
| * @param trailingIcon the trailing icon to be displayed at the end of the search bar container |
| * @param shape the shape of this search bar when it is not [active]. When [active], the shape will |
| * always be [SearchBarDefaults.fullScreenShape]. |
| * @param colors [SearchBarColors] that will be used to resolve the colors used for this search bar |
| * in different states. See [SearchBarDefaults.colors]. |
| * @param tonalElevation when [SearchBarColors.containerColor] is [ColorScheme.surface], a |
| * translucent primary color overlay is applied on top of the container. A higher tonal elevation |
| * value will result in a darker color in light theme and lighter color in dark theme. See also: |
| * [Surface]. |
| * @param windowInsets the window insets that the search bar will respect |
| * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s |
| * for this search bar. You can create and pass in your own `remember`ed instance to observe |
| * [Interaction]s and customize the appearance / behavior of this search bar in different states. |
| * @param content the content of this search bar that will be displayed below the input field |
| */ |
| @ExperimentalMaterial3Api |
| @Composable |
| fun SearchBar( |
| query: String, |
| onQueryChange: (String) -> Unit, |
| onSearch: (String) -> Unit, |
| active: Boolean, |
| onActiveChange: (Boolean) -> Unit, |
| modifier: Modifier = Modifier, |
| enabled: Boolean = true, |
| placeholder: @Composable (() -> Unit)? = null, |
| leadingIcon: @Composable (() -> Unit)? = null, |
| trailingIcon: @Composable (() -> Unit)? = null, |
| shape: Shape = SearchBarDefaults.inputFieldShape, |
| colors: SearchBarColors = SearchBarDefaults.colors(), |
| tonalElevation: Dp = SearchBarDefaults.Elevation, |
| windowInsets: WindowInsets = SearchBarDefaults.windowInsets, |
| interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, |
| content: @Composable ColumnScope.() -> Unit, |
| ) { |
| val animationProgress: State<Float> = animateFloatAsState( |
| targetValue = if (active) 1f else 0f, |
| animationSpec = if (active) AnimationEnterFloatSpec else AnimationExitFloatSpec |
| ) |
| |
| val focusManager = LocalFocusManager.current |
| val density = LocalDensity.current |
| |
| val defaultInputFieldShape = SearchBarDefaults.inputFieldShape |
| val defaultFullScreenShape = SearchBarDefaults.fullScreenShape |
| val useFullScreenShape by remember { |
| derivedStateOf(structuralEqualityPolicy()) { animationProgress.value == 1f } |
| } |
| val animatedShape = remember(useFullScreenShape, shape) { |
| when { |
| shape == defaultInputFieldShape -> |
| // The shape can only be animated if it's the default spec value |
| GenericShape { size, _ -> |
| val radius = with(density) { |
| (SearchBarCornerRadius * (1 - animationProgress.value)).toPx() |
| } |
| addRoundRect(RoundRect(size.toRect(), CornerRadius(radius))) |
| } |
| useFullScreenShape -> defaultFullScreenShape |
| else -> shape |
| } |
| } |
| |
| // The main animation complexity is allowing the component to smoothly expand while keeping the |
| // input field at the same relative location on screen. `Modifier.windowInsetsPadding` does not |
| // support animation and thus is not suitable. Instead, we convert the insets to a padding |
| // applied to the Surface, which gradually becomes padding applied to the input field as the |
| // animation proceeds. |
| val unconsumedInsets = remember { MutableWindowInsets() } |
| val topPadding = remember(density) { |
| derivedStateOf { |
| SearchBarVerticalPadding + |
| unconsumedInsets.asPaddingValues(density).calculateTopPadding() |
| } |
| } |
| |
| Surface( |
| shape = animatedShape, |
| color = colors.containerColor, |
| contentColor = contentColorFor(colors.containerColor), |
| tonalElevation = tonalElevation, |
| modifier = modifier |
| .zIndex(1f) |
| .onConsumedWindowInsetsChanged { consumedInsets -> |
| unconsumedInsets.insets = windowInsets.exclude(consumedInsets) |
| } |
| .consumeWindowInsets(unconsumedInsets) |
| .layout { measurable, constraints -> |
| val animatedTopPadding = |
| lerp(topPadding.value, 0.dp, animationProgress.value).roundToPx() |
| |
| val startWidth = max(constraints.minWidth, SearchBarMinWidth.roundToPx()) |
| .coerceAtMost(min(constraints.maxWidth, SearchBarMaxWidth.roundToPx())) |
| val startHeight = max(constraints.minHeight, InputFieldHeight.roundToPx()) |
| .coerceAtMost(constraints.maxHeight) |
| val endWidth = constraints.maxWidth |
| val endHeight = constraints.maxHeight |
| |
| val width = lerp(startWidth, endWidth, animationProgress.value) |
| val height = |
| lerp(startHeight, endHeight, animationProgress.value) + animatedTopPadding |
| |
| val placeable = measurable.measure(Constraints.fixed(width, height) |
| .offset(vertical = -animatedTopPadding)) |
| layout(width, height) { |
| placeable.placeRelative(0, animatedTopPadding) |
| } |
| } |
| ) { |
| Column { |
| val animatedInputFieldPadding = remember { |
| AnimatedPaddingValues(animationProgress, topPadding) |
| } |
| SearchBarInputField( |
| query = query, |
| onQueryChange = onQueryChange, |
| onSearch = onSearch, |
| active = active, |
| onActiveChange = onActiveChange, |
| modifier = Modifier.padding(paddingValues = animatedInputFieldPadding), |
| enabled = enabled, |
| placeholder = placeholder, |
| leadingIcon = leadingIcon, |
| trailingIcon = trailingIcon, |
| colors = colors.inputFieldColors, |
| interactionSource = interactionSource, |
| ) |
| |
| val showResults by remember { |
| derivedStateOf(structuralEqualityPolicy()) { animationProgress.value > 0 } |
| } |
| if (showResults) { |
| Column(Modifier.graphicsLayer { alpha = animationProgress.value }) { |
| Divider(color = colors.dividerColor) |
| content() |
| } |
| } |
| } |
| } |
| |
| LaunchedEffect(active) { |
| if (!active) { |
| // 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() |
| } |
| } |
| |
| BackHandler(enabled = active) { |
| onActiveChange(false) |
| } |
| } |
| |
| /** |
| * <a href="https://m3.material.io/components/search/overview" class="external" target="_blank">Material Design search</a>. |
| * |
| * A search bar represents a floating search field that allows users to enter a keyword or phrase |
| * and get relevant information. It can be used as a way to navigate through an app via search |
| * queries. |
| * |
| * An active search bar expands into a search "view" and can be used to display dynamic suggestions. |
| * |
| * ![Search bar image](https://developer.android.com/images/reference/androidx/compose/material3/docked-search-bar.png) |
| * |
| * A [DockedSearchBar] displays search results in a bounded table below the input field. It is meant |
| * to be an alternative to [SearchBar] when expanding to full-screen size is undesirable on large |
| * screens such as tablets. |
| * |
| * An example looks like: |
| * @sample androidx.compose.material3.samples.DockedSearchBarSample |
| * |
| * @param query the query text to be shown in the search bar's input field |
| * @param onQueryChange the callback to be invoked when the input service updates the query. An |
| * updated text comes as a parameter of the callback. |
| * @param onSearch the callback to be invoked when the input service triggers the [ImeAction.Search] |
| * action. The current [query] comes as a parameter of the callback. |
| * @param active whether this search bar is active |
| * @param onActiveChange the callback to be invoked when this search bar's active state is changed |
| * @param modifier the [Modifier] to be applied to this search bar |
| * @param enabled controls the enabled state of this search bar. When `false`, this component will |
| * not respond to user input, and it will appear visually disabled and disabled to accessibility |
| * services. |
| * @param placeholder the placeholder to be displayed when the search bar's [query] is empty. |
| * @param leadingIcon the leading icon to be displayed at the beginning of the search bar container |
| * @param trailingIcon the trailing icon to be displayed at the end of the search bar container |
| * @param shape the shape of this search bar |
| * @param colors [SearchBarColors] that will be used to resolve the colors used for this search bar |
| * in different states. See [SearchBarDefaults.colors]. |
| * @param tonalElevation when [SearchBarColors.containerColor] is [ColorScheme.surface], a |
| * translucent primary color overlay is applied on top of the container. A higher tonal elevation |
| * value will result in a darker color in light theme and lighter color in dark theme. See also: |
| * [Surface]. |
| * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s |
| * for this search bar. You can create and pass in your own `remember`ed instance to observe |
| * [Interaction]s and customize the appearance / behavior of this search bar in different states. |
| * @param content the content of this search bar that will be displayed below the input field |
| */ |
| @ExperimentalMaterial3Api |
| @Composable |
| fun DockedSearchBar( |
| query: String, |
| onQueryChange: (String) -> Unit, |
| onSearch: (String) -> Unit, |
| active: Boolean, |
| onActiveChange: (Boolean) -> Unit, |
| modifier: Modifier = Modifier, |
| enabled: Boolean = true, |
| placeholder: @Composable (() -> Unit)? = null, |
| leadingIcon: @Composable (() -> Unit)? = null, |
| trailingIcon: @Composable (() -> Unit)? = null, |
| shape: Shape = SearchBarDefaults.dockedShape, |
| colors: SearchBarColors = SearchBarDefaults.colors(), |
| tonalElevation: Dp = SearchBarDefaults.Elevation, |
| interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, |
| content: @Composable ColumnScope.() -> Unit, |
| ) { |
| val focusManager = LocalFocusManager.current |
| |
| Surface( |
| shape = shape, |
| color = colors.containerColor, |
| contentColor = contentColorFor(colors.containerColor), |
| tonalElevation = tonalElevation, |
| modifier = modifier |
| .zIndex(1f) |
| .width(SearchBarMinWidth) |
| ) { |
| Column { |
| SearchBarInputField( |
| query = query, |
| onQueryChange = onQueryChange, |
| onSearch = onSearch, |
| active = active, |
| onActiveChange = onActiveChange, |
| enabled = enabled, |
| placeholder = placeholder, |
| leadingIcon = leadingIcon, |
| trailingIcon = trailingIcon, |
| colors = colors.inputFieldColors, |
| interactionSource = interactionSource, |
| ) |
| |
| AnimatedVisibility( |
| visible = active, |
| enter = DockedEnterTransition, |
| exit = DockedExitTransition, |
| ) { |
| val screenHeight = LocalConfiguration.current.screenHeightDp.dp |
| val maxHeight = remember(screenHeight) { |
| screenHeight * DockedActiveTableMaxHeightScreenRatio |
| } |
| val minHeight = remember(maxHeight) { |
| DockedActiveTableMinHeight.coerceAtMost(maxHeight) |
| } |
| |
| Column(Modifier.heightIn(min = minHeight, max = maxHeight)) { |
| Divider(color = colors.dividerColor) |
| content() |
| } |
| } |
| } |
| } |
| |
| LaunchedEffect(active) { |
| if (!active) { |
| // 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() |
| } |
| } |
| |
| BackHandler(enabled = active) { |
| onActiveChange(false) |
| } |
| } |
| |
| @OptIn(ExperimentalMaterial3Api::class) |
| @Composable |
| private fun SearchBarInputField( |
| query: String, |
| onQueryChange: (String) -> Unit, |
| onSearch: (String) -> Unit, |
| active: Boolean, |
| onActiveChange: (Boolean) -> Unit, |
| modifier: Modifier = Modifier, |
| enabled: Boolean = true, |
| placeholder: @Composable (() -> Unit)? = null, |
| leadingIcon: @Composable (() -> Unit)? = null, |
| trailingIcon: @Composable (() -> Unit)? = null, |
| colors: TextFieldColors = SearchBarDefaults.inputFieldColors(), |
| interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, |
| ) { |
| val focusRequester = remember { FocusRequester() } |
| val searchSemantics = getString(Strings.SearchBarSearch) |
| val suggestionsAvailableSemantics = getString(Strings.SuggestionsAvailable) |
| val textColor = LocalTextStyle.current.color.takeOrElse { |
| colors.textColor(enabled, isError = false, interactionSource = interactionSource).value |
| } |
| |
| BasicTextField( |
| value = query, |
| onValueChange = onQueryChange, |
| modifier = modifier |
| .height(InputFieldHeight) |
| .fillMaxWidth() |
| .focusRequester(focusRequester) |
| .onFocusChanged { if (it.isFocused) onActiveChange(true) } |
| .semantics { |
| contentDescription = searchSemantics |
| if (active) { |
| stateDescription = suggestionsAvailableSemantics |
| } |
| onClick { |
| focusRequester.requestFocus() |
| true |
| } |
| }, |
| enabled = enabled, |
| singleLine = true, |
| textStyle = LocalTextStyle.current.merge(TextStyle(color = textColor)), |
| cursorBrush = SolidColor(colors.cursorColor(isError = false).value), |
| keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), |
| keyboardActions = KeyboardActions(onSearch = { onSearch(query) }), |
| interactionSource = interactionSource, |
| decorationBox = @Composable { innerTextField -> |
| TextFieldDefaults.TextFieldDecorationBox( |
| value = query, |
| innerTextField = innerTextField, |
| enabled = enabled, |
| singleLine = true, |
| visualTransformation = VisualTransformation.None, |
| interactionSource = interactionSource, |
| placeholder = placeholder, |
| leadingIcon = leadingIcon?.let { leading -> { |
| Box(Modifier.offset(x = SearchBarIconOffsetX)) { leading() } |
| } }, |
| trailingIcon = trailingIcon?.let { trailing -> { |
| Box(Modifier.offset(x = -SearchBarIconOffsetX)) { trailing() } |
| } }, |
| shape = SearchBarDefaults.inputFieldShape, |
| colors = colors, |
| contentPadding = TextFieldDefaults.textFieldWithoutLabelPadding(), |
| container = {}, |
| ) |
| } |
| ) |
| } |
| |
| /** |
| * Defaults used in [SearchBar] and [DockedSearchBar]. |
| */ |
| @ExperimentalMaterial3Api |
| object SearchBarDefaults { |
| /** Default elevation for a search bar. */ |
| val Elevation: Dp = SearchBarTokens.ContainerElevation |
| |
| /** Default height for a search bar's input field, or a search bar in the inactive state. */ |
| val InputFieldHeight: Dp = SearchBarTokens.ContainerHeight |
| |
| /** Default shape for a search bar's input field, or a search bar in the inactive state. */ |
| val inputFieldShape: Shape @Composable get() = SearchBarTokens.ContainerShape.toShape() |
| |
| /** Default shape for a [SearchBar] in the active state. */ |
| val fullScreenShape: Shape |
| @Composable get() = SearchViewTokens.FullScreenContainerShape.toShape() |
| |
| /** Default shape for a [DockedSearchBar]. */ |
| val dockedShape: Shape @Composable get() = SearchViewTokens.DockedContainerShape.toShape() |
| |
| /** Default window insets for a [SearchBar]. */ |
| val windowInsets: WindowInsets @Composable get() = WindowInsets.statusBars |
| |
| /** |
| * Creates a [SearchBarColors] that represents the different colors used in parts of the |
| * search bar in different states. |
| * |
| * @param containerColor the container color of the search bar |
| * @param dividerColor the color of the divider between the input field and the search results |
| * @param inputFieldColors the colors of the input field |
| */ |
| @Composable |
| fun colors( |
| containerColor: Color = SearchBarTokens.ContainerColor.toColor(), |
| dividerColor: Color = SearchViewTokens.DividerColor.toColor(), |
| inputFieldColors: TextFieldColors = inputFieldColors(), |
| ): SearchBarColors = SearchBarColors( |
| containerColor = containerColor, |
| dividerColor = dividerColor, |
| inputFieldColors = inputFieldColors, |
| ) |
| |
| /** |
| * Creates a [TextFieldColors] that represents the different colors used in the search bar |
| * input field in different states. |
| * |
| * Only a subset of the full list of [TextFieldColors] parameters are used in the input field. |
| * All other parameters have no effect. |
| * |
| * @param focusedTextColor the color used for the input text of this input field when focused |
| * @param unfocusedTextColor the color used for the input text of this input field when not |
| * focused |
| * @param disabledTextColor the color used for the input text of this input field when disabled |
| * @param cursorColor the cursor color for this input field |
| * @param selectionColors the colors used when the input text of this input field is selected |
| * @param focusedLeadingIconColor the leading icon color for this input field when focused |
| * @param unfocusedLeadingIconColor the leading icon color for this input field when not focused |
| * @param disabledLeadingIconColor the leading icon color for this input field when disabled |
| * @param focusedTrailingIconColor the trailing icon color for this input field when focused |
| * @param unfocusedTrailingIconColor the trailing icon color for this input field when not |
| * focused |
| * @param disabledTrailingIconColor the trailing icon color for this input field when disabled |
| * @param focusedPlaceholderColor the placeholder color for this input field when focused |
| * @param unfocusedPlaceholderColor the placeholder color for this input field when not focused |
| * @param disabledPlaceholderColor the placeholder color for this input field when disabled |
| */ |
| @Composable |
| fun inputFieldColors( |
| focusedTextColor: Color = SearchBarTokens.InputTextColor.toColor(), |
| unfocusedTextColor: Color = SearchBarTokens.InputTextColor.toColor(), |
| disabledTextColor: Color = FilledTextFieldTokens.DisabledInputColor.toColor() |
| .copy(alpha = FilledTextFieldTokens.DisabledInputOpacity), |
| cursorColor: Color = FilledTextFieldTokens.CaretColor.toColor(), |
| selectionColors: TextSelectionColors = LocalTextSelectionColors.current, |
| focusedLeadingIconColor: Color = SearchBarTokens.LeadingIconColor.toColor(), |
| unfocusedLeadingIconColor: Color = SearchBarTokens.LeadingIconColor.toColor(), |
| disabledLeadingIconColor: Color = FilledTextFieldTokens.DisabledLeadingIconColor |
| .toColor().copy(alpha = FilledTextFieldTokens.DisabledLeadingIconOpacity), |
| focusedTrailingIconColor: Color = SearchBarTokens.TrailingIconColor.toColor(), |
| unfocusedTrailingIconColor: Color = SearchBarTokens.TrailingIconColor.toColor(), |
| disabledTrailingIconColor: Color = FilledTextFieldTokens.DisabledTrailingIconColor |
| .toColor().copy(alpha = FilledTextFieldTokens.DisabledTrailingIconOpacity), |
| focusedPlaceholderColor: Color = SearchBarTokens.SupportingTextColor.toColor(), |
| unfocusedPlaceholderColor: Color = SearchBarTokens.SupportingTextColor.toColor(), |
| disabledPlaceholderColor: Color = FilledTextFieldTokens.DisabledInputColor.toColor() |
| .copy(alpha = FilledTextFieldTokens.DisabledInputOpacity), |
| ): TextFieldColors = |
| TextFieldDefaults.textFieldColors( |
| focusedTextColor = focusedTextColor, |
| unfocusedTextColor = unfocusedTextColor, |
| disabledTextColor = disabledTextColor, |
| cursorColor = cursorColor, |
| selectionColors = selectionColors, |
| focusedLeadingIconColor = focusedLeadingIconColor, |
| unfocusedLeadingIconColor = unfocusedLeadingIconColor, |
| disabledLeadingIconColor = disabledLeadingIconColor, |
| focusedTrailingIconColor = focusedTrailingIconColor, |
| unfocusedTrailingIconColor = unfocusedTrailingIconColor, |
| disabledTrailingIconColor = disabledTrailingIconColor, |
| focusedPlaceholderColor = focusedPlaceholderColor, |
| unfocusedPlaceholderColor = unfocusedPlaceholderColor, |
| disabledPlaceholderColor = disabledPlaceholderColor, |
| ) |
| |
| @Deprecated("Maintained for binary compatibility", level = DeprecationLevel.HIDDEN) |
| @Composable |
| fun inputFieldColors( |
| textColor: Color = SearchBarTokens.InputTextColor.toColor(), |
| disabledTextColor: Color = FilledTextFieldTokens.DisabledInputColor.toColor() |
| .copy(alpha = FilledTextFieldTokens.DisabledInputOpacity), |
| cursorColor: Color = FilledTextFieldTokens.CaretColor.toColor(), |
| selectionColors: TextSelectionColors = LocalTextSelectionColors.current, |
| focusedLeadingIconColor: Color = SearchBarTokens.LeadingIconColor.toColor(), |
| unfocusedLeadingIconColor: Color = SearchBarTokens.LeadingIconColor.toColor(), |
| disabledLeadingIconColor: Color = FilledTextFieldTokens.DisabledLeadingIconColor |
| .toColor().copy(alpha = FilledTextFieldTokens.DisabledLeadingIconOpacity), |
| focusedTrailingIconColor: Color = SearchBarTokens.TrailingIconColor.toColor(), |
| unfocusedTrailingIconColor: Color = SearchBarTokens.TrailingIconColor.toColor(), |
| disabledTrailingIconColor: Color = FilledTextFieldTokens.DisabledTrailingIconColor |
| .toColor().copy(alpha = FilledTextFieldTokens.DisabledTrailingIconOpacity), |
| placeholderColor: Color = SearchBarTokens.SupportingTextColor.toColor(), |
| disabledPlaceholderColor: Color = FilledTextFieldTokens.DisabledInputColor.toColor() |
| .copy(alpha = FilledTextFieldTokens.DisabledInputOpacity), |
| ) = inputFieldColors( |
| focusedTextColor = textColor, |
| unfocusedTextColor = textColor, |
| disabledTextColor = disabledTextColor, |
| cursorColor = cursorColor, |
| selectionColors = selectionColors, |
| focusedLeadingIconColor = focusedLeadingIconColor, |
| unfocusedLeadingIconColor = unfocusedLeadingIconColor, |
| disabledLeadingIconColor = disabledLeadingIconColor, |
| focusedTrailingIconColor = focusedTrailingIconColor, |
| unfocusedTrailingIconColor = unfocusedTrailingIconColor, |
| disabledTrailingIconColor = disabledTrailingIconColor, |
| focusedPlaceholderColor = placeholderColor, |
| unfocusedPlaceholderColor = placeholderColor, |
| disabledPlaceholderColor = disabledPlaceholderColor, |
| ) |
| } |
| |
| /** |
| * Represents the colors used by a search bar in different states. |
| * |
| * See [SearchBarDefaults.colors] for the default implementation that follows Material |
| * specifications. |
| */ |
| @ExperimentalMaterial3Api |
| @Immutable |
| class SearchBarColors internal constructor( |
| val containerColor: Color, |
| val dividerColor: Color, |
| val inputFieldColors: TextFieldColors, |
| ) { |
| override fun equals(other: Any?): Boolean { |
| if (this === other) return true |
| if (javaClass != other?.javaClass) return false |
| |
| other as SearchBarColors |
| |
| if (containerColor != other.containerColor) return false |
| if (dividerColor != other.dividerColor) return false |
| if (inputFieldColors != other.inputFieldColors) return false |
| |
| return true |
| } |
| |
| override fun hashCode(): Int { |
| var result = containerColor.hashCode() |
| result = 31 * result + dividerColor.hashCode() |
| result = 31 * result + inputFieldColors.hashCode() |
| return result |
| } |
| } |
| |
| @Stable |
| private class AnimatedPaddingValues( |
| val animationProgress: State<Float>, |
| val topPadding: State<Dp>, |
| ) : PaddingValues { |
| override fun calculateTopPadding(): Dp = topPadding.value * animationProgress.value |
| override fun calculateBottomPadding(): Dp = SearchBarVerticalPadding * animationProgress.value |
| |
| override fun calculateLeftPadding(layoutDirection: LayoutDirection): Dp = 0.dp |
| override fun calculateRightPadding(layoutDirection: LayoutDirection): Dp = 0.dp |
| } |
| |
| /** |
| * A [WindowInsets] whose values can change without changing the instance. This is useful |
| * to avoid recomposition when [WindowInsets] can change. |
| * |
| * Copied from [androidx.compose.foundation.layout.MutableWindowInsets], which is marked as |
| * experimental and thus cannot be used cross-module. |
| */ |
| private class MutableWindowInsets( |
| initialInsets: WindowInsets = WindowInsets(0, 0, 0, 0) |
| ) : WindowInsets { |
| /** |
| * The [WindowInsets] that are used for [left][getLeft], [top][getTop], [right][getRight], |
| * and [bottom][getBottom] values. |
| */ |
| var insets by mutableStateOf(initialInsets) |
| |
| override fun getLeft(density: Density, layoutDirection: LayoutDirection): Int = |
| insets.getLeft(density, layoutDirection) |
| override fun getTop(density: Density): Int = insets.getTop(density) |
| override fun getRight(density: Density, layoutDirection: LayoutDirection): Int = |
| insets.getRight(density, layoutDirection) |
| override fun getBottom(density: Density): Int = insets.getBottom(density) |
| } |
| |
| // Measurement specs |
| @OptIn(ExperimentalMaterial3Api::class) |
| private val SearchBarCornerRadius: Dp = InputFieldHeight / 2 |
| internal val DockedActiveTableMinHeight: Dp = 240.dp |
| private const val DockedActiveTableMaxHeightScreenRatio: Float = 2f / 3f |
| internal val SearchBarMinWidth: Dp = 360.dp |
| private val SearchBarMaxWidth: Dp = 720.dp |
| internal val SearchBarVerticalPadding: Dp = 8.dp |
| // Search bar has 16dp padding between icons and start/end, while by default text field has 12dp. |
| private val SearchBarIconOffsetX: Dp = 4.dp |
| |
| // Animation specs |
| private const val AnimationEnterDurationMillis: Int = MotionTokens.DurationLong4.toInt() |
| private const val AnimationExitDurationMillis: Int = MotionTokens.DurationMedium3.toInt() |
| private const val AnimationDelayMillis: Int = MotionTokens.DurationShort2.toInt() |
| private val AnimationEnterEasing = MotionTokens.EasingEmphasizedDecelerateCubicBezier |
| private val AnimationExitEasing = CubicBezierEasing(0.0f, 1.0f, 0.0f, 1.0f) |
| private val AnimationEnterFloatSpec: FiniteAnimationSpec<Float> = tween( |
| durationMillis = AnimationEnterDurationMillis, |
| delayMillis = AnimationDelayMillis, |
| easing = AnimationEnterEasing, |
| ) |
| private val AnimationExitFloatSpec: FiniteAnimationSpec<Float> = tween( |
| durationMillis = AnimationExitDurationMillis, |
| delayMillis = AnimationDelayMillis, |
| easing = AnimationExitEasing, |
| ) |
| private val AnimationEnterSizeSpec: FiniteAnimationSpec<IntSize> = tween( |
| durationMillis = AnimationEnterDurationMillis, |
| delayMillis = AnimationDelayMillis, |
| easing = AnimationEnterEasing, |
| ) |
| private val AnimationExitSizeSpec: FiniteAnimationSpec<IntSize> = tween( |
| durationMillis = AnimationExitDurationMillis, |
| delayMillis = AnimationDelayMillis, |
| easing = AnimationExitEasing, |
| ) |
| private val DockedEnterTransition: EnterTransition = |
| fadeIn(AnimationEnterFloatSpec) + expandVertically(AnimationEnterSizeSpec) |
| private val DockedExitTransition: ExitTransition = |
| fadeOut(AnimationExitFloatSpec) + shrinkVertically(AnimationExitSizeSpec) |