/* * 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.fastFirst import androidx.compose.ui.util.fastFirstOrNull import androidx.compose.ui.util.lerp import kotlin.math.max import kotlin.math.roundToInt /** * Material Design outlined text field. * * 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 [OutlinedTextFieldDefaults.colors]. */ @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 = OutlinedTextFieldDefaults.shape, colors: TextFieldColors = OutlinedTextFieldDefaults.colors() ) { // 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 } .defaultErrorSemantics(isError, getString(Strings.DefaultErrorMessage)) .defaultMinSize( minWidth = OutlinedTextFieldDefaults.MinWidth, minHeight = OutlinedTextFieldDefaults.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 -> OutlinedTextFieldDefaults.DecorationBox( 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 = { OutlinedTextFieldDefaults.ContainerBox( enabled, isError, interactionSource, colors, shape ) } ) } ) } } /** * Material Design outlined text field. * * 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 [OutlinedTextFieldDefaults.colors]. */ @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 = OutlinedTextFieldDefaults.shape, colors: TextFieldColors = OutlinedTextFieldDefaults.colors() ) { // 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 } .defaultErrorSemantics(isError, getString(Strings.DefaultErrorMessage)) .defaultMinSize( minWidth = OutlinedTextFieldDefaults.MinWidth, minHeight = OutlinedTextFieldDefaults.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 -> OutlinedTextFieldDefaults.DecorationBox( 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 = { OutlinedTextFieldDefaults.ContainerBox( 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 = OutlinedTextFieldDefaults.shape, colors: TextFieldColors = OutlinedTextFieldDefaults.colors() ) { 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 = OutlinedTextFieldDefaults.shape, colors: TextFieldColors = OutlinedTextFieldDefaults.colors() ) { 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, 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.fastFirstOrNull { it.layoutId == LeadingId }?.measure(relaxedConstraints) occupiedSpaceHorizontally += widthOrZero(leadingPlaceable) occupiedSpaceVertically = max(occupiedSpaceVertically, heightOrZero(leadingPlaceable)) // measure trailing icon val trailingPlaceable = measurables.fastFirstOrNull { it.layoutId == TrailingId } ?.measure(relaxedConstraints.offset(horizontal = -occupiedSpaceHorizontally)) occupiedSpaceHorizontally += widthOrZero(trailingPlaceable) occupiedSpaceVertically = max(occupiedSpaceVertically, heightOrZero(trailingPlaceable)) // measure prefix val prefixPlaceable = measurables.fastFirstOrNull { it.layoutId == PrefixId } ?.measure(relaxedConstraints.offset(horizontal = -occupiedSpaceHorizontally)) occupiedSpaceHorizontally += widthOrZero(prefixPlaceable) occupiedSpaceVertically = max(occupiedSpaceVertically, heightOrZero(prefixPlaceable)) // measure suffix val suffixPlaceable = measurables.fastFirstOrNull { it.layoutId == SuffixId } ?.measure(relaxedConstraints.offset(horizontal = -occupiedSpaceHorizontally)) occupiedSpaceHorizontally += widthOrZero(suffixPlaceable) occupiedSpaceVertically = max(occupiedSpaceVertically, heightOrZero(suffixPlaceable)) // measure label val labelHorizontalPaddingOffset = paddingValues.calculateLeftPadding(layoutDirection).roundToPx() + paddingValues.calculateRightPadding(layoutDirection).roundToPx() val labelConstraints = relaxedConstraints.offset( horizontal = lerp( -occupiedSpaceHorizontally - labelHorizontalPaddingOffset, // label in middle -labelHorizontalPaddingOffset, // label at top animationProgress, ), vertical = -bottomPadding ) val labelPlaceable = measurables.fastFirstOrNull { it.layoutId == LabelId }?.measure(labelConstraints) labelPlaceable?.let { onLabelMeasured(Size(it.width.toFloat(), it.height.toFloat())) } // supporting text must be measured after other elements, but we // reserve space for it using its intrinsic height as a heuristic val supportingMeasurable = measurables.fastFirstOrNull { it.layoutId == SupportingId } val supportingIntrinsicHeight = supportingMeasurable?.minIntrinsicHeight(constraints.minWidth) ?: 0 // measure text field val topPadding = max( heightOrZero(labelPlaceable) / 2, paddingValues.calculateTopPadding().roundToPx() ) val textConstraints = constraints.offset( horizontal = -occupiedSpaceHorizontally, vertical = -bottomPadding - topPadding - supportingIntrinsicHeight ).copy(minHeight = 0) val textFieldPlaceable = measurables.fastFirst { it.layoutId == TextFieldId }.measure(textConstraints) // measure placeholder val placeholderConstraints = textConstraints.copy(minWidth = 0) val placeholderPlaceable = measurables.fastFirstOrNull { it.layoutId == PlaceholderId } ?.measure(placeholderConstraints) occupiedSpaceVertically = max( occupiedSpaceVertically, max(heightOrZero(textFieldPlaceable), heightOrZero(placeholderPlaceable)) + topPadding + bottomPadding ) val width = calculateWidth( leadingPlaceableWidth = widthOrZero(leadingPlaceable), trailingPlaceableWidth = widthOrZero(trailingPlaceable), prefixPlaceableWidth = widthOrZero(prefixPlaceable), suffixPlaceableWidth = widthOrZero(suffixPlaceable), textFieldPlaceableWidth = textFieldPlaceable.width, labelPlaceableWidth = widthOrZero(labelPlaceable), placeholderPlaceableWidth = widthOrZero(placeholderPlaceable), animationProgress = animationProgress, constraints = constraints, density = density, paddingValues = paddingValues, ) // measure supporting text val supportingConstraints = relaxedConstraints.offset( vertical = -occupiedSpaceVertically ).copy(minHeight = 0, maxWidth = width) val supportingPlaceable = supportingMeasurable?.measure(supportingConstraints) val supportingHeight = heightOrZero(supportingPlaceable) val totalHeight = calculateHeight( leadingHeight = heightOrZero(leadingPlaceable), trailingHeight = heightOrZero(trailingPlaceable), prefixHeight = heightOrZero(prefixPlaceable), suffixHeight = heightOrZero(suffixPlaceable), textFieldHeight = textFieldPlaceable.height, labelHeight = heightOrZero(labelPlaceable), placeholderHeight = heightOrZero(placeholderPlaceable), supportingHeight = heightOrZero(supportingPlaceable), animationProgress = animationProgress, constraints = constraints, density = density, paddingValues = paddingValues, ) val height = totalHeight - supportingHeight val containerPlaceable = measurables.fastFirst { 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, width: Int ): Int { return intrinsicHeight(measurables, width) { intrinsicMeasurable, w -> intrinsicMeasurable.maxIntrinsicHeight(w) } } override fun IntrinsicMeasureScope.minIntrinsicHeight( measurables: List, width: Int ): Int { return intrinsicHeight(measurables, width) { intrinsicMeasurable, w -> intrinsicMeasurable.minIntrinsicHeight(w) } } override fun IntrinsicMeasureScope.maxIntrinsicWidth( measurables: List, height: Int ): Int { return intrinsicWidth(measurables, height) { intrinsicMeasurable, h -> intrinsicMeasurable.maxIntrinsicWidth(h) } } override fun IntrinsicMeasureScope.minIntrinsicWidth( measurables: List, height: Int ): Int { return intrinsicWidth(measurables, height) { intrinsicMeasurable, h -> intrinsicMeasurable.minIntrinsicWidth(h) } } private fun IntrinsicMeasureScope.intrinsicWidth( measurables: List, height: Int, intrinsicMeasurer: (IntrinsicMeasurable, Int) -> Int ): Int { val textFieldWidth = intrinsicMeasurer(measurables.fastFirst { it.layoutId == TextFieldId }, height) val labelWidth = measurables.fastFirstOrNull { it.layoutId == LabelId }?.let { intrinsicMeasurer(it, height) } ?: 0 val trailingWidth = measurables.fastFirstOrNull { it.layoutId == TrailingId }?.let { intrinsicMeasurer(it, height) } ?: 0 val leadingWidth = measurables.fastFirstOrNull { it.layoutId == LeadingId }?.let { intrinsicMeasurer(it, height) } ?: 0 val prefixWidth = measurables.fastFirstOrNull { it.layoutId == PrefixId }?.let { intrinsicMeasurer(it, height) } ?: 0 val suffixWidth = measurables.fastFirstOrNull { it.layoutId == SuffixId }?.let { intrinsicMeasurer(it, height) } ?: 0 val placeholderWidth = measurables.fastFirstOrNull { it.layoutId == PlaceholderId }?.let { intrinsicMeasurer(it, height) } ?: 0 return calculateWidth( leadingPlaceableWidth = leadingWidth, trailingPlaceableWidth = trailingWidth, prefixPlaceableWidth = prefixWidth, suffixPlaceableWidth = suffixWidth, textFieldPlaceableWidth = textFieldWidth, labelPlaceableWidth = labelWidth, placeholderPlaceableWidth = placeholderWidth, animationProgress = animationProgress, constraints = ZeroConstraints, density = density, paddingValues = paddingValues, ) } private fun IntrinsicMeasureScope.intrinsicHeight( measurables: List, width: Int, intrinsicMeasurer: (IntrinsicMeasurable, Int) -> Int ): Int { var remainingWidth = width val leadingHeight = measurables.fastFirstOrNull { it.layoutId == LeadingId }?.let { remainingWidth = remainingWidth.substractConstraintSafely( it.maxIntrinsicWidth(Constraints.Infinity) ) intrinsicMeasurer(it, width) } ?: 0 val trailingHeight = measurables.fastFirstOrNull { it.layoutId == TrailingId }?.let { remainingWidth = remainingWidth.substractConstraintSafely( it.maxIntrinsicWidth(Constraints.Infinity) ) intrinsicMeasurer(it, width) } ?: 0 val labelHeight = measurables.fastFirstOrNull { it.layoutId == LabelId }?.let { intrinsicMeasurer(it, lerp(remainingWidth, width, animationProgress)) } ?: 0 val prefixHeight = measurables.fastFirstOrNull { it.layoutId == PrefixId }?.let { val height = intrinsicMeasurer(it, remainingWidth) remainingWidth = remainingWidth.substractConstraintSafely( it.maxIntrinsicWidth(Constraints.Infinity) ) height } ?: 0 val suffixHeight = measurables.fastFirstOrNull { it.layoutId == SuffixId }?.let { val height = intrinsicMeasurer(it, remainingWidth) remainingWidth = remainingWidth.substractConstraintSafely( it.maxIntrinsicWidth(Constraints.Infinity) ) height } ?: 0 val textFieldHeight = intrinsicMeasurer(measurables.fastFirst { it.layoutId == TextFieldId }, remainingWidth) val placeholderHeight = measurables.fastFirstOrNull { it.layoutId == PlaceholderId }?.let { intrinsicMeasurer(it, remainingWidth) } ?: 0 val supportingHeight = measurables.fastFirstOrNull { it.layoutId == SupportingId }?.let { intrinsicMeasurer(it, width) } ?: 0 return calculateHeight( leadingHeight = leadingHeight, trailingHeight = trailingHeight, prefixHeight = prefixHeight, suffixHeight = suffixHeight, textFieldHeight = textFieldHeight, labelHeight = labelHeight, placeholderHeight = placeholderHeight, supportingHeight = supportingHeight, animationProgress = animationProgress, constraints = ZeroConstraints, density = density, paddingValues = paddingValues ) } } private fun Int.substractConstraintSafely(from: Int): Int { if (this == Constraints.Infinity) { return this } return this - from } /** * 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, animationProgress: Float, 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 lerp(labelPlaceableWidth, 0, animationProgress), ) val wrappedWidth = leadingPlaceableWidth + middleSection + trailingPlaceableWidth // Actual LayoutDirection doesn't matter; we only need the sum val labelHorizontalPadding = (paddingValues.calculateLeftPadding(LayoutDirection.Ltr) + paddingValues.calculateRightPadding(LayoutDirection.Ltr)).value * density val focusedLabelWidth = ((labelPlaceableWidth + labelHorizontalPadding) * animationProgress).roundToInt() 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( leadingHeight: Int, trailingHeight: Int, prefixHeight: Int, suffixHeight: Int, textFieldHeight: Int, labelHeight: Int, placeholderHeight: Int, supportingHeight: Int, animationProgress: Float, constraints: Constraints, density: Float, paddingValues: PaddingValues ): Int { val inputFieldHeight = maxOf( textFieldHeight, placeholderHeight, prefixHeight, suffixHeight, lerp(labelHeight, 0, animationProgress) ) val topPadding = paddingValues.calculateTopPadding().value * density val actualTopPadding = lerp(topPadding, max(topPadding, labelHeight / 2f), animationProgress) val bottomPadding = paddingValues.calculateBottomPadding().value * density val middleSectionHeight = actualTopPadding + inputFieldHeight + bottomPadding return max( constraints.minHeight, maxOf( leadingHeight, trailingHeight, middleSectionHeight.roundToInt() ) + supportingHeight ) } /** * 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