/* * 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.BorderStroke import androidx.compose.foundation.background 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.padding import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.TextFieldDefaults.indicatorLine import androidx.compose.material3.tokens.FilledTextFieldTokens import androidx.compose.runtime.Composable 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.Offset import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.takeOrElse import androidx.compose.ui.layout.AlignmentLine import androidx.compose.ui.layout.IntrinsicMeasurable import androidx.compose.ui.layout.IntrinsicMeasureScope import androidx.compose.ui.layout.LastBaseline 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.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.Dp import androidx.compose.ui.unit.coerceAtLeast import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.offset import kotlin.math.max import kotlin.math.roundToInt /** * Material Design filled text field. * * Text fields allow users to enter text into a UI. They typically appear in forms and dialogs. * Filled text fields have more visual emphasis than outlined text fields, making them stand out * when surrounded by other content and components. * * ![Filled text field image](https://developer.android.com/images/reference/androidx/compose/material3/filled-text-field.png) * * If you are looking for an outlined version, see [OutlinedTextField]. * * A simple single line text field looks like: * * @sample androidx.compose.material3.samples.SimpleTextFieldSample * * You may provide a placeholder: * * @sample androidx.compose.material3.samples.TextFieldWithPlaceholder * * You can also provide leading and trailing icons: * * @sample androidx.compose.material3.samples.TextFieldWithIcons * * To handle the error input state, use [isError] parameter: * * @sample androidx.compose.material3.samples.TextFieldWithErrorState * * Additionally, you may provide additional message at the bottom: * * @sample androidx.compose.material3.samples.TextFieldWithSupportingText * * Password text field example: * * @sample androidx.compose.material3.samples.PasswordTextField * * Hiding a software keyboard on IME action performed: * * @sample androidx.compose.material3.samples.TextFieldWithHideKeyboardOnImeAction * * If apart from input text change you also want to observe the cursor location, selection range, * or IME composition use the TextField 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 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. Should be * equal or greater than 1. Note that this parameter will be ignored and instead maxLines will be * set to 1 if [singleLine] is set to 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 container * @param colors [TextFieldColors] that will be used to resolve the colors used for this text field * in different states. See [TextFieldDefaults.textFieldColors]. */ @Composable fun TextField( 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, isError: Boolean = false, visualTransformation: VisualTransformation = VisualTransformation.None, keyboardOptions: KeyboardOptions = KeyboardOptions.Default, keyboardActions: KeyboardActions = KeyboardActions.Default, singleLine: Boolean = false, maxLines: Int = Int.MAX_VALUE, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, shape: Shape = FilledTextFieldTokens.ContainerShape.toShape(), colors: TextFieldColors = TextFieldDefaults.textFieldColors() ) { // If color is not provided via the text style, use content color as a default val textColor = textStyle.color.takeOrElse { colors.textColor(enabled).value } val mergedTextStyle = textStyle.merge(TextStyle(color = textColor)) @OptIn(ExperimentalMaterial3Api::class) BasicTextField( value = value, modifier = modifier .background(colors.containerColor(enabled).value, shape) .indicatorLine(enabled, isError, interactionSource, colors) .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, decorationBox = @Composable { innerTextField -> // places leading icon, text field with label and placeholder, trailing icon TextFieldDefaults.TextFieldDecorationBox( value = value, visualTransformation = visualTransformation, innerTextField = innerTextField, placeholder = placeholder, label = label, leadingIcon = leadingIcon, trailingIcon = trailingIcon, singleLine = singleLine, enabled = enabled, isError = isError, interactionSource = interactionSource, colors = colors ) } ) } /** * Material Design filled text field. * * Text fields allow users to enter text into a UI. They typically appear in forms and dialogs. * Filled text fields have more visual emphasis than outlined text fields, making them stand out * when surrounded by other content and components. * * ![Filled text field image](https://developer.android.com/images/reference/androidx/compose/material3/filled-text-field.png) * * If you are looking for an outlined version, see [OutlinedTextField]. * * See example usage: * @sample androidx.compose.material3.samples.TextFieldSample * * This overload provides access to the input text, cursor position, selection range and * IME composition. If you only want to observe an input text change, use the TextField * 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 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. Should be * equal or greater than 1. Note that this parameter will be ignored and instead maxLines will be * set to 1 if [singleLine] is set to 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 container * @param colors [TextFieldColors] that will be used to resolve the colors used for this text field * in different states. See [TextFieldDefaults.textFieldColors]. */ @Composable fun TextField( 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, isError: Boolean = false, visualTransformation: VisualTransformation = VisualTransformation.None, keyboardOptions: KeyboardOptions = KeyboardOptions.Default, keyboardActions: KeyboardActions = KeyboardActions.Default, singleLine: Boolean = false, maxLines: Int = Int.MAX_VALUE, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, shape: Shape = FilledTextFieldTokens.ContainerShape.toShape(), colors: TextFieldColors = TextFieldDefaults.textFieldColors() ) { // If color is not provided via the text style, use content color as a default val textColor = textStyle.color.takeOrElse { colors.textColor(enabled).value } val mergedTextStyle = textStyle.merge(TextStyle(color = textColor)) @OptIn(ExperimentalMaterial3Api::class) BasicTextField( value = value, modifier = modifier .background(colors.containerColor(enabled).value, shape) .indicatorLine(enabled, isError, interactionSource, colors) .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, decorationBox = @Composable { innerTextField -> // places leading icon, text field with label and placeholder, trailing icon TextFieldDefaults.TextFieldDecorationBox( value = value.text, visualTransformation = visualTransformation, innerTextField = innerTextField, placeholder = placeholder, label = label, leadingIcon = leadingIcon, trailingIcon = trailingIcon, singleLine = singleLine, enabled = enabled, isError = isError, interactionSource = interactionSource, colors = colors ) } ) } /** * Composable responsible for measuring and laying out leading and trailing icons, label, * placeholder and the input field. */ @Composable internal fun TextFieldLayout( modifier: Modifier, textField: @Composable () -> Unit, label: @Composable (() -> Unit)?, placeholder: @Composable ((Modifier) -> Unit)?, leading: @Composable (() -> Unit)?, trailing: @Composable (() -> Unit)?, singleLine: Boolean, animationProgress: Float, paddingValues: PaddingValues ) { val measurePolicy = remember(singleLine, animationProgress, paddingValues) { TextFieldMeasurePolicy(singleLine, animationProgress, paddingValues) } val layoutDirection = LocalLayoutDirection.current Layout( modifier = modifier, content = { 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 padding = Modifier.padding( start = if (leading != null) { (startTextFieldPadding - HorizontalIconPadding).coerceAtLeast( 0.dp ) } else { startTextFieldPadding }, end = if (trailing != null) { (endTextFieldPadding - HorizontalIconPadding).coerceAtLeast(0.dp) } else { endTextFieldPadding } ) if (placeholder != null) { placeholder(Modifier.layoutId(PlaceholderId).then(padding)) } if (label != null) { Box(Modifier.layoutId(LabelId).then(padding)) { label() } } Box( modifier = Modifier.layoutId(TextFieldId).then(padding), propagateMinConstraints = true, ) { textField() } }, measurePolicy = measurePolicy ) } private class TextFieldMeasurePolicy( private val singleLine: Boolean, private val animationProgress: Float, private val paddingValues: PaddingValues ) : MeasurePolicy { override fun MeasureScope.measure( measurables: List, constraints: Constraints ): MeasureResult { val topPaddingValue = paddingValues.calculateTopPadding().roundToPx() val bottomPaddingValue = paddingValues.calculateBottomPadding().roundToPx() // padding between label and input text val topPadding = TextFieldTopPadding.roundToPx() var occupiedSpaceHorizontally = 0 // measure leading icon val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0) val leadingPlaceable = measurables.find { it.layoutId == LeadingId }?.measure(looseConstraints) occupiedSpaceHorizontally += widthOrZero( leadingPlaceable ) // measure trailing icon val trailingPlaceable = measurables.find { it.layoutId == TrailingId } ?.measure(looseConstraints.offset(horizontal = -occupiedSpaceHorizontally)) occupiedSpaceHorizontally += widthOrZero( trailingPlaceable ) // measure label val labelConstraints = looseConstraints .offset( vertical = -bottomPaddingValue, horizontal = -occupiedSpaceHorizontally ) val labelPlaceable = measurables.find { it.layoutId == LabelId }?.measure(labelConstraints) val lastBaseline = labelPlaceable?.get(LastBaseline)?.let { if (it != AlignmentLine.Unspecified) it else labelPlaceable.height } ?: 0 val effectiveLabelBaseline = max(lastBaseline, topPaddingValue) // measure input field // input field is laid out differently depending on whether the label is present or not val verticalConstraintOffset = if (labelPlaceable != null) { -bottomPaddingValue - topPadding - effectiveLabelBaseline } else { -topPaddingValue - bottomPaddingValue } val textFieldConstraints = constraints .copy(minHeight = 0) .offset( vertical = verticalConstraintOffset, horizontal = -occupiedSpaceHorizontally ) val textFieldPlaceable = measurables .first { it.layoutId == TextFieldId } .measure(textFieldConstraints) // measure placeholder val placeholderConstraints = textFieldConstraints.copy(minWidth = 0) val placeholderPlaceable = measurables .find { it.layoutId == PlaceholderId } ?.measure(placeholderConstraints) val width = calculateWidth( widthOrZero(leadingPlaceable), widthOrZero(trailingPlaceable), textFieldPlaceable.width, widthOrZero(labelPlaceable), widthOrZero(placeholderPlaceable), constraints ) val height = calculateHeight( textFieldPlaceable.height, labelPlaceable != null, effectiveLabelBaseline, heightOrZero(leadingPlaceable), heightOrZero(trailingPlaceable), heightOrZero(placeholderPlaceable), constraints, density, paddingValues ) return layout(width, height) { if (labelPlaceable != null) { // label's final position is always relative to the baseline val labelEndPosition = (topPaddingValue - lastBaseline).coerceAtLeast(0) placeWithLabel( width, height, textFieldPlaceable, labelPlaceable, placeholderPlaceable, leadingPlaceable, trailingPlaceable, singleLine, labelEndPosition, effectiveLabelBaseline + topPadding, animationProgress, density ) } else { placeWithoutLabel( width, height, textFieldPlaceable, placeholderPlaceable, leadingPlaceable, trailingPlaceable, singleLine, density, 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 intrinsicWidth( measurables: List, 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 placeholderWidth = measurables.find { it.layoutId == PlaceholderId }?.let { intrinsicMeasurer(it, height) } ?: 0 return calculateWidth( leadingWidth = leadingWidth, trailingWidth = trailingWidth, textFieldWidth = textFieldWidth, labelWidth = labelWidth, placeholderWidth = placeholderWidth, constraints = ZeroConstraints ) } private fun IntrinsicMeasureScope.intrinsicHeight( measurables: List, 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 placeholderHeight = measurables.find { it.layoutId == PlaceholderId }?.let { intrinsicMeasurer(it, width) } ?: 0 return calculateHeight( textFieldHeight = textFieldHeight, hasLabel = labelHeight > 0, labelBaseline = labelHeight, leadingHeight = leadingHeight, trailingHeight = trailingHeight, placeholderHeight = placeholderHeight, constraints = ZeroConstraints, density = density, paddingValues = paddingValues ) } } private fun calculateWidth( leadingWidth: Int, trailingWidth: Int, textFieldWidth: Int, labelWidth: Int, placeholderWidth: Int, constraints: Constraints ): Int { val middleSection = maxOf( textFieldWidth, labelWidth, placeholderWidth ) val wrappedWidth = leadingWidth + middleSection + trailingWidth return max(wrappedWidth, constraints.minWidth) } private fun calculateHeight( textFieldHeight: Int, hasLabel: Boolean, labelBaseline: Int, leadingHeight: Int, trailingHeight: Int, placeholderHeight: Int, constraints: Constraints, density: Float, paddingValues: PaddingValues ): Int { val paddingToLabel = TextFieldTopPadding.value * density val topPaddingValue = paddingValues.calculateTopPadding().value * density val bottomPaddingValue = paddingValues.calculateBottomPadding().value * density val inputFieldHeight = max(textFieldHeight, placeholderHeight) val middleSectionHeight = if (hasLabel) { labelBaseline + paddingToLabel + inputFieldHeight + bottomPaddingValue } else { topPaddingValue + inputFieldHeight + bottomPaddingValue } return maxOf( middleSectionHeight.roundToInt(), max(leadingHeight, trailingHeight), constraints.minHeight ) } /** * Places the provided text field, placeholder and label with respect to the baseline offsets in * [TextField] when there is a label. When there is no label, [placeWithoutLabel] is used. */ private fun Placeable.PlacementScope.placeWithLabel( width: Int, height: Int, textfieldPlaceable: Placeable, labelPlaceable: Placeable?, placeholderPlaceable: Placeable?, leadingPlaceable: Placeable?, trailingPlaceable: Placeable?, singleLine: Boolean, labelEndPosition: Int, textPosition: Int, animationProgress: Float, density: Float ) { leadingPlaceable?.placeRelative( 0, Alignment.CenterVertically.align(leadingPlaceable.height, height) ) trailingPlaceable?.placeRelative( width - trailingPlaceable.width, Alignment.CenterVertically.align(trailingPlaceable.height, height) ) labelPlaceable?.let { // if it's a single line, the label's start position is in the center of the // container. When it's a multiline text field, the label's start position is at the // top with padding val startPosition = if (singleLine) { Alignment.CenterVertically.align(it.height, height) } else { // even though the padding is defined by developer, it only affects text field when // animation progress == 1, which is when text field is focused or non-empty input text. // The start position of the label is always 16.dp. (TextFieldPadding.value * density).roundToInt() } val distance = startPosition - labelEndPosition val positionY = startPosition - (distance * animationProgress).roundToInt() it.placeRelative(widthOrZero(leadingPlaceable), positionY) } textfieldPlaceable.placeRelative(widthOrZero(leadingPlaceable), textPosition) placeholderPlaceable?.placeRelative(widthOrZero(leadingPlaceable), textPosition) } /** * Places the provided text field and placeholder in [TextField] when there is no label. When * there is a label, [placeWithLabel] is used */ private fun Placeable.PlacementScope.placeWithoutLabel( width: Int, height: Int, textPlaceable: Placeable, placeholderPlaceable: Placeable?, leadingPlaceable: Placeable?, trailingPlaceable: Placeable?, singleLine: Boolean, density: Float, paddingValues: PaddingValues ) { val topPadding = (paddingValues.calculateTopPadding().value * density).roundToInt() leadingPlaceable?.placeRelative( 0, Alignment.CenterVertically.align(leadingPlaceable.height, height) ) trailingPlaceable?.placeRelative( width - trailingPlaceable.width, Alignment.CenterVertically.align(trailingPlaceable.height, height) ) // Single line text field without label places its input center vertically. Multiline text // field without label places its input at the top with padding val textVerticalPosition = if (singleLine) { Alignment.CenterVertically.align(textPlaceable.height, height) } else { topPadding } textPlaceable.placeRelative( widthOrZero(leadingPlaceable), textVerticalPosition ) // placeholder is placed similar to the text input above placeholderPlaceable?.let { val placeholderVerticalPosition = if (singleLine) { Alignment.CenterVertically.align(placeholderPlaceable.height, height) } else { topPadding } it.placeRelative( widthOrZero(leadingPlaceable), placeholderVerticalPosition ) } } /** * A draw modifier that draws a bottom indicator line in [TextField] */ internal fun Modifier.drawIndicatorLine(indicatorBorder: BorderStroke): Modifier { val strokeWidthDp = indicatorBorder.width return drawWithContent { drawContent() if (strokeWidthDp == Dp.Hairline) return@drawWithContent val strokeWidth = strokeWidthDp.value * density val y = size.height - strokeWidth / 2 drawLine( indicatorBorder.brush, Offset(0f, y), Offset(size.width, y), strokeWidth ) } } /** Padding from the label's baseline to the top */ internal val FirstBaselineOffset = 20.dp /** Padding from input field to the bottom */ internal val TextFieldBottomPadding = 10.dp /** Padding from label's baseline (or FirstBaselineOffset) to the input field */ /*@VisibleForTesting*/ internal val TextFieldTopPadding = 4.dp