| /* |
| * 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.compose.foundation.interaction.Interaction |
| import androidx.compose.foundation.interaction.MutableInteractionSource |
| import androidx.compose.foundation.layout.Box |
| import androidx.compose.foundation.layout.PaddingValues |
| import androidx.compose.foundation.layout.calculateEndPadding |
| import androidx.compose.foundation.layout.calculateStartPadding |
| import androidx.compose.foundation.layout.defaultMinSize |
| import androidx.compose.foundation.layout.heightIn |
| import androidx.compose.foundation.layout.padding |
| import androidx.compose.foundation.layout.wrapContentHeight |
| 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.runtime.Composable |
| import androidx.compose.runtime.CompositionLocalProvider |
| import androidx.compose.runtime.remember |
| import androidx.compose.ui.Alignment |
| import androidx.compose.ui.Modifier |
| import androidx.compose.ui.draw.drawWithContent |
| import androidx.compose.ui.geometry.Size |
| import androidx.compose.ui.graphics.ClipOp |
| import androidx.compose.ui.graphics.Shape |
| import androidx.compose.ui.graphics.SolidColor |
| import androidx.compose.ui.graphics.drawscope.clipRect |
| import androidx.compose.ui.graphics.takeOrElse |
| import androidx.compose.ui.layout.IntrinsicMeasurable |
| import androidx.compose.ui.layout.IntrinsicMeasureScope |
| import androidx.compose.ui.layout.Layout |
| import androidx.compose.ui.layout.Measurable |
| import androidx.compose.ui.layout.MeasurePolicy |
| import androidx.compose.ui.layout.MeasureResult |
| import androidx.compose.ui.layout.MeasureScope |
| import androidx.compose.ui.layout.Placeable |
| import androidx.compose.ui.layout.layoutId |
| import androidx.compose.ui.platform.LocalLayoutDirection |
| import androidx.compose.ui.semantics.semantics |
| import androidx.compose.ui.text.TextStyle |
| import androidx.compose.ui.text.input.ImeAction |
| import androidx.compose.ui.text.input.KeyboardType |
| import androidx.compose.ui.text.input.TextFieldValue |
| import androidx.compose.ui.text.input.VisualTransformation |
| import androidx.compose.ui.unit.Constraints |
| import androidx.compose.ui.unit.IntOffset |
| import androidx.compose.ui.unit.LayoutDirection |
| import androidx.compose.ui.unit.coerceAtLeast |
| 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 kotlin.math.max |
| import kotlin.math.roundToInt |
| |
| /** |
| * <a href="https://m3.material.io/components/text-fields/overview" class="external" target="_blank">Material Design outlined text field</a>. |
| * |
| * Text fields allow users to enter text into a UI. They typically appear in forms and dialogs. |
| * Outlined text fields have less visual emphasis than filled text fields. When they appear in |
| * places like forms, where many text fields are placed together, their reduced emphasis helps |
| * simplify the layout. |
| * |
| * ![Outlined text field image](https://developer.android.com/images/reference/androidx/compose/material3/outlined-text-field.png) |
| * |
| * See example usage: |
| * @sample androidx.compose.material3.samples.SimpleOutlinedTextFieldSample |
| * |
| * If apart from input text change you also want to observe the cursor location, selection range, |
| * or IME composition use the OutlinedTextField overload with the [TextFieldValue] parameter |
| * instead. |
| * |
| * @param value the input text to be shown in the text field |
| * @param onValueChange the callback that is triggered when the input service updates the text. An |
| * updated text comes as a parameter of the callback |
| * @param modifier the [Modifier] to be applied to this text field |
| * @param enabled controls the enabled state of this text 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 text field. When `true`, the text field cannot |
| * be modified. However, a user can focus it and copy text from it. Read-only text fields are |
| * usually used to display pre-filled forms that a user cannot edit. |
| * @param textStyle the style to be applied to the input text. Defaults to [LocalTextStyle]. |
| * @param label the optional label to be displayed inside the text field container. The default |
| * text style for internal [Text] is [Typography.bodySmall] when the text field is in focus and |
| * [Typography.bodyLarge] when the text field is not in focus |
| * @param placeholder the optional placeholder to be displayed when the text field is in focus and |
| * the input text is empty. The default text style for internal [Text] is [Typography.bodyLarge] |
| * @param leadingIcon the optional leading icon to be displayed at the beginning of the text field |
| * container |
| * @param trailingIcon the optional trailing icon to be displayed at the end of the text field |
| * container |
| * @param prefix the optional prefix to be displayed before the input text in the text field |
| * @param suffix the optional suffix to be displayed after the input text in the text field |
| * @param supportingText the optional supporting text to be displayed below the text field |
| * @param isError indicates if the text field's current value is in error. If set to true, the |
| * label, bottom indicator and trailing icon by default will be displayed in error color |
| * @param visualTransformation transforms the visual representation of the input [value] |
| * For example, you can use |
| * [PasswordVisualTransformation][androidx.compose.ui.text.input.PasswordVisualTransformation] to |
| * create a password text field. By default, no visual transformation is applied. |
| * @param keyboardOptions software keyboard options that contains configuration such as |
| * [KeyboardType] and [ImeAction] |
| * @param keyboardActions when the input service emits an IME action, the corresponding callback |
| * is called. Note that this IME action may be different from what you specified in |
| * [KeyboardOptions.imeAction] |
| * @param singleLine when `true`, this text field becomes a single horizontally scrolling text field |
| * instead of wrapping onto multiple lines. The keyboard will be informed to not show the return key |
| * as the [ImeAction]. Note that [maxLines] parameter will be ignored as the maxLines attribute will |
| * be automatically set to 1. |
| * @param maxLines the maximum height in terms of maximum number of visible lines. It is required |
| * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true. |
| * @param minLines the minimum height in terms of minimum number of visible lines. It is required |
| * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true. |
| * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s |
| * for this text field. You can create and pass in your own `remember`ed instance to observe |
| * [Interaction]s and customize the appearance / behavior of this text field in different states. |
| * @param shape defines the shape of this text field's border |
| * @param colors [TextFieldColors] that will be used to resolve the colors used for this text field |
| * in different states. See [TextFieldDefaults.outlinedTextFieldColors]. |
| */ |
| @OptIn(ExperimentalMaterial3Api::class) |
| @Composable |
| fun OutlinedTextField( |
| value: String, |
| onValueChange: (String) -> Unit, |
| modifier: Modifier = Modifier, |
| enabled: Boolean = true, |
| readOnly: Boolean = false, |
| textStyle: TextStyle = LocalTextStyle.current, |
| label: @Composable (() -> Unit)? = null, |
| placeholder: @Composable (() -> Unit)? = null, |
| leadingIcon: @Composable (() -> Unit)? = null, |
| trailingIcon: @Composable (() -> Unit)? = null, |
| prefix: @Composable (() -> Unit)? = null, |
| suffix: @Composable (() -> Unit)? = null, |
| supportingText: @Composable (() -> Unit)? = null, |
| isError: Boolean = false, |
| visualTransformation: VisualTransformation = VisualTransformation.None, |
| keyboardOptions: KeyboardOptions = KeyboardOptions.Default, |
| keyboardActions: KeyboardActions = KeyboardActions.Default, |
| singleLine: Boolean = false, |
| maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, |
| minLines: Int = 1, |
| interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, |
| shape: Shape = TextFieldDefaults.outlinedShape, |
| colors: TextFieldColors = TextFieldDefaults.outlinedTextFieldColors() |
| ) { |
| // If color is not provided via the text style, use content color as a default |
| val textColor = textStyle.color.takeOrElse { |
| colors.textColor(enabled, isError, interactionSource).value |
| } |
| val mergedTextStyle = textStyle.merge(TextStyle(color = textColor)) |
| |
| CompositionLocalProvider(LocalTextSelectionColors provides colors.selectionColors) { |
| BasicTextField( |
| value = value, |
| modifier = if (label != null) { |
| modifier |
| // Merge semantics at the beginning of the modifier chain to ensure padding is |
| // considered part of the text field. |
| .semantics(mergeDescendants = true) {} |
| .padding(top = OutlinedTextFieldTopPadding) |
| } else { |
| modifier |
| } |
| .defaultMinSize( |
| minWidth = TextFieldDefaults.MinWidth, |
| minHeight = TextFieldDefaults.MinHeight |
| ), |
| onValueChange = onValueChange, |
| enabled = enabled, |
| readOnly = readOnly, |
| textStyle = mergedTextStyle, |
| cursorBrush = SolidColor(colors.cursorColor(isError).value), |
| visualTransformation = visualTransformation, |
| keyboardOptions = keyboardOptions, |
| keyboardActions = keyboardActions, |
| interactionSource = interactionSource, |
| singleLine = singleLine, |
| maxLines = maxLines, |
| minLines = minLines, |
| decorationBox = @Composable { innerTextField -> |
| TextFieldDefaults.OutlinedTextFieldDecorationBox( |
| value = value, |
| visualTransformation = visualTransformation, |
| innerTextField = innerTextField, |
| placeholder = placeholder, |
| label = label, |
| leadingIcon = leadingIcon, |
| trailingIcon = trailingIcon, |
| prefix = prefix, |
| suffix = suffix, |
| supportingText = supportingText, |
| singleLine = singleLine, |
| enabled = enabled, |
| isError = isError, |
| interactionSource = interactionSource, |
| colors = colors, |
| container = { |
| TextFieldDefaults.OutlinedBorderContainerBox( |
| enabled, |
| isError, |
| interactionSource, |
| colors, |
| shape |
| ) |
| } |
| ) |
| } |
| ) |
| } |
| } |
| |
| /** |
| * <a href="https://m3.material.io/components/text-fields/overview" class="external" target="_blank">Material Design outlined text field</a>. |
| * |
| * Text fields allow users to enter text into a UI. They typically appear in forms and dialogs. |
| * Outlined text fields have less visual emphasis than filled text fields. When they appear in |
| * places like forms, where many text fields are placed together, their reduced emphasis helps |
| * simplify the layout. |
| * |
| * ![Outlined text field image](https://developer.android.com/images/reference/androidx/compose/material3/outlined-text-field.png) |
| * |
| * See example usage: |
| * @sample androidx.compose.material3.samples.OutlinedTextFieldSample |
| * |
| * This overload provides access to the input text, cursor position and selection range and |
| * IME composition. If you only want to observe an input text change, use the OutlinedTextField |
| * overload with the [String] parameter instead. |
| * |
| * @param value the input [TextFieldValue] to be shown in the text field |
| * @param onValueChange the callback that is triggered when the input service updates values in |
| * [TextFieldValue]. An updated [TextFieldValue] comes as a parameter of the callback |
| * @param modifier the [Modifier] to be applied to this text field |
| * @param enabled controls the enabled state of this text 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 text field. When `true`, the text field cannot |
| * be modified. However, a user can focus it and copy text from it. Read-only text fields are |
| * usually used to display pre-filled forms that a user cannot edit. |
| * @param textStyle the style to be applied to the input text. Defaults to [LocalTextStyle]. |
| * @param label the optional label to be displayed inside the text field container. The default |
| * text style for internal [Text] is [Typography.bodySmall] when the text field is in focus and |
| * [Typography.bodyLarge] when the text field is not in focus |
| * @param placeholder the optional placeholder to be displayed when the text field is in focus and |
| * the input text is empty. The default text style for internal [Text] is [Typography.bodyLarge] |
| * @param leadingIcon the optional leading icon to be displayed at the beginning of the text field |
| * container |
| * @param trailingIcon the optional trailing icon to be displayed at the end of the text field |
| * container |
| * @param prefix the optional prefix to be displayed before the input text in the text field |
| * @param suffix the optional suffix to be displayed after the input text in the text field |
| * @param supportingText the optional supporting text to be displayed below the text field |
| * @param isError indicates if the text field's current value is in error state. If set to |
| * true, the label, bottom indicator and trailing icon by default will be displayed in error color |
| * @param visualTransformation transforms the visual representation of the input [value] |
| * For example, you can use |
| * [PasswordVisualTransformation][androidx.compose.ui.text.input.PasswordVisualTransformation] to |
| * create a password text field. By default, no visual transformation is applied. |
| * @param keyboardOptions software keyboard options that contains configuration such as |
| * [KeyboardType] and [ImeAction] |
| * @param keyboardActions when the input service emits an IME action, the corresponding callback |
| * is called. Note that this IME action may be different from what you specified in |
| * [KeyboardOptions.imeAction] |
| * @param singleLine when `true`, this text field becomes a single horizontally scrolling text field |
| * instead of wrapping onto multiple lines. The keyboard will be informed to not show the return key |
| * as the [ImeAction]. Note that [maxLines] parameter will be ignored as the maxLines attribute will |
| * be automatically set to 1. |
| * @param maxLines the maximum height in terms of maximum number of visible lines. It is required |
| * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true. |
| * @param minLines the minimum height in terms of minimum number of visible lines. It is required |
| * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true. |
| * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s |
| * for this text field. You can create and pass in your own `remember`ed instance to observe |
| * [Interaction]s and customize the appearance / behavior of this text field in different states. |
| * @param shape defines the shape of this text field's border |
| * @param colors [TextFieldColors] that will be used to resolve the colors used for this text field |
| * in different states. See [TextFieldDefaults.outlinedTextFieldColors]. |
| */ |
| @OptIn(ExperimentalMaterial3Api::class) |
| @Composable |
| fun OutlinedTextField( |
| value: TextFieldValue, |
| onValueChange: (TextFieldValue) -> Unit, |
| modifier: Modifier = Modifier, |
| enabled: Boolean = true, |
| readOnly: Boolean = false, |
| textStyle: TextStyle = LocalTextStyle.current, |
| label: @Composable (() -> Unit)? = null, |
| placeholder: @Composable (() -> Unit)? = null, |
| leadingIcon: @Composable (() -> Unit)? = null, |
| trailingIcon: @Composable (() -> Unit)? = null, |
| prefix: @Composable (() -> Unit)? = null, |
| suffix: @Composable (() -> Unit)? = null, |
| supportingText: @Composable (() -> Unit)? = null, |
| isError: Boolean = false, |
| visualTransformation: VisualTransformation = VisualTransformation.None, |
| keyboardOptions: KeyboardOptions = KeyboardOptions.Default, |
| keyboardActions: KeyboardActions = KeyboardActions.Default, |
| singleLine: Boolean = false, |
| maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, |
| minLines: Int = 1, |
| interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, |
| shape: Shape = TextFieldDefaults.outlinedShape, |
| colors: TextFieldColors = TextFieldDefaults.outlinedTextFieldColors() |
| ) { |
| // If color is not provided via the text style, use content color as a default |
| val textColor = textStyle.color.takeOrElse { |
| colors.textColor(enabled, isError, interactionSource).value |
| } |
| val mergedTextStyle = textStyle.merge(TextStyle(color = textColor)) |
| |
| CompositionLocalProvider(LocalTextSelectionColors provides colors.selectionColors) { |
| BasicTextField( |
| value = value, |
| modifier = if (label != null) { |
| modifier |
| // Merge semantics at the beginning of the modifier chain to ensure padding is |
| // considered part of the text field. |
| .semantics(mergeDescendants = true) {} |
| .padding(top = OutlinedTextFieldTopPadding) |
| } else { |
| modifier |
| } |
| .defaultMinSize( |
| minWidth = TextFieldDefaults.MinWidth, |
| minHeight = TextFieldDefaults.MinHeight |
| ), |
| onValueChange = onValueChange, |
| enabled = enabled, |
| readOnly = readOnly, |
| textStyle = mergedTextStyle, |
| cursorBrush = SolidColor(colors.cursorColor(isError).value), |
| visualTransformation = visualTransformation, |
| keyboardOptions = keyboardOptions, |
| keyboardActions = keyboardActions, |
| interactionSource = interactionSource, |
| singleLine = singleLine, |
| maxLines = maxLines, |
| minLines = minLines, |
| decorationBox = @Composable { innerTextField -> |
| TextFieldDefaults.OutlinedTextFieldDecorationBox( |
| value = value.text, |
| visualTransformation = visualTransformation, |
| innerTextField = innerTextField, |
| placeholder = placeholder, |
| label = label, |
| leadingIcon = leadingIcon, |
| trailingIcon = trailingIcon, |
| prefix = prefix, |
| suffix = suffix, |
| supportingText = supportingText, |
| singleLine = singleLine, |
| enabled = enabled, |
| isError = isError, |
| interactionSource = interactionSource, |
| colors = colors, |
| container = { |
| TextFieldDefaults.OutlinedBorderContainerBox( |
| enabled, |
| isError, |
| interactionSource, |
| colors, |
| shape |
| ) |
| } |
| ) |
| } |
| ) |
| } |
| } |
| |
| @Deprecated("Use overload with prefix and suffix parameters", level = DeprecationLevel.HIDDEN) |
| @ExperimentalMaterial3Api |
| @Composable |
| fun OutlinedTextField( |
| value: String, |
| onValueChange: (String) -> Unit, |
| modifier: Modifier = Modifier, |
| enabled: Boolean = true, |
| readOnly: Boolean = false, |
| textStyle: TextStyle = LocalTextStyle.current, |
| label: @Composable (() -> Unit)? = null, |
| placeholder: @Composable (() -> Unit)? = null, |
| leadingIcon: @Composable (() -> Unit)? = null, |
| trailingIcon: @Composable (() -> Unit)? = null, |
| supportingText: @Composable (() -> Unit)? = null, |
| isError: Boolean = false, |
| visualTransformation: VisualTransformation = VisualTransformation.None, |
| keyboardOptions: KeyboardOptions = KeyboardOptions.Default, |
| keyboardActions: KeyboardActions = KeyboardActions.Default, |
| singleLine: Boolean = false, |
| maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, |
| minLines: Int = 1, |
| interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, |
| shape: Shape = TextFieldDefaults.outlinedShape, |
| colors: TextFieldColors = TextFieldDefaults.outlinedTextFieldColors() |
| ) { |
| OutlinedTextField( |
| value = value, |
| onValueChange = onValueChange, |
| modifier = modifier, |
| enabled = enabled, |
| readOnly = readOnly, |
| textStyle = textStyle, |
| label = label, |
| placeholder = placeholder, |
| leadingIcon = leadingIcon, |
| trailingIcon = trailingIcon, |
| prefix = null, |
| suffix = null, |
| supportingText = supportingText, |
| isError = isError, |
| visualTransformation = visualTransformation, |
| keyboardOptions = keyboardOptions, |
| keyboardActions = keyboardActions, |
| singleLine = singleLine, |
| maxLines = maxLines, |
| minLines = minLines, |
| interactionSource = interactionSource, |
| shape = shape, |
| colors = colors, |
| ) |
| } |
| |
| @Deprecated("Use overload with prefix and suffix parameters", level = DeprecationLevel.HIDDEN) |
| @ExperimentalMaterial3Api |
| @Composable |
| fun OutlinedTextField( |
| value: TextFieldValue, |
| onValueChange: (TextFieldValue) -> Unit, |
| modifier: Modifier = Modifier, |
| enabled: Boolean = true, |
| readOnly: Boolean = false, |
| textStyle: TextStyle = LocalTextStyle.current, |
| label: @Composable (() -> Unit)? = null, |
| placeholder: @Composable (() -> Unit)? = null, |
| leadingIcon: @Composable (() -> Unit)? = null, |
| trailingIcon: @Composable (() -> Unit)? = null, |
| supportingText: @Composable (() -> Unit)? = null, |
| isError: Boolean = false, |
| visualTransformation: VisualTransformation = VisualTransformation.None, |
| keyboardOptions: KeyboardOptions = KeyboardOptions.Default, |
| keyboardActions: KeyboardActions = KeyboardActions.Default, |
| singleLine: Boolean = false, |
| maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, |
| minLines: Int = 1, |
| interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, |
| shape: Shape = TextFieldDefaults.outlinedShape, |
| colors: TextFieldColors = TextFieldDefaults.outlinedTextFieldColors() |
| ) { |
| OutlinedTextField( |
| value = value, |
| onValueChange = onValueChange, |
| modifier = modifier, |
| enabled = enabled, |
| readOnly = readOnly, |
| textStyle = textStyle, |
| label = label, |
| placeholder = placeholder, |
| leadingIcon = leadingIcon, |
| trailingIcon = trailingIcon, |
| prefix = null, |
| suffix = null, |
| supportingText = supportingText, |
| isError = isError, |
| visualTransformation = visualTransformation, |
| keyboardOptions = keyboardOptions, |
| keyboardActions = keyboardActions, |
| singleLine = singleLine, |
| maxLines = maxLines, |
| minLines = minLines, |
| interactionSource = interactionSource, |
| shape = shape, |
| colors = colors, |
| ) |
| } |
| |
| /** |
| * Layout of the leading and trailing icons and the text field, label and placeholder in |
| * [OutlinedTextField]. |
| * It doesn't use Row to position the icons and middle part because label should not be |
| * positioned in the middle part. |
| */ |
| @Composable |
| internal fun OutlinedTextFieldLayout( |
| modifier: Modifier, |
| textField: @Composable () -> Unit, |
| placeholder: @Composable ((Modifier) -> Unit)?, |
| label: @Composable (() -> Unit)?, |
| leading: @Composable (() -> Unit)?, |
| trailing: @Composable (() -> Unit)?, |
| prefix: @Composable (() -> Unit)?, |
| suffix: @Composable (() -> Unit)?, |
| singleLine: Boolean, |
| animationProgress: Float, |
| onLabelMeasured: (Size) -> Unit, |
| container: @Composable () -> Unit, |
| supporting: @Composable (() -> Unit)?, |
| paddingValues: PaddingValues |
| ) { |
| val measurePolicy = remember(onLabelMeasured, singleLine, animationProgress, paddingValues) { |
| OutlinedTextFieldMeasurePolicy( |
| onLabelMeasured, |
| singleLine, |
| animationProgress, |
| paddingValues |
| ) |
| } |
| val layoutDirection = LocalLayoutDirection.current |
| Layout( |
| modifier = modifier, |
| content = { |
| container() |
| |
| if (leading != null) { |
| Box( |
| modifier = Modifier.layoutId(LeadingId).then(IconDefaultSizeModifier), |
| contentAlignment = Alignment.Center |
| ) { |
| leading() |
| } |
| } |
| if (trailing != null) { |
| Box( |
| modifier = Modifier.layoutId(TrailingId).then(IconDefaultSizeModifier), |
| contentAlignment = Alignment.Center |
| ) { |
| trailing() |
| } |
| } |
| |
| val startTextFieldPadding = paddingValues.calculateStartPadding(layoutDirection) |
| val endTextFieldPadding = paddingValues.calculateEndPadding(layoutDirection) |
| |
| val startPadding = if (leading != null) { |
| (startTextFieldPadding - HorizontalIconPadding).coerceAtLeast(0.dp) |
| } else { |
| startTextFieldPadding |
| } |
| val endPadding = if (trailing != null) { |
| (endTextFieldPadding - HorizontalIconPadding).coerceAtLeast(0.dp) |
| } else { |
| endTextFieldPadding |
| } |
| |
| if (prefix != null) { |
| Box( |
| Modifier |
| .layoutId(PrefixId) |
| .heightIn(min = MinTextLineHeight) |
| .wrapContentHeight() |
| .padding(start = startPadding, end = PrefixSuffixTextPadding) |
| ) { |
| prefix() |
| } |
| } |
| if (suffix != null) { |
| Box( |
| Modifier |
| .layoutId(SuffixId) |
| .heightIn(min = MinTextLineHeight) |
| .wrapContentHeight() |
| .padding(start = PrefixSuffixTextPadding, end = endPadding) |
| ) { |
| suffix() |
| } |
| } |
| |
| val textPadding = Modifier |
| .heightIn(min = MinTextLineHeight) |
| .wrapContentHeight() |
| .padding( |
| start = if (prefix == null) startPadding else 0.dp, |
| end = if (suffix == null) endPadding else 0.dp, |
| ) |
| |
| if (placeholder != null) { |
| placeholder(Modifier |
| .layoutId(PlaceholderId) |
| .then(textPadding)) |
| } |
| |
| Box( |
| modifier = Modifier |
| .layoutId(TextFieldId) |
| .then(textPadding), |
| propagateMinConstraints = true |
| ) { |
| textField() |
| } |
| |
| if (label != null) { |
| Box(Modifier |
| .heightIn(min = lerp( |
| MinTextLineHeight, MinFocusedLabelLineHeight, animationProgress)) |
| .wrapContentHeight() |
| .layoutId(LabelId)) { label() } |
| } |
| |
| if (supporting != null) { |
| @OptIn(ExperimentalMaterial3Api::class) |
| Box(Modifier |
| .layoutId(SupportingId) |
| .heightIn(min = MinSupportingTextLineHeight) |
| .wrapContentHeight() |
| .padding(TextFieldDefaults.supportingTextPadding()) |
| ) { supporting() } |
| } |
| }, |
| measurePolicy = measurePolicy |
| ) |
| } |
| |
| private class OutlinedTextFieldMeasurePolicy( |
| private val onLabelMeasured: (Size) -> Unit, |
| private val singleLine: Boolean, |
| private val animationProgress: Float, |
| private val paddingValues: PaddingValues |
| ) : MeasurePolicy { |
| override fun MeasureScope.measure( |
| measurables: List<Measurable>, |
| constraints: Constraints |
| ): MeasureResult { |
| var occupiedSpaceHorizontally = 0 |
| var occupiedSpaceVertically = 0 |
| val bottomPadding = paddingValues.calculateBottomPadding().roundToPx() |
| |
| val relaxedConstraints = constraints.copy(minWidth = 0, minHeight = 0) |
| |
| // measure leading icon |
| val leadingPlaceable = measurables.find { |
| it.layoutId == LeadingId |
| }?.measure(relaxedConstraints) |
| occupiedSpaceHorizontally += widthOrZero(leadingPlaceable) |
| occupiedSpaceVertically = max(occupiedSpaceVertically, heightOrZero(leadingPlaceable)) |
| |
| // measure trailing icon |
| val trailingPlaceable = measurables.find { it.layoutId == TrailingId } |
| ?.measure(relaxedConstraints.offset(horizontal = -occupiedSpaceHorizontally)) |
| occupiedSpaceHorizontally += widthOrZero(trailingPlaceable) |
| occupiedSpaceVertically = max(occupiedSpaceVertically, heightOrZero(trailingPlaceable)) |
| |
| // measure prefix |
| val prefixPlaceable = measurables.find { it.layoutId == PrefixId } |
| ?.measure(relaxedConstraints.offset(horizontal = -occupiedSpaceHorizontally)) |
| occupiedSpaceHorizontally += widthOrZero(prefixPlaceable) |
| occupiedSpaceVertically = max(occupiedSpaceVertically, heightOrZero(prefixPlaceable)) |
| |
| // measure suffix |
| val suffixPlaceable = measurables.find { it.layoutId == SuffixId } |
| ?.measure(relaxedConstraints.offset(horizontal = -occupiedSpaceHorizontally)) |
| occupiedSpaceHorizontally += widthOrZero(suffixPlaceable) |
| occupiedSpaceVertically = max(occupiedSpaceVertically, heightOrZero(suffixPlaceable)) |
| |
| // measure label |
| val isLabelInMiddleSection = animationProgress < 1f |
| val labelHorizontalPaddingOffset = |
| paddingValues.calculateLeftPadding(layoutDirection).roundToPx() + |
| paddingValues.calculateRightPadding(layoutDirection).roundToPx() |
| val labelConstraints = relaxedConstraints.offset( |
| horizontal = if (isLabelInMiddleSection) { |
| -occupiedSpaceHorizontally - labelHorizontalPaddingOffset |
| } else { |
| -labelHorizontalPaddingOffset |
| }, |
| vertical = -bottomPadding |
| ) |
| val labelPlaceable = |
| measurables.find { it.layoutId == LabelId }?.measure(labelConstraints) |
| labelPlaceable?.let { |
| onLabelMeasured(Size(it.width.toFloat(), it.height.toFloat())) |
| } |
| |
| // measure text field |
| // On top, we offset either by default padding or by label's half height if its too big. |
| // On bottom, we offset to make room for supporting text. |
| // minHeight must not be set to 0 due to how foundation TextField treats zero minHeight. |
| val topPadding = max( |
| heightOrZero(labelPlaceable) / 2, |
| paddingValues.calculateTopPadding().roundToPx() |
| ) |
| val textConstraints = constraints.offset( |
| horizontal = -occupiedSpaceHorizontally, |
| vertical = -bottomPadding - topPadding |
| ).copy(minHeight = 0) |
| val textFieldPlaceable = |
| measurables.first { it.layoutId == TextFieldId }.measure(textConstraints) |
| |
| // measure placeholder |
| val placeholderConstraints = textConstraints.copy(minWidth = 0) |
| val placeholderPlaceable = |
| measurables.find { it.layoutId == PlaceholderId }?.measure(placeholderConstraints) |
| |
| occupiedSpaceVertically = max( |
| occupiedSpaceVertically, |
| max(heightOrZero(textFieldPlaceable), heightOrZero(placeholderPlaceable)) + |
| topPadding + bottomPadding |
| ) |
| |
| // measure supporting text |
| val supportingConstraints = relaxedConstraints.offset( |
| vertical = -occupiedSpaceVertically |
| ).copy(minHeight = 0) |
| val supportingPlaceable = |
| measurables.find { it.layoutId == SupportingId }?.measure(supportingConstraints) |
| val supportingHeight = heightOrZero(supportingPlaceable) |
| |
| val width = |
| calculateWidth( |
| leadingPlaceableWidth = widthOrZero(leadingPlaceable), |
| trailingPlaceableWidth = widthOrZero(trailingPlaceable), |
| prefixPlaceableWidth = widthOrZero(prefixPlaceable), |
| suffixPlaceableWidth = widthOrZero(suffixPlaceable), |
| textFieldPlaceableWidth = textFieldPlaceable.width, |
| labelPlaceableWidth = widthOrZero(labelPlaceable), |
| placeholderPlaceableWidth = widthOrZero(placeholderPlaceable), |
| isLabelInMiddleSection = isLabelInMiddleSection, |
| constraints = constraints, |
| density = density, |
| paddingValues = paddingValues, |
| ) |
| val totalHeight = |
| calculateHeight( |
| leadingPlaceableHeight = heightOrZero(leadingPlaceable), |
| trailingPlaceableHeight = heightOrZero(trailingPlaceable), |
| prefixPlaceableHeight = heightOrZero(prefixPlaceable), |
| suffixPlaceableHeight = heightOrZero(suffixPlaceable), |
| textFieldPlaceableHeight = textFieldPlaceable.height, |
| labelPlaceableHeight = heightOrZero(labelPlaceable), |
| placeholderPlaceableHeight = heightOrZero(placeholderPlaceable), |
| supportingPlaceableHeight = heightOrZero(supportingPlaceable), |
| constraints = constraints, |
| density = density, |
| paddingValues = paddingValues, |
| ) |
| val height = totalHeight - supportingHeight |
| |
| val containerPlaceable = measurables.first { it.layoutId == ContainerId }.measure( |
| Constraints( |
| minWidth = if (width != Constraints.Infinity) width else 0, |
| maxWidth = width, |
| minHeight = if (height != Constraints.Infinity) height else 0, |
| maxHeight = height |
| ) |
| ) |
| return layout(width, totalHeight) { |
| place( |
| totalHeight = totalHeight, |
| width = width, |
| leadingPlaceable = leadingPlaceable, |
| trailingPlaceable = trailingPlaceable, |
| prefixPlaceable = prefixPlaceable, |
| suffixPlaceable = suffixPlaceable, |
| textFieldPlaceable = textFieldPlaceable, |
| labelPlaceable = labelPlaceable, |
| placeholderPlaceable = placeholderPlaceable, |
| containerPlaceable = containerPlaceable, |
| supportingPlaceable = supportingPlaceable, |
| animationProgress = animationProgress, |
| singleLine = singleLine, |
| density = density, |
| layoutDirection = layoutDirection, |
| paddingValues = paddingValues, |
| ) |
| } |
| } |
| |
| override fun IntrinsicMeasureScope.maxIntrinsicHeight( |
| measurables: List<IntrinsicMeasurable>, |
| width: Int |
| ): Int { |
| return intrinsicHeight(measurables, width) { intrinsicMeasurable, w -> |
| intrinsicMeasurable.maxIntrinsicHeight(w) |
| } |
| } |
| |
| override fun IntrinsicMeasureScope.minIntrinsicHeight( |
| measurables: List<IntrinsicMeasurable>, |
| width: Int |
| ): Int { |
| return intrinsicHeight(measurables, width) { intrinsicMeasurable, w -> |
| intrinsicMeasurable.minIntrinsicHeight(w) |
| } |
| } |
| |
| override fun IntrinsicMeasureScope.maxIntrinsicWidth( |
| measurables: List<IntrinsicMeasurable>, |
| height: Int |
| ): Int { |
| return intrinsicWidth(measurables, height) { intrinsicMeasurable, h -> |
| intrinsicMeasurable.maxIntrinsicWidth(h) |
| } |
| } |
| |
| override fun IntrinsicMeasureScope.minIntrinsicWidth( |
| measurables: List<IntrinsicMeasurable>, |
| height: Int |
| ): Int { |
| return intrinsicWidth(measurables, height) { intrinsicMeasurable, h -> |
| intrinsicMeasurable.minIntrinsicWidth(h) |
| } |
| } |
| |
| private fun IntrinsicMeasureScope.intrinsicWidth( |
| measurables: List<IntrinsicMeasurable>, |
| height: Int, |
| intrinsicMeasurer: (IntrinsicMeasurable, Int) -> Int |
| ): Int { |
| val textFieldWidth = |
| intrinsicMeasurer(measurables.first { it.layoutId == TextFieldId }, height) |
| val labelWidth = measurables.find { it.layoutId == LabelId }?.let { |
| intrinsicMeasurer(it, height) |
| } ?: 0 |
| val trailingWidth = measurables.find { it.layoutId == TrailingId }?.let { |
| intrinsicMeasurer(it, height) |
| } ?: 0 |
| val leadingWidth = measurables.find { it.layoutId == LeadingId }?.let { |
| intrinsicMeasurer(it, height) |
| } ?: 0 |
| val prefixWidth = measurables.find { it.layoutId == PrefixId }?.let { |
| intrinsicMeasurer(it, height) |
| } ?: 0 |
| val suffixWidth = measurables.find { it.layoutId == SuffixId }?.let { |
| intrinsicMeasurer(it, height) |
| } ?: 0 |
| val placeholderWidth = measurables.find { it.layoutId == PlaceholderId }?.let { |
| intrinsicMeasurer(it, height) |
| } ?: 0 |
| return calculateWidth( |
| leadingPlaceableWidth = leadingWidth, |
| trailingPlaceableWidth = trailingWidth, |
| prefixPlaceableWidth = prefixWidth, |
| suffixPlaceableWidth = suffixWidth, |
| textFieldPlaceableWidth = textFieldWidth, |
| labelPlaceableWidth = labelWidth, |
| placeholderPlaceableWidth = placeholderWidth, |
| isLabelInMiddleSection = animationProgress < 1f, |
| constraints = ZeroConstraints, |
| density = density, |
| paddingValues = paddingValues, |
| ) |
| } |
| |
| private fun IntrinsicMeasureScope.intrinsicHeight( |
| measurables: List<IntrinsicMeasurable>, |
| width: Int, |
| intrinsicMeasurer: (IntrinsicMeasurable, Int) -> Int |
| ): Int { |
| val textFieldHeight = |
| intrinsicMeasurer(measurables.first { it.layoutId == TextFieldId }, width) |
| val labelHeight = measurables.find { it.layoutId == LabelId }?.let { |
| intrinsicMeasurer(it, width) |
| } ?: 0 |
| val trailingHeight = measurables.find { it.layoutId == TrailingId }?.let { |
| intrinsicMeasurer(it, width) |
| } ?: 0 |
| val leadingHeight = measurables.find { it.layoutId == LeadingId }?.let { |
| intrinsicMeasurer(it, width) |
| } ?: 0 |
| val prefixHeight = measurables.find { it.layoutId == PrefixId }?.let { |
| intrinsicMeasurer(it, width) |
| } ?: 0 |
| val suffixHeight = measurables.find { it.layoutId == SuffixId }?.let { |
| intrinsicMeasurer(it, width) |
| } ?: 0 |
| val placeholderHeight = measurables.find { it.layoutId == PlaceholderId }?.let { |
| intrinsicMeasurer(it, width) |
| } ?: 0 |
| val supportingHeight = measurables.find { it.layoutId == SupportingId }?.let { |
| intrinsicMeasurer(it, width) |
| } ?: 0 |
| return calculateHeight( |
| leadingPlaceableHeight = leadingHeight, |
| trailingPlaceableHeight = trailingHeight, |
| prefixPlaceableHeight = prefixHeight, |
| suffixPlaceableHeight = suffixHeight, |
| textFieldPlaceableHeight = textFieldHeight, |
| labelPlaceableHeight = labelHeight, |
| placeholderPlaceableHeight = placeholderHeight, |
| supportingPlaceableHeight = supportingHeight, |
| constraints = ZeroConstraints, |
| density = density, |
| paddingValues = paddingValues |
| ) |
| } |
| } |
| |
| /** |
| * Calculate the width of the [OutlinedTextField] given all elements that should be placed inside. |
| */ |
| private fun calculateWidth( |
| leadingPlaceableWidth: Int, |
| trailingPlaceableWidth: Int, |
| prefixPlaceableWidth: Int, |
| suffixPlaceableWidth: Int, |
| textFieldPlaceableWidth: Int, |
| labelPlaceableWidth: Int, |
| placeholderPlaceableWidth: Int, |
| isLabelInMiddleSection: Boolean, |
| constraints: Constraints, |
| density: Float, |
| paddingValues: PaddingValues, |
| ): Int { |
| val affixTotalWidth = prefixPlaceableWidth + suffixPlaceableWidth |
| val middleSection = maxOf( |
| textFieldPlaceableWidth + affixTotalWidth, |
| placeholderPlaceableWidth + affixTotalWidth, |
| // Prefix/suffix does not get applied to label |
| if (isLabelInMiddleSection) labelPlaceableWidth else 0, |
| ) |
| val wrappedWidth = |
| leadingPlaceableWidth + middleSection + trailingPlaceableWidth |
| val focusedLabelWidth = |
| if (!isLabelInMiddleSection) { |
| // Actual LayoutDirection doesn't matter; we only need the sum |
| val labelHorizontalPadding = (paddingValues.calculateLeftPadding(LayoutDirection.Ltr) + |
| paddingValues.calculateRightPadding(LayoutDirection.Ltr)).value * density |
| labelPlaceableWidth + labelHorizontalPadding.roundToInt() |
| } else { |
| 0 |
| } |
| return maxOf(wrappedWidth, focusedLabelWidth, constraints.minWidth) |
| } |
| |
| /** |
| * Calculate the height of the [OutlinedTextField] given all elements that should be placed inside. |
| * This includes the supporting text, if it exists, even though this element is not "visually" |
| * inside the text field. |
| */ |
| private fun calculateHeight( |
| leadingPlaceableHeight: Int, |
| trailingPlaceableHeight: Int, |
| prefixPlaceableHeight: Int, |
| suffixPlaceableHeight: Int, |
| textFieldPlaceableHeight: Int, |
| labelPlaceableHeight: Int, |
| placeholderPlaceableHeight: Int, |
| supportingPlaceableHeight: Int, |
| constraints: Constraints, |
| density: Float, |
| paddingValues: PaddingValues |
| ): Int { |
| // middle section is defined as a height of the text field or placeholder (whichever is |
| // taller) plus 16.dp or half height of the label if it is taller, given that the label |
| // is vertically centered to the top edge of the resulting text field's container |
| val inputFieldHeight = max( |
| textFieldPlaceableHeight, |
| placeholderPlaceableHeight |
| ) |
| val topPadding = paddingValues.calculateTopPadding().value * density |
| val bottomPadding = paddingValues.calculateBottomPadding().value * density |
| val middleSectionHeight = inputFieldHeight + bottomPadding + max( |
| topPadding, |
| labelPlaceableHeight / 2f |
| ) |
| return max( |
| constraints.minHeight, |
| maxOf( |
| leadingPlaceableHeight, |
| trailingPlaceableHeight, |
| prefixPlaceableHeight, |
| suffixPlaceableHeight, |
| middleSectionHeight.roundToInt() |
| ) + supportingPlaceableHeight |
| ) |
| } |
| |
| /** |
| * Places the provided text field, placeholder, label, optional leading and trailing icons inside |
| * the [OutlinedTextField] |
| */ |
| private fun Placeable.PlacementScope.place( |
| totalHeight: Int, |
| width: Int, |
| leadingPlaceable: Placeable?, |
| trailingPlaceable: Placeable?, |
| prefixPlaceable: Placeable?, |
| suffixPlaceable: Placeable?, |
| textFieldPlaceable: Placeable, |
| labelPlaceable: Placeable?, |
| placeholderPlaceable: Placeable?, |
| containerPlaceable: Placeable, |
| supportingPlaceable: Placeable?, |
| animationProgress: Float, |
| singleLine: Boolean, |
| density: Float, |
| layoutDirection: LayoutDirection, |
| paddingValues: PaddingValues |
| ) { |
| // place container |
| containerPlaceable.place(IntOffset.Zero) |
| |
| // Most elements should be positioned w.r.t the text field's "visual" height, i.e., excluding |
| // the supporting text on bottom |
| val height = totalHeight - heightOrZero(supportingPlaceable) |
| val topPadding = (paddingValues.calculateTopPadding().value * density).roundToInt() |
| val startPadding = |
| (paddingValues.calculateStartPadding(layoutDirection).value * density).roundToInt() |
| |
| val iconPadding = HorizontalIconPadding.value * density |
| |
| // placed center vertically and to the start edge horizontally |
| leadingPlaceable?.placeRelative( |
| 0, |
| Alignment.CenterVertically.align(leadingPlaceable.height, height) |
| ) |
| |
| // placed center vertically and to the end edge horizontally |
| trailingPlaceable?.placeRelative( |
| width - trailingPlaceable.width, |
| Alignment.CenterVertically.align(trailingPlaceable.height, height) |
| ) |
| |
| // label position is animated |
| // in single line text field, label is centered vertically before animation starts |
| labelPlaceable?.let { |
| val startPositionY = if (singleLine) { |
| Alignment.CenterVertically.align(it.height, height) |
| } else { |
| topPadding |
| } |
| val positionY = lerp(startPositionY, -(it.height / 2), animationProgress) |
| val positionX = ( |
| if (leadingPlaceable == null) { |
| 0f |
| } else { |
| (widthOrZero(leadingPlaceable) - iconPadding) * (1 - animationProgress) |
| } |
| ).roundToInt() + startPadding |
| it.placeRelative(positionX, positionY) |
| } |
| |
| // Single line text fields have text components centered vertically. |
| // Multiline text fields have text components aligned to top with padding. |
| fun calculateVerticalPosition(placeable: Placeable): Int = |
| max( |
| if (singleLine) { |
| Alignment.CenterVertically.align(placeable.height, height) |
| } else { |
| topPadding |
| }, |
| heightOrZero(labelPlaceable) / 2 |
| ) |
| |
| prefixPlaceable?.placeRelative( |
| widthOrZero(leadingPlaceable), |
| calculateVerticalPosition(prefixPlaceable) |
| ) |
| |
| suffixPlaceable?.placeRelative( |
| width - widthOrZero(trailingPlaceable) - suffixPlaceable.width, |
| calculateVerticalPosition(suffixPlaceable) |
| ) |
| |
| val textHorizontalPosition = widthOrZero(leadingPlaceable) + widthOrZero(prefixPlaceable) |
| |
| textFieldPlaceable.placeRelative( |
| textHorizontalPosition, |
| calculateVerticalPosition(textFieldPlaceable) |
| ) |
| |
| // placed similar to the input text above |
| placeholderPlaceable?.placeRelative( |
| textHorizontalPosition, |
| calculateVerticalPosition(placeholderPlaceable) |
| ) |
| |
| // place supporting text |
| supportingPlaceable?.placeRelative(0, height) |
| } |
| |
| internal fun Modifier.outlineCutout(labelSize: Size, paddingValues: PaddingValues) = |
| this.drawWithContent { |
| val labelWidth = labelSize.width |
| if (labelWidth > 0f) { |
| val innerPadding = OutlinedTextFieldInnerPadding.toPx() |
| val leftLtr = paddingValues.calculateLeftPadding(layoutDirection).toPx() - innerPadding |
| val rightLtr = leftLtr + labelWidth + 2 * innerPadding |
| val left = when (layoutDirection) { |
| LayoutDirection.Rtl -> size.width - rightLtr |
| else -> leftLtr.coerceAtLeast(0f) |
| } |
| val right = when (layoutDirection) { |
| LayoutDirection.Rtl -> size.width - leftLtr.coerceAtLeast(0f) |
| else -> rightLtr |
| } |
| val labelHeight = labelSize.height |
| // using label height as a cutout area to make sure that no hairline artifacts are |
| // left when we clip the border |
| clipRect(left, -labelHeight / 2, right, labelHeight / 2, ClipOp.Difference) { |
| this@drawWithContent.drawContent() |
| } |
| } else { |
| this@drawWithContent.drawContent() |
| } |
| } |
| |
| private val OutlinedTextFieldInnerPadding = 4.dp |
| |
| /* |
| This padding is used to allow label not overlap with the content above it. This 8.dp will work |
| for default cases when developers do not override the label's font size. If they do, they will |
| need to add additional padding themselves |
| */ |
| /* @VisibleForTesting */ |
| internal val OutlinedTextFieldTopPadding = 8.dp |