1 /*
<lambda>null2 * Copyright 2022 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package androidx.compose.material3
18
19 import androidx.compose.animation.VectorConverter
20 import androidx.compose.animation.core.Animatable
21 import androidx.compose.animation.core.AnimationVector1D
22 import androidx.compose.animation.core.AnimationVector4D
23 import androidx.compose.animation.core.VectorConverter
24 import androidx.compose.animation.core.snap
25 import androidx.compose.foundation.ScrollState
26 import androidx.compose.foundation.interaction.FocusInteraction
27 import androidx.compose.foundation.interaction.Interaction
28 import androidx.compose.foundation.interaction.InteractionSource
29 import androidx.compose.foundation.interaction.MutableInteractionSource
30 import androidx.compose.foundation.interaction.collectIsFocusedAsState
31 import androidx.compose.foundation.layout.Box
32 import androidx.compose.foundation.layout.PaddingValues
33 import androidx.compose.foundation.layout.calculateEndPadding
34 import androidx.compose.foundation.layout.calculateStartPadding
35 import androidx.compose.foundation.layout.defaultMinSize
36 import androidx.compose.foundation.layout.heightIn
37 import androidx.compose.foundation.layout.padding
38 import androidx.compose.foundation.layout.wrapContentHeight
39 import androidx.compose.foundation.rememberScrollState
40 import androidx.compose.foundation.text.BasicTextField
41 import androidx.compose.foundation.text.KeyboardActions
42 import androidx.compose.foundation.text.KeyboardOptions
43 import androidx.compose.foundation.text.input.InputTransformation
44 import androidx.compose.foundation.text.input.KeyboardActionHandler
45 import androidx.compose.foundation.text.input.OutputTransformation
46 import androidx.compose.foundation.text.input.TextFieldLineLimits
47 import androidx.compose.foundation.text.input.TextFieldLineLimits.MultiLine
48 import androidx.compose.foundation.text.input.TextFieldLineLimits.SingleLine
49 import androidx.compose.foundation.text.input.TextFieldState
50 import androidx.compose.foundation.text.selection.LocalTextSelectionColors
51 import androidx.compose.material3.TextFieldDefaults.defaultTextFieldColors
52 import androidx.compose.material3.internal.AboveLabelBottomPadding
53 import androidx.compose.material3.internal.AboveLabelHorizontalPadding
54 import androidx.compose.material3.internal.ContainerId
55 import androidx.compose.material3.internal.FloatProducer
56 import androidx.compose.material3.internal.LabelId
57 import androidx.compose.material3.internal.LeadingId
58 import androidx.compose.material3.internal.MinFocusedLabelLineHeight
59 import androidx.compose.material3.internal.MinSupportingTextLineHeight
60 import androidx.compose.material3.internal.MinTextLineHeight
61 import androidx.compose.material3.internal.PlaceholderId
62 import androidx.compose.material3.internal.PrefixId
63 import androidx.compose.material3.internal.PrefixSuffixTextPadding
64 import androidx.compose.material3.internal.Strings
65 import androidx.compose.material3.internal.SuffixId
66 import androidx.compose.material3.internal.SupportingId
67 import androidx.compose.material3.internal.TextFieldId
68 import androidx.compose.material3.internal.TrailingId
69 import androidx.compose.material3.internal.defaultErrorSemantics
70 import androidx.compose.material3.internal.expandedAlignment
71 import androidx.compose.material3.internal.getString
72 import androidx.compose.material3.internal.heightOrZero
73 import androidx.compose.material3.internal.layoutId
74 import androidx.compose.material3.internal.minimizedAlignment
75 import androidx.compose.material3.internal.minimizedLabelHalfHeight
76 import androidx.compose.material3.internal.subtractConstraintSafely
77 import androidx.compose.material3.internal.textFieldHorizontalIconPadding
78 import androidx.compose.material3.internal.textFieldLabelMinHeight
79 import androidx.compose.material3.internal.widthOrZero
80 import androidx.compose.material3.tokens.FilledTextFieldTokens
81 import androidx.compose.material3.tokens.MotionSchemeKeyTokens
82 import androidx.compose.material3.tokens.MotionTokens.EasingEmphasizedAccelerateCubicBezier
83 import androidx.compose.runtime.Composable
84 import androidx.compose.runtime.CompositionLocalProvider
85 import androidx.compose.runtime.remember
86 import androidx.compose.ui.Alignment
87 import androidx.compose.ui.Modifier
88 import androidx.compose.ui.draw.CacheDrawModifierNode
89 import androidx.compose.ui.geometry.Rect
90 import androidx.compose.ui.graphics.Color
91 import androidx.compose.ui.graphics.Path
92 import androidx.compose.ui.graphics.Shape
93 import androidx.compose.ui.graphics.SolidColor
94 import androidx.compose.ui.graphics.addOutline
95 import androidx.compose.ui.graphics.takeOrElse
96 import androidx.compose.ui.layout.IntrinsicMeasurable
97 import androidx.compose.ui.layout.IntrinsicMeasureScope
98 import androidx.compose.ui.layout.Layout
99 import androidx.compose.ui.layout.Measurable
100 import androidx.compose.ui.layout.MeasurePolicy
101 import androidx.compose.ui.layout.MeasureResult
102 import androidx.compose.ui.layout.MeasureScope
103 import androidx.compose.ui.layout.Placeable
104 import androidx.compose.ui.layout.layoutId
105 import androidx.compose.ui.node.CompositionLocalConsumerModifierNode
106 import androidx.compose.ui.node.DelegatingNode
107 import androidx.compose.ui.node.ModifierNodeElement
108 import androidx.compose.ui.node.currentValueOf
109 import androidx.compose.ui.platform.InspectorInfo
110 import androidx.compose.ui.platform.LocalLayoutDirection
111 import androidx.compose.ui.text.TextLayoutResult
112 import androidx.compose.ui.text.TextStyle
113 import androidx.compose.ui.text.input.ImeAction
114 import androidx.compose.ui.text.input.KeyboardType
115 import androidx.compose.ui.text.input.TextFieldValue
116 import androidx.compose.ui.text.input.VisualTransformation
117 import androidx.compose.ui.unit.Constraints
118 import androidx.compose.ui.unit.Density
119 import androidx.compose.ui.unit.Dp
120 import androidx.compose.ui.unit.IntOffset
121 import androidx.compose.ui.unit.LayoutDirection
122 import androidx.compose.ui.unit.coerceAtLeast
123 import androidx.compose.ui.unit.constrainHeight
124 import androidx.compose.ui.unit.constrainWidth
125 import androidx.compose.ui.unit.dp
126 import androidx.compose.ui.unit.lerp
127 import androidx.compose.ui.unit.offset
128 import androidx.compose.ui.util.fastFirst
129 import androidx.compose.ui.util.fastFirstOrNull
130 import androidx.compose.ui.util.lerp
131 import kotlin.math.max
132 import kotlin.math.roundToInt
133 import kotlinx.coroutines.Job
134 import kotlinx.coroutines.launch
135
136 /**
137 * [Material Design filled text field](https://m3.material.io/components/text-fields/overview)
138 *
139 * Text fields allow users to enter text into a UI. They typically appear in forms and dialogs.
140 * Filled text fields have more visual emphasis than outlined text fields, making them stand out
141 * when surrounded by other content and components.
142 *
143 * 
145 *
146 * If you are looking for an outlined version, see [OutlinedTextField]. For a text field
147 * specifically designed for passwords or other secure content, see [SecureTextField].
148 *
149 * This overload of [TextField] uses [TextFieldState] to keep track of its text content and position
150 * of the cursor or selection.
151 *
152 * A simple single line text field looks like:
153 *
154 * @sample androidx.compose.material3.samples.SimpleTextFieldSample
155 *
156 * You can control the initial text input and selection:
157 *
158 * @sample androidx.compose.material3.samples.TextFieldWithInitialValueAndSelection
159 *
160 * Use input and output transformations to control user input and the displayed text:
161 *
162 * @sample androidx.compose.material3.samples.TextFieldWithTransformations
163 *
164 * You may provide a placeholder:
165 *
166 * @sample androidx.compose.material3.samples.TextFieldWithPlaceholder
167 *
168 * You can also provide leading and trailing icons:
169 *
170 * @sample androidx.compose.material3.samples.TextFieldWithIcons
171 *
172 * You can also provide a prefix or suffix to the text:
173 *
174 * @sample androidx.compose.material3.samples.TextFieldWithPrefixAndSuffix
175 *
176 * To handle the error input state, use [isError] parameter:
177 *
178 * @sample androidx.compose.material3.samples.TextFieldWithErrorState
179 *
180 * Additionally, you may provide additional message at the bottom:
181 *
182 * @sample androidx.compose.material3.samples.TextFieldWithSupportingText
183 *
184 * You can change the content padding to create a dense text field:
185 *
186 * @sample androidx.compose.material3.samples.DenseTextFieldContentPadding
187 *
188 * Hiding a software keyboard on IME action performed:
189 *
190 * @sample androidx.compose.material3.samples.TextFieldWithHideKeyboardOnImeAction
191 * @param state [TextFieldState] object that holds the internal editing state of the text field.
192 * @param modifier the [Modifier] to be applied to this text field.
193 * @param enabled controls the enabled state of this text field. When `false`, this component will
194 * not respond to user input, and it will appear visually disabled and disabled to accessibility
195 * services.
196 * @param readOnly controls the editable state of the text field. When `true`, the text field cannot
197 * be modified. However, a user can focus it and copy text from it. Read-only text fields are
198 * usually used to display pre-filled forms that a user cannot edit.
199 * @param textStyle the style to be applied to the input text. Defaults to [LocalTextStyle].
200 * @param labelPosition the position of the label. See [TextFieldLabelPosition].
201 * @param label the optional label to be displayed with this text field. The default text style uses
202 * [Typography.bodySmall] when minimized and [Typography.bodyLarge] when expanded.
203 * @param placeholder the optional placeholder to be displayed when the input text is empty. The
204 * default text style uses [Typography.bodyLarge].
205 * @param leadingIcon the optional leading icon to be displayed at the beginning of the text field
206 * container.
207 * @param trailingIcon the optional trailing icon to be displayed at the end of the text field
208 * container.
209 * @param prefix the optional prefix to be displayed before the input text in the text field.
210 * @param suffix the optional suffix to be displayed after the input text in the text field.
211 * @param supportingText the optional supporting text to be displayed below the text field.
212 * @param isError indicates if the text field's current value is in error. When `true`, the
213 * components of the text field will be displayed in an error color, and an error will be
214 * announced to accessibility services.
215 * @param inputTransformation optional [InputTransformation] that will be used to transform changes
216 * to the [TextFieldState] made by the user. The transformation will be applied to changes made by
217 * hardware and software keyboard events, pasting or dropping text, accessibility services, and
218 * tests. The transformation will _not_ be applied when changing the [state] programmatically, or
219 * when the transformation is changed. If the transformation is changed on an existing text field,
220 * it will be applied to the next user edit. The transformation will not immediately affect the
221 * current [state].
222 * @param outputTransformation optional [OutputTransformation] that transforms how the contents of
223 * the text field are presented.
224 * @param keyboardOptions software keyboard options that contains configuration such as
225 * [KeyboardType] and [ImeAction].
226 * @param onKeyboardAction called when the user presses the action button in the input method editor
227 * (IME), or by pressing the enter key on a hardware keyboard. By default this parameter is null,
228 * and would execute the default behavior for a received IME Action e.g., [ImeAction.Done] would
229 * close the keyboard, [ImeAction.Next] would switch the focus to the next focusable item on the
230 * screen.
231 * @param lineLimits whether the text field should be [SingleLine], scroll horizontally, and ignore
232 * newlines; or [MultiLine] and grow and scroll vertically. If [SingleLine] is passed, all newline
233 * characters ('\n') within the text will be replaced with regular whitespace (' ').
234 * @param onTextLayout Callback that is executed when the text layout becomes queryable. The
235 * callback receives a function that returns a [TextLayoutResult] if the layout can be calculated,
236 * or null if it cannot. The function reads the layout result from a snapshot state object, and
237 * will invalidate its caller when the layout result changes. A [TextLayoutResult] object contains
238 * paragraph information, size of the text, baselines and other details. [Density] scope is the
239 * one that was used while creating the given text layout.
240 * @param scrollState scroll state that manages either horizontal or vertical scroll of the text
241 * field. If [lineLimits] is [SingleLine], this text field is treated as single line with
242 * horizontal scroll behavior. Otherwise, the text field becomes vertically scrollable.
243 * @param shape defines the shape of this text field's container.
244 * @param colors [TextFieldColors] that will be used to resolve the colors used for this text field
245 * in different states. See [TextFieldDefaults.colors].
246 * @param contentPadding the padding applied to the inner text field that separates it from the
247 * surrounding elements of the text field. Note that the padding values may not be respected if
248 * they are incompatible with the text field's size constraints or layout. See
249 * [TextFieldDefaults.contentPaddingWithLabel] and [TextFieldDefaults.contentPaddingWithoutLabel].
250 * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
251 * emitting [Interaction]s for this text field. You can use this to change the text field's
252 * appearance or preview the text field in different states. Note that if `null` is provided,
253 * interactions will still happen internally.
254 */
255 @OptIn(ExperimentalMaterial3Api::class)
256 @Composable
257 fun TextField(
258 state: TextFieldState,
259 modifier: Modifier = Modifier,
260 enabled: Boolean = true,
261 readOnly: Boolean = false,
262 textStyle: TextStyle = LocalTextStyle.current,
263 labelPosition: TextFieldLabelPosition = TextFieldLabelPosition.Attached(),
264 label: @Composable (TextFieldLabelScope.() -> Unit)? = null,
265 placeholder: @Composable (() -> Unit)? = null,
266 leadingIcon: @Composable (() -> Unit)? = null,
267 trailingIcon: @Composable (() -> Unit)? = null,
268 prefix: @Composable (() -> Unit)? = null,
269 suffix: @Composable (() -> Unit)? = null,
270 supportingText: @Composable (() -> Unit)? = null,
271 isError: Boolean = false,
272 inputTransformation: InputTransformation? = null,
273 outputTransformation: OutputTransformation? = null,
274 keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
275 onKeyboardAction: KeyboardActionHandler? = null,
276 lineLimits: TextFieldLineLimits = TextFieldLineLimits.Default,
277 onTextLayout: (Density.(getResult: () -> TextLayoutResult?) -> Unit)? = null,
278 scrollState: ScrollState = rememberScrollState(),
279 shape: Shape = TextFieldDefaults.shape,
280 colors: TextFieldColors = TextFieldDefaults.colors(),
281 contentPadding: PaddingValues =
282 if (label == null || labelPosition is TextFieldLabelPosition.Above) {
283 TextFieldDefaults.contentPaddingWithoutLabel()
284 } else {
285 TextFieldDefaults.contentPaddingWithLabel()
286 },
287 interactionSource: MutableInteractionSource? = null,
288 ) {
289 @Suppress("NAME_SHADOWING")
<lambda>null290 val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
291 // If color is not provided via the text style, use content color as a default
292 val textColor =
<lambda>null293 textStyle.color.takeOrElse {
294 val focused = interactionSource.collectIsFocusedAsState().value
295 colors.textColor(enabled, isError, focused)
296 }
297 val mergedTextStyle = textStyle.merge(TextStyle(color = textColor))
298
<lambda>null299 CompositionLocalProvider(LocalTextSelectionColors provides colors.textSelectionColors) {
300 BasicTextField(
301 state = state,
302 modifier =
303 modifier
304 .defaultErrorSemantics(isError, getString(Strings.DefaultErrorMessage))
305 .defaultMinSize(
306 minWidth = TextFieldDefaults.MinWidth,
307 minHeight = TextFieldDefaults.MinHeight
308 ),
309 enabled = enabled,
310 readOnly = readOnly,
311 textStyle = mergedTextStyle,
312 cursorBrush = SolidColor(colors.cursorColor(isError)),
313 keyboardOptions = keyboardOptions,
314 onKeyboardAction = onKeyboardAction,
315 lineLimits = lineLimits,
316 onTextLayout = onTextLayout,
317 interactionSource = interactionSource,
318 inputTransformation = inputTransformation,
319 outputTransformation = outputTransformation,
320 scrollState = scrollState,
321 decorator =
322 TextFieldDefaults.decorator(
323 state = state,
324 enabled = enabled,
325 lineLimits = lineLimits,
326 outputTransformation = outputTransformation,
327 interactionSource = interactionSource,
328 labelPosition = labelPosition,
329 label = label,
330 placeholder = placeholder,
331 leadingIcon = leadingIcon,
332 trailingIcon = trailingIcon,
333 prefix = prefix,
334 suffix = suffix,
335 supportingText = supportingText,
336 isError = isError,
337 colors = colors,
338 contentPadding = contentPadding,
339 container = {
340 TextFieldDefaults.Container(
341 enabled = enabled,
342 isError = isError,
343 interactionSource = interactionSource,
344 colors = colors,
345 shape = shape,
346 )
347 }
348 )
349 )
350 }
351 }
352
353 /**
354 * [Material Design filled text field](https://m3.material.io/components/text-fields/overview)
355 *
356 * Text fields allow users to enter text into a UI. They typically appear in forms and dialogs.
357 * Filled text fields have more visual emphasis than outlined text fields, making them stand out
358 * when surrounded by other content and components.
359 *
360 * 
362 *
363 * If you are looking for an outlined version, see [OutlinedTextField].
364 *
365 * If apart from input text change you also want to observe the cursor location, selection range, or
366 * IME composition use the TextField overload with the [TextFieldValue] parameter instead.
367 *
368 * @param value the input text to be shown in the text field
369 * @param onValueChange the callback that is triggered when the input service updates the text. An
370 * updated text comes as a parameter of the callback
371 * @param modifier the [Modifier] to be applied to this text field
372 * @param enabled controls the enabled state of this text field. When `false`, this component will
373 * not respond to user input, and it will appear visually disabled and disabled to accessibility
374 * services.
375 * @param readOnly controls the editable state of the text field. When `true`, the text field cannot
376 * be modified. However, a user can focus it and copy text from it. Read-only text fields are
377 * usually used to display pre-filled forms that a user cannot edit.
378 * @param textStyle the style to be applied to the input text. Defaults to [LocalTextStyle].
379 * @param label the optional label to be displayed with this text field. The default text style uses
380 * [Typography.bodySmall] when minimized and [Typography.bodyLarge] when expanded.
381 * @param placeholder the optional placeholder to be displayed when the text field is in focus and
382 * the input text is empty. The default text style for internal [Text] is [Typography.bodyLarge]
383 * @param leadingIcon the optional leading icon to be displayed at the beginning of the text field
384 * container
385 * @param trailingIcon the optional trailing icon to be displayed at the end of the text field
386 * container
387 * @param prefix the optional prefix to be displayed before the input text in the text field
388 * @param suffix the optional suffix to be displayed after the input text in the text field
389 * @param supportingText the optional supporting text to be displayed below the text field
390 * @param isError indicates if the text field's current value is in error. If set to true, the
391 * label, bottom indicator and trailing icon by default will be displayed in error color
392 * @param visualTransformation transforms the visual representation of the input [value] For
393 * example, you can use
394 * [PasswordVisualTransformation][androidx.compose.ui.text.input.PasswordVisualTransformation] to
395 * create a password text field. By default, no visual transformation is applied.
396 * @param keyboardOptions software keyboard options that contains configuration such as
397 * [KeyboardType] and [ImeAction].
398 * @param keyboardActions when the input service emits an IME action, the corresponding callback is
399 * called. Note that this IME action may be different from what you specified in
400 * [KeyboardOptions.imeAction].
401 * @param singleLine when `true`, this text field becomes a single horizontally scrolling text field
402 * instead of wrapping onto multiple lines. The keyboard will be informed to not show the return
403 * key as the [ImeAction]. Note that [maxLines] parameter will be ignored as the maxLines
404 * attribute will be automatically set to 1.
405 * @param maxLines the maximum height in terms of maximum number of visible lines. It is required
406 * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true.
407 * @param minLines the minimum height in terms of minimum number of visible lines. It is required
408 * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true.
409 * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
410 * emitting [Interaction]s for this text field. You can use this to change the text field's
411 * appearance or preview the text field in different states. Note that if `null` is provided,
412 * interactions will still happen internally.
413 * @param shape defines the shape of this text field's container
414 * @param colors [TextFieldColors] that will be used to resolve the colors used for this text field
415 * in different states. See [TextFieldDefaults.colors].
416 */
417 @OptIn(ExperimentalMaterial3Api::class)
418 @Composable
TextFieldnull419 fun TextField(
420 value: String,
421 onValueChange: (String) -> Unit,
422 modifier: Modifier = Modifier,
423 enabled: Boolean = true,
424 readOnly: Boolean = false,
425 textStyle: TextStyle = LocalTextStyle.current,
426 label: @Composable (() -> Unit)? = null,
427 placeholder: @Composable (() -> Unit)? = null,
428 leadingIcon: @Composable (() -> Unit)? = null,
429 trailingIcon: @Composable (() -> Unit)? = null,
430 prefix: @Composable (() -> Unit)? = null,
431 suffix: @Composable (() -> Unit)? = null,
432 supportingText: @Composable (() -> Unit)? = null,
433 isError: Boolean = false,
434 visualTransformation: VisualTransformation = VisualTransformation.None,
435 keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
436 keyboardActions: KeyboardActions = KeyboardActions.Default,
437 singleLine: Boolean = false,
438 maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
439 minLines: Int = 1,
440 interactionSource: MutableInteractionSource? = null,
441 shape: Shape = TextFieldDefaults.shape,
442 colors: TextFieldColors = TextFieldDefaults.colors()
443 ) {
444 @Suppress("NAME_SHADOWING")
445 val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
446 // If color is not provided via the text style, use content color as a default
447 val textColor =
448 textStyle.color.takeOrElse {
449 val focused = interactionSource.collectIsFocusedAsState().value
450 colors.textColor(enabled, isError, focused)
451 }
452 val mergedTextStyle = textStyle.merge(TextStyle(color = textColor))
453
454 CompositionLocalProvider(LocalTextSelectionColors provides colors.textSelectionColors) {
455 BasicTextField(
456 value = value,
457 modifier =
458 modifier
459 .defaultErrorSemantics(isError, getString(Strings.DefaultErrorMessage))
460 .defaultMinSize(
461 minWidth = TextFieldDefaults.MinWidth,
462 minHeight = TextFieldDefaults.MinHeight
463 ),
464 onValueChange = onValueChange,
465 enabled = enabled,
466 readOnly = readOnly,
467 textStyle = mergedTextStyle,
468 cursorBrush = SolidColor(colors.cursorColor(isError)),
469 visualTransformation = visualTransformation,
470 keyboardOptions = keyboardOptions,
471 keyboardActions = keyboardActions,
472 interactionSource = interactionSource,
473 singleLine = singleLine,
474 maxLines = maxLines,
475 minLines = minLines,
476 decorationBox =
477 @Composable { innerTextField ->
478 // places leading icon, text field with label and placeholder, trailing icon
479 TextFieldDefaults.DecorationBox(
480 value = value,
481 visualTransformation = visualTransformation,
482 innerTextField = innerTextField,
483 placeholder = placeholder,
484 label = label,
485 leadingIcon = leadingIcon,
486 trailingIcon = trailingIcon,
487 prefix = prefix,
488 suffix = suffix,
489 supportingText = supportingText,
490 shape = shape,
491 singleLine = singleLine,
492 enabled = enabled,
493 isError = isError,
494 interactionSource = interactionSource,
495 colors = colors
496 )
497 }
498 )
499 }
500 }
501
502 /**
503 * [Material Design filled text field](https://m3.material.io/components/text-fields/overview)
504 *
505 * Text fields allow users to enter text into a UI. They typically appear in forms and dialogs.
506 * Filled text fields have more visual emphasis than outlined text fields, making them stand out
507 * when surrounded by other content and components.
508 *
509 * 
511 *
512 * If you are looking for an outlined version, see [OutlinedTextField].
513 *
514 * This overload provides access to the input text, cursor position, selection range and IME
515 * composition. If you only want to observe an input text change, use the TextField overload with
516 * the [String] parameter instead.
517 *
518 * @param value the input [TextFieldValue] to be shown in the text field
519 * @param onValueChange the callback that is triggered when the input service updates values in
520 * [TextFieldValue]. An updated [TextFieldValue] comes as a parameter of the callback
521 * @param modifier the [Modifier] to be applied to this text field
522 * @param enabled controls the enabled state of this text field. When `false`, this component will
523 * not respond to user input, and it will appear visually disabled and disabled to accessibility
524 * services.
525 * @param readOnly controls the editable state of the text field. When `true`, the text field cannot
526 * be modified. However, a user can focus it and copy text from it. Read-only text fields are
527 * usually used to display pre-filled forms that a user cannot edit.
528 * @param textStyle the style to be applied to the input text. Defaults to [LocalTextStyle].
529 * @param label the optional label to be displayed with this text field. The default text style uses
530 * [Typography.bodySmall] when minimized and [Typography.bodyLarge] when expanded.
531 * @param placeholder the optional placeholder to be displayed when the text field is in focus and
532 * the input text is empty. The default text style for internal [Text] is [Typography.bodyLarge]
533 * @param leadingIcon the optional leading icon to be displayed at the beginning of the text field
534 * container
535 * @param trailingIcon the optional trailing icon to be displayed at the end of the text field
536 * container
537 * @param prefix the optional prefix to be displayed before the input text in the text field
538 * @param suffix the optional suffix to be displayed after the input text in the text field
539 * @param supportingText the optional supporting text to be displayed below the text field
540 * @param isError indicates if the text field's current value is in error state. If set to true, the
541 * label, bottom indicator and trailing icon by default will be displayed in error color
542 * @param visualTransformation transforms the visual representation of the input [value]. For
543 * example, you can use
544 * [PasswordVisualTransformation][androidx.compose.ui.text.input.PasswordVisualTransformation] to
545 * create a password text field. By default, no visual transformation is applied.
546 * @param keyboardOptions software keyboard options that contains configuration such as
547 * [KeyboardType] and [ImeAction].
548 * @param keyboardActions when the input service emits an IME action, the corresponding callback is
549 * called. Note that this IME action may be different from what you specified in
550 * [KeyboardOptions.imeAction].
551 * @param singleLine when `true`, this text field becomes a single horizontally scrolling text field
552 * instead of wrapping onto multiple lines. The keyboard will be informed to not show the return
553 * key as the [ImeAction]. Note that [maxLines] parameter will be ignored as the maxLines
554 * attribute will be automatically set to 1.
555 * @param maxLines the maximum height in terms of maximum number of visible lines. It is required
556 * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true.
557 * @param minLines the minimum height in terms of minimum number of visible lines. It is required
558 * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true.
559 * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
560 * emitting [Interaction]s for this text field. You can use this to change the text field's
561 * appearance or preview the text field in different states. Note that if `null` is provided,
562 * interactions will still happen internally.
563 * @param shape defines the shape of this text field's container
564 * @param colors [TextFieldColors] that will be used to resolve the colors used for this text field
565 * in different states. See [TextFieldDefaults.colors].
566 */
567 @OptIn(ExperimentalMaterial3Api::class)
568 @Composable
TextFieldnull569 fun TextField(
570 value: TextFieldValue,
571 onValueChange: (TextFieldValue) -> Unit,
572 modifier: Modifier = Modifier,
573 enabled: Boolean = true,
574 readOnly: Boolean = false,
575 textStyle: TextStyle = LocalTextStyle.current,
576 label: @Composable (() -> Unit)? = null,
577 placeholder: @Composable (() -> Unit)? = null,
578 leadingIcon: @Composable (() -> Unit)? = null,
579 trailingIcon: @Composable (() -> Unit)? = null,
580 prefix: @Composable (() -> Unit)? = null,
581 suffix: @Composable (() -> Unit)? = null,
582 supportingText: @Composable (() -> Unit)? = null,
583 isError: Boolean = false,
584 visualTransformation: VisualTransformation = VisualTransformation.None,
585 keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
586 keyboardActions: KeyboardActions = KeyboardActions.Default,
587 singleLine: Boolean = false,
588 maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
589 minLines: Int = 1,
590 interactionSource: MutableInteractionSource? = null,
591 shape: Shape = TextFieldDefaults.shape,
592 colors: TextFieldColors = TextFieldDefaults.colors()
593 ) {
594 @Suppress("NAME_SHADOWING")
595 val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
596 // If color is not provided via the text style, use content color as a default
597 val textColor =
598 textStyle.color.takeOrElse {
599 val focused = interactionSource.collectIsFocusedAsState().value
600 colors.textColor(enabled, isError, focused)
601 }
602 val mergedTextStyle = textStyle.merge(TextStyle(color = textColor))
603
604 CompositionLocalProvider(LocalTextSelectionColors provides colors.textSelectionColors) {
605 BasicTextField(
606 value = value,
607 modifier =
608 modifier
609 .defaultErrorSemantics(isError, getString(Strings.DefaultErrorMessage))
610 .defaultMinSize(
611 minWidth = TextFieldDefaults.MinWidth,
612 minHeight = TextFieldDefaults.MinHeight
613 ),
614 onValueChange = onValueChange,
615 enabled = enabled,
616 readOnly = readOnly,
617 textStyle = mergedTextStyle,
618 cursorBrush = SolidColor(colors.cursorColor(isError)),
619 visualTransformation = visualTransformation,
620 keyboardOptions = keyboardOptions,
621 keyboardActions = keyboardActions,
622 interactionSource = interactionSource,
623 singleLine = singleLine,
624 maxLines = maxLines,
625 minLines = minLines,
626 decorationBox =
627 @Composable { innerTextField ->
628 // places leading icon, text field with label and placeholder, trailing icon
629 TextFieldDefaults.DecorationBox(
630 value = value.text,
631 visualTransformation = visualTransformation,
632 innerTextField = innerTextField,
633 placeholder = placeholder,
634 label = label,
635 leadingIcon = leadingIcon,
636 trailingIcon = trailingIcon,
637 prefix = prefix,
638 suffix = suffix,
639 supportingText = supportingText,
640 shape = shape,
641 singleLine = singleLine,
642 enabled = enabled,
643 isError = isError,
644 interactionSource = interactionSource,
645 colors = colors
646 )
647 }
648 )
649 }
650 }
651
652 /**
653 * Composable responsible for measuring and laying out leading and trailing icons, label,
654 * placeholder and the input field.
655 */
656 @Composable
657 internal fun TextFieldLayout(
658 modifier: Modifier,
659 textField: @Composable () -> Unit,
660 label: @Composable (() -> Unit)?,
661 placeholder: @Composable ((Modifier) -> Unit)?,
662 leading: @Composable (() -> Unit)?,
663 trailing: @Composable (() -> Unit)?,
664 prefix: @Composable (() -> Unit)?,
665 suffix: @Composable (() -> Unit)?,
666 singleLine: Boolean,
667 labelPosition: TextFieldLabelPosition,
668 labelProgress: FloatProducer,
669 container: @Composable () -> Unit,
670 supporting: @Composable (() -> Unit)?,
671 paddingValues: PaddingValues
672 ) {
673 val minimizedLabelHalfHeight = minimizedLabelHalfHeight()
674 val measurePolicy =
675 remember(
676 singleLine,
677 labelPosition,
678 labelProgress,
679 paddingValues,
680 minimizedLabelHalfHeight,
<lambda>null681 ) {
682 TextFieldMeasurePolicy(
683 singleLine = singleLine,
684 labelPosition = labelPosition,
685 labelProgress = labelProgress,
686 paddingValues = paddingValues,
687 minimizedLabelHalfHeight = minimizedLabelHalfHeight,
688 )
689 }
690 val layoutDirection = LocalLayoutDirection.current
691 Layout(
692 modifier = modifier,
<lambda>null693 content = {
694 // The container is given as a Composable instead of a background modifier so that
695 // elements like supporting text can be placed outside of it while still contributing
696 // to the text field's measurements overall.
697 container()
698
699 if (leading != null) {
700 Box(
701 modifier = Modifier.layoutId(LeadingId).minimumInteractiveComponentSize(),
702 contentAlignment = Alignment.Center
703 ) {
704 leading()
705 }
706 }
707 if (trailing != null) {
708 Box(
709 modifier = Modifier.layoutId(TrailingId).minimumInteractiveComponentSize(),
710 contentAlignment = Alignment.Center
711 ) {
712 trailing()
713 }
714 }
715
716 val startTextFieldPadding = paddingValues.calculateStartPadding(layoutDirection)
717 val endTextFieldPadding = paddingValues.calculateEndPadding(layoutDirection)
718
719 val horizontalIconPadding = textFieldHorizontalIconPadding()
720 val startPadding =
721 if (leading != null) {
722 (startTextFieldPadding - horizontalIconPadding).coerceAtLeast(0.dp)
723 } else {
724 startTextFieldPadding
725 }
726 val endPadding =
727 if (trailing != null) {
728 (endTextFieldPadding - horizontalIconPadding).coerceAtLeast(0.dp)
729 } else {
730 endTextFieldPadding
731 }
732
733 if (prefix != null) {
734 Box(
735 Modifier.layoutId(PrefixId)
736 .heightIn(min = MinTextLineHeight)
737 .wrapContentHeight()
738 .padding(start = startPadding, end = PrefixSuffixTextPadding)
739 ) {
740 prefix()
741 }
742 }
743 if (suffix != null) {
744 Box(
745 Modifier.layoutId(SuffixId)
746 .heightIn(min = MinTextLineHeight)
747 .wrapContentHeight()
748 .padding(start = PrefixSuffixTextPadding, end = endPadding)
749 ) {
750 suffix()
751 }
752 }
753
754 val labelPadding =
755 if (labelPosition is TextFieldLabelPosition.Above) {
756 Modifier.padding(
757 start = AboveLabelHorizontalPadding,
758 end = AboveLabelHorizontalPadding,
759 bottom = AboveLabelBottomPadding,
760 )
761 } else {
762 Modifier.padding(start = startPadding, end = endPadding)
763 }
764 if (label != null) {
765 Box(
766 Modifier.layoutId(LabelId)
767 .textFieldLabelMinHeight {
768 lerp(MinTextLineHeight, MinFocusedLabelLineHeight, labelProgress())
769 }
770 .wrapContentHeight()
771 .then(labelPadding)
772 ) {
773 label()
774 }
775 }
776
777 val textPadding =
778 Modifier.heightIn(min = MinTextLineHeight)
779 .wrapContentHeight()
780 .padding(
781 start = if (prefix == null) startPadding else 0.dp,
782 end = if (suffix == null) endPadding else 0.dp,
783 )
784
785 if (placeholder != null) {
786 placeholder(Modifier.layoutId(PlaceholderId).then(textPadding))
787 }
788 Box(
789 modifier = Modifier.layoutId(TextFieldId).then(textPadding),
790 propagateMinConstraints = true,
791 ) {
792 textField()
793 }
794
795 if (supporting != null) {
796 @OptIn(ExperimentalMaterial3Api::class)
797 Box(
798 Modifier.layoutId(SupportingId)
799 .heightIn(min = MinSupportingTextLineHeight)
800 .wrapContentHeight()
801 .padding(TextFieldDefaults.supportingTextPadding())
802 ) {
803 supporting()
804 }
805 }
806 },
807 measurePolicy = measurePolicy
808 )
809 }
810
811 private class TextFieldMeasurePolicy(
812 private val singleLine: Boolean,
813 private val labelPosition: TextFieldLabelPosition,
814 private val labelProgress: FloatProducer,
815 private val paddingValues: PaddingValues,
816 private val minimizedLabelHalfHeight: Dp,
817 ) : MeasurePolicy {
measurenull818 override fun MeasureScope.measure(
819 measurables: List<Measurable>,
820 constraints: Constraints
821 ): MeasureResult {
822 val labelProgress = labelProgress()
823 val topPaddingValue = paddingValues.calculateTopPadding().roundToPx()
824 val bottomPaddingValue = paddingValues.calculateBottomPadding().roundToPx()
825
826 var occupiedSpaceHorizontally = 0
827 var occupiedSpaceVertically = 0
828
829 val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)
830
831 // measure leading icon
832 val leadingPlaceable =
833 measurables.fastFirstOrNull { it.layoutId == LeadingId }?.measure(looseConstraints)
834 occupiedSpaceHorizontally += leadingPlaceable.widthOrZero
835 occupiedSpaceVertically = max(occupiedSpaceVertically, leadingPlaceable.heightOrZero)
836
837 // measure trailing icon
838 val trailingPlaceable =
839 measurables
840 .fastFirstOrNull { it.layoutId == TrailingId }
841 ?.measure(looseConstraints.offset(horizontal = -occupiedSpaceHorizontally))
842 occupiedSpaceHorizontally += trailingPlaceable.widthOrZero
843 occupiedSpaceVertically = max(occupiedSpaceVertically, trailingPlaceable.heightOrZero)
844
845 // measure prefix
846 val prefixPlaceable =
847 measurables
848 .fastFirstOrNull { it.layoutId == PrefixId }
849 ?.measure(looseConstraints.offset(horizontal = -occupiedSpaceHorizontally))
850 occupiedSpaceHorizontally += prefixPlaceable.widthOrZero
851 occupiedSpaceVertically = max(occupiedSpaceVertically, prefixPlaceable.heightOrZero)
852
853 // measure suffix
854 val suffixPlaceable =
855 measurables
856 .fastFirstOrNull { it.layoutId == SuffixId }
857 ?.measure(looseConstraints.offset(horizontal = -occupiedSpaceHorizontally))
858 occupiedSpaceHorizontally += suffixPlaceable.widthOrZero
859 occupiedSpaceVertically = max(occupiedSpaceVertically, suffixPlaceable.heightOrZero)
860
861 val isLabelAbove = labelPosition is TextFieldLabelPosition.Above
862 val labelMeasurable = measurables.fastFirstOrNull { it.layoutId == LabelId }
863 var labelPlaceable: Placeable? = null
864 val labelIntrinsicHeight: Int
865 if (!isLabelAbove) {
866 // if label is not Above, we can measure it like normal
867 val labelConstraints =
868 looseConstraints.offset(
869 vertical = -bottomPaddingValue,
870 horizontal = -occupiedSpaceHorizontally
871 )
872 labelPlaceable = labelMeasurable?.measure(labelConstraints)
873 labelIntrinsicHeight = 0
874 } else {
875 // if label is Above, it must be measured after other elements, but we
876 // reserve space for it using its intrinsic height as a heuristic
877 labelIntrinsicHeight = labelMeasurable?.minIntrinsicHeight(constraints.minWidth) ?: 0
878 }
879
880 // supporting text must be measured after other elements, but we
881 // reserve space for it using its intrinsic height as a heuristic
882 val supportingMeasurable = measurables.fastFirstOrNull { it.layoutId == SupportingId }
883 val supportingIntrinsicHeight =
884 supportingMeasurable?.minIntrinsicHeight(constraints.minWidth) ?: 0
885
886 // at most one of these is non-zero
887 val labelHeightOrIntrinsic = labelPlaceable.heightOrZero + labelIntrinsicHeight
888
889 // measure input field
890 val effectiveTopOffset = topPaddingValue + labelHeightOrIntrinsic
891 val textFieldConstraints =
892 constraints
893 .copy(minHeight = 0)
894 .offset(
895 vertical = -effectiveTopOffset - bottomPaddingValue - supportingIntrinsicHeight,
896 horizontal = -occupiedSpaceHorizontally
897 )
898 val textFieldPlaceable =
899 measurables.fastFirst { it.layoutId == TextFieldId }.measure(textFieldConstraints)
900
901 // measure placeholder
902 val placeholderConstraints = textFieldConstraints.copy(minWidth = 0)
903 val placeholderPlaceable =
904 measurables
905 .fastFirstOrNull { it.layoutId == PlaceholderId }
906 ?.measure(placeholderConstraints)
907
908 occupiedSpaceVertically =
909 max(
910 occupiedSpaceVertically,
911 max(textFieldPlaceable.heightOrZero, placeholderPlaceable.heightOrZero) +
912 effectiveTopOffset +
913 bottomPaddingValue
914 )
915 val width =
916 calculateWidth(
917 leadingWidth = leadingPlaceable.widthOrZero,
918 trailingWidth = trailingPlaceable.widthOrZero,
919 prefixWidth = prefixPlaceable.widthOrZero,
920 suffixWidth = suffixPlaceable.widthOrZero,
921 textFieldWidth = textFieldPlaceable.width,
922 labelWidth = labelPlaceable.widthOrZero,
923 placeholderWidth = placeholderPlaceable.widthOrZero,
924 constraints = constraints,
925 )
926
927 if (isLabelAbove) {
928 // now that we know the width, measure label
929 val labelConstraints =
930 looseConstraints.copy(maxHeight = labelIntrinsicHeight, maxWidth = width)
931 labelPlaceable = labelMeasurable?.measure(labelConstraints)
932 }
933
934 // measure supporting text
935 val supportingConstraints =
936 looseConstraints
937 .offset(vertical = -occupiedSpaceVertically)
938 .copy(minHeight = 0, maxWidth = width)
939 val supportingPlaceable = supportingMeasurable?.measure(supportingConstraints)
940 val supportingHeight = supportingPlaceable.heightOrZero
941
942 val totalHeight =
943 calculateHeight(
944 textFieldHeight = textFieldPlaceable.height,
945 labelHeight = labelPlaceable.heightOrZero,
946 leadingHeight = leadingPlaceable.heightOrZero,
947 trailingHeight = trailingPlaceable.heightOrZero,
948 prefixHeight = prefixPlaceable.heightOrZero,
949 suffixHeight = suffixPlaceable.heightOrZero,
950 placeholderHeight = placeholderPlaceable.heightOrZero,
951 supportingHeight = supportingPlaceable.heightOrZero,
952 constraints = constraints,
953 isLabelAbove = isLabelAbove,
954 labelProgress = labelProgress,
955 )
956 val height =
957 totalHeight - supportingHeight - (if (isLabelAbove) labelPlaceable.heightOrZero else 0)
958
959 val containerPlaceable =
960 measurables
961 .fastFirst { it.layoutId == ContainerId }
962 .measure(
963 Constraints(
964 minWidth = if (width != Constraints.Infinity) width else 0,
965 maxWidth = width,
966 minHeight = if (height != Constraints.Infinity) height else 0,
967 maxHeight = height
968 )
969 )
970
971 return layout(width, totalHeight) {
972 if (labelPlaceable != null) {
973 val labelStartY =
974 when {
975 isLabelAbove -> 0
976 singleLine ->
977 Alignment.CenterVertically.align(labelPlaceable.height, height)
978 else ->
979 // The padding defined by the user only applies to the text field when
980 // the label is focused. More padding needs to be added when the text
981 // field is unfocused.
982 topPaddingValue + minimizedLabelHalfHeight.roundToPx()
983 }
984 val labelEndY =
985 when {
986 isLabelAbove -> 0
987 else -> topPaddingValue
988 }
989 placeWithLabel(
990 width = width,
991 totalHeight = totalHeight,
992 textfieldPlaceable = textFieldPlaceable,
993 labelPlaceable = labelPlaceable,
994 placeholderPlaceable = placeholderPlaceable,
995 leadingPlaceable = leadingPlaceable,
996 trailingPlaceable = trailingPlaceable,
997 prefixPlaceable = prefixPlaceable,
998 suffixPlaceable = suffixPlaceable,
999 containerPlaceable = containerPlaceable,
1000 supportingPlaceable = supportingPlaceable,
1001 labelStartY = labelStartY,
1002 labelEndY = labelEndY,
1003 isLabelAbove = isLabelAbove,
1004 labelProgress = labelProgress,
1005 textPosition =
1006 topPaddingValue + (if (isLabelAbove) 0 else labelPlaceable.height),
1007 layoutDirection = layoutDirection,
1008 )
1009 } else {
1010 placeWithoutLabel(
1011 width = width,
1012 totalHeight = totalHeight,
1013 textPlaceable = textFieldPlaceable,
1014 placeholderPlaceable = placeholderPlaceable,
1015 leadingPlaceable = leadingPlaceable,
1016 trailingPlaceable = trailingPlaceable,
1017 prefixPlaceable = prefixPlaceable,
1018 suffixPlaceable = suffixPlaceable,
1019 containerPlaceable = containerPlaceable,
1020 supportingPlaceable = supportingPlaceable,
1021 density = density,
1022 )
1023 }
1024 }
1025 }
1026
maxIntrinsicHeightnull1027 override fun IntrinsicMeasureScope.maxIntrinsicHeight(
1028 measurables: List<IntrinsicMeasurable>,
1029 width: Int
1030 ): Int {
1031 return intrinsicHeight(measurables, width) { intrinsicMeasurable, w ->
1032 intrinsicMeasurable.maxIntrinsicHeight(w)
1033 }
1034 }
1035
minIntrinsicHeightnull1036 override fun IntrinsicMeasureScope.minIntrinsicHeight(
1037 measurables: List<IntrinsicMeasurable>,
1038 width: Int
1039 ): Int {
1040 return intrinsicHeight(measurables, width) { intrinsicMeasurable, w ->
1041 intrinsicMeasurable.minIntrinsicHeight(w)
1042 }
1043 }
1044
maxIntrinsicWidthnull1045 override fun IntrinsicMeasureScope.maxIntrinsicWidth(
1046 measurables: List<IntrinsicMeasurable>,
1047 height: Int
1048 ): Int {
1049 return intrinsicWidth(measurables, height) { intrinsicMeasurable, h ->
1050 intrinsicMeasurable.maxIntrinsicWidth(h)
1051 }
1052 }
1053
minIntrinsicWidthnull1054 override fun IntrinsicMeasureScope.minIntrinsicWidth(
1055 measurables: List<IntrinsicMeasurable>,
1056 height: Int
1057 ): Int {
1058 return intrinsicWidth(measurables, height) { intrinsicMeasurable, h ->
1059 intrinsicMeasurable.minIntrinsicWidth(h)
1060 }
1061 }
1062
intrinsicWidthnull1063 private fun intrinsicWidth(
1064 measurables: List<IntrinsicMeasurable>,
1065 height: Int,
1066 intrinsicMeasurer: (IntrinsicMeasurable, Int) -> Int
1067 ): Int {
1068 val textFieldWidth =
1069 intrinsicMeasurer(measurables.fastFirst { it.layoutId == TextFieldId }, height)
1070 val labelWidth =
1071 measurables
1072 .fastFirstOrNull { it.layoutId == LabelId }
1073 ?.let { intrinsicMeasurer(it, height) } ?: 0
1074 val trailingWidth =
1075 measurables
1076 .fastFirstOrNull { it.layoutId == TrailingId }
1077 ?.let { intrinsicMeasurer(it, height) } ?: 0
1078 val prefixWidth =
1079 measurables
1080 .fastFirstOrNull { it.layoutId == PrefixId }
1081 ?.let { intrinsicMeasurer(it, height) } ?: 0
1082 val suffixWidth =
1083 measurables
1084 .fastFirstOrNull { it.layoutId == SuffixId }
1085 ?.let { intrinsicMeasurer(it, height) } ?: 0
1086 val leadingWidth =
1087 measurables
1088 .fastFirstOrNull { it.layoutId == LeadingId }
1089 ?.let { intrinsicMeasurer(it, height) } ?: 0
1090 val placeholderWidth =
1091 measurables
1092 .fastFirstOrNull { it.layoutId == PlaceholderId }
1093 ?.let { intrinsicMeasurer(it, height) } ?: 0
1094 return calculateWidth(
1095 leadingWidth = leadingWidth,
1096 trailingWidth = trailingWidth,
1097 prefixWidth = prefixWidth,
1098 suffixWidth = suffixWidth,
1099 textFieldWidth = textFieldWidth,
1100 labelWidth = labelWidth,
1101 placeholderWidth = placeholderWidth,
1102 constraints = Constraints(),
1103 )
1104 }
1105
intrinsicHeightnull1106 private fun IntrinsicMeasureScope.intrinsicHeight(
1107 measurables: List<IntrinsicMeasurable>,
1108 width: Int,
1109 intrinsicMeasurer: (IntrinsicMeasurable, Int) -> Int
1110 ): Int {
1111 var remainingWidth = width
1112 val leadingHeight =
1113 measurables
1114 .fastFirstOrNull { it.layoutId == LeadingId }
1115 ?.let {
1116 remainingWidth =
1117 remainingWidth.subtractConstraintSafely(
1118 it.maxIntrinsicWidth(Constraints.Infinity)
1119 )
1120 intrinsicMeasurer(it, width)
1121 } ?: 0
1122 val trailingHeight =
1123 measurables
1124 .fastFirstOrNull { it.layoutId == TrailingId }
1125 ?.let {
1126 remainingWidth =
1127 remainingWidth.subtractConstraintSafely(
1128 it.maxIntrinsicWidth(Constraints.Infinity)
1129 )
1130 intrinsicMeasurer(it, width)
1131 } ?: 0
1132 val labelHeight =
1133 measurables
1134 .fastFirstOrNull { it.layoutId == LabelId }
1135 ?.let { intrinsicMeasurer(it, remainingWidth) } ?: 0
1136
1137 val prefixHeight =
1138 measurables
1139 .fastFirstOrNull { it.layoutId == PrefixId }
1140 ?.let {
1141 val height = intrinsicMeasurer(it, remainingWidth)
1142 remainingWidth =
1143 remainingWidth.subtractConstraintSafely(
1144 it.maxIntrinsicWidth(Constraints.Infinity)
1145 )
1146 height
1147 } ?: 0
1148 val suffixHeight =
1149 measurables
1150 .fastFirstOrNull { it.layoutId == SuffixId }
1151 ?.let {
1152 val height = intrinsicMeasurer(it, remainingWidth)
1153 remainingWidth =
1154 remainingWidth.subtractConstraintSafely(
1155 it.maxIntrinsicWidth(Constraints.Infinity)
1156 )
1157 height
1158 } ?: 0
1159
1160 val textFieldHeight =
1161 intrinsicMeasurer(measurables.fastFirst { it.layoutId == TextFieldId }, remainingWidth)
1162 val placeholderHeight =
1163 measurables
1164 .fastFirstOrNull { it.layoutId == PlaceholderId }
1165 ?.let { intrinsicMeasurer(it, remainingWidth) } ?: 0
1166
1167 val supportingHeight =
1168 measurables
1169 .fastFirstOrNull { it.layoutId == SupportingId }
1170 ?.let { intrinsicMeasurer(it, width) } ?: 0
1171
1172 return calculateHeight(
1173 textFieldHeight = textFieldHeight,
1174 labelHeight = labelHeight,
1175 leadingHeight = leadingHeight,
1176 trailingHeight = trailingHeight,
1177 prefixHeight = prefixHeight,
1178 suffixHeight = suffixHeight,
1179 placeholderHeight = placeholderHeight,
1180 supportingHeight = supportingHeight,
1181 constraints = Constraints(),
1182 isLabelAbove = labelPosition is TextFieldLabelPosition.Above,
1183 labelProgress = labelProgress(),
1184 )
1185 }
1186
calculateWidthnull1187 private fun calculateWidth(
1188 leadingWidth: Int,
1189 trailingWidth: Int,
1190 prefixWidth: Int,
1191 suffixWidth: Int,
1192 textFieldWidth: Int,
1193 labelWidth: Int,
1194 placeholderWidth: Int,
1195 constraints: Constraints
1196 ): Int {
1197 val affixTotalWidth = prefixWidth + suffixWidth
1198 val middleSection =
1199 maxOf(
1200 textFieldWidth + affixTotalWidth,
1201 placeholderWidth + affixTotalWidth,
1202 // Prefix/suffix does not get applied to label
1203 labelWidth,
1204 )
1205 val wrappedWidth = leadingWidth + middleSection + trailingWidth
1206 return constraints.constrainWidth(wrappedWidth)
1207 }
1208
calculateHeightnull1209 private fun Density.calculateHeight(
1210 textFieldHeight: Int,
1211 labelHeight: Int,
1212 leadingHeight: Int,
1213 trailingHeight: Int,
1214 prefixHeight: Int,
1215 suffixHeight: Int,
1216 placeholderHeight: Int,
1217 supportingHeight: Int,
1218 constraints: Constraints,
1219 isLabelAbove: Boolean,
1220 labelProgress: Float,
1221 ): Int {
1222 val verticalPadding =
1223 (paddingValues.calculateTopPadding() + paddingValues.calculateBottomPadding())
1224 .roundToPx()
1225
1226 val inputFieldHeight =
1227 maxOf(
1228 textFieldHeight,
1229 placeholderHeight,
1230 prefixHeight,
1231 suffixHeight,
1232 if (isLabelAbove) 0 else lerp(labelHeight, 0, labelProgress)
1233 )
1234
1235 val hasLabel = labelHeight > 0
1236 val nonOverlappedLabelHeight =
1237 if (hasLabel && !isLabelAbove) {
1238 // The label animates from overlapping the input field to floating above it,
1239 // so its contribution to the height calculation changes over time. A baseline
1240 // height is provided in the unfocused state to keep the overall height consistent
1241 // across the animation.
1242 max(
1243 (minimizedLabelHalfHeight * 2).roundToPx(),
1244 lerp(
1245 0,
1246 labelHeight,
1247 EasingEmphasizedAccelerateCubicBezier.transform(labelProgress)
1248 )
1249 )
1250 } else {
1251 0
1252 }
1253
1254 val middleSectionHeight = verticalPadding + nonOverlappedLabelHeight + inputFieldHeight
1255
1256 return constraints.constrainHeight(
1257 (if (isLabelAbove) labelHeight else 0) +
1258 maxOf(leadingHeight, trailingHeight, middleSectionHeight) +
1259 supportingHeight
1260 )
1261 }
1262
1263 /**
1264 * Places the provided text field, placeholder, and label in the TextField given the
1265 * PaddingValues when there is a label. When there is no label, [placeWithoutLabel] is used
1266 * instead.
1267 */
placeWithLabelnull1268 private fun Placeable.PlacementScope.placeWithLabel(
1269 width: Int,
1270 totalHeight: Int,
1271 textfieldPlaceable: Placeable,
1272 labelPlaceable: Placeable,
1273 placeholderPlaceable: Placeable?,
1274 leadingPlaceable: Placeable?,
1275 trailingPlaceable: Placeable?,
1276 prefixPlaceable: Placeable?,
1277 suffixPlaceable: Placeable?,
1278 containerPlaceable: Placeable,
1279 supportingPlaceable: Placeable?,
1280 labelStartY: Int,
1281 labelEndY: Int,
1282 isLabelAbove: Boolean,
1283 labelProgress: Float,
1284 textPosition: Int,
1285 layoutDirection: LayoutDirection,
1286 ) {
1287 val yOffset = if (isLabelAbove) labelPlaceable.height else 0
1288
1289 // place container
1290 containerPlaceable.place(0, yOffset)
1291
1292 // Most elements should be positioned w.r.t the text field's "visual" height, i.e.,
1293 // excluding the label (if it's Above) and the supporting text on bottom
1294 val height =
1295 totalHeight -
1296 supportingPlaceable.heightOrZero -
1297 (if (isLabelAbove) labelPlaceable.height else 0)
1298
1299 leadingPlaceable?.placeRelative(
1300 0,
1301 yOffset + Alignment.CenterVertically.align(leadingPlaceable.height, height)
1302 )
1303
1304 val labelY = lerp(labelStartY, labelEndY, labelProgress)
1305 if (isLabelAbove) {
1306 val labelX =
1307 labelPosition.minimizedAlignment.align(
1308 size = labelPlaceable.width,
1309 space = width,
1310 layoutDirection = layoutDirection,
1311 )
1312 // Not placeRelative because alignment already handles RTL
1313 labelPlaceable.place(labelX, labelY)
1314 } else {
1315 val leftIconWidth =
1316 if (layoutDirection == LayoutDirection.Ltr) leadingPlaceable.widthOrZero
1317 else trailingPlaceable.widthOrZero
1318 val labelStartX =
1319 labelPosition.expandedAlignment.align(
1320 size = labelPlaceable.width,
1321 space = width - leadingPlaceable.widthOrZero - trailingPlaceable.widthOrZero,
1322 layoutDirection = layoutDirection,
1323 ) + leftIconWidth
1324 val labelEndX =
1325 labelPosition.minimizedAlignment.align(
1326 size = labelPlaceable.width,
1327 space = width - leadingPlaceable.widthOrZero - trailingPlaceable.widthOrZero,
1328 layoutDirection = layoutDirection,
1329 ) + leftIconWidth
1330 val labelX = lerp(labelStartX, labelEndX, labelProgress)
1331 // Not placeRelative because alignment already handles RTL
1332 labelPlaceable.place(labelX, labelY)
1333 }
1334
1335 prefixPlaceable?.placeRelative(leadingPlaceable.widthOrZero, yOffset + textPosition)
1336
1337 val textHorizontalPosition = leadingPlaceable.widthOrZero + prefixPlaceable.widthOrZero
1338 textfieldPlaceable.placeRelative(textHorizontalPosition, yOffset + textPosition)
1339 placeholderPlaceable?.placeRelative(textHorizontalPosition, yOffset + textPosition)
1340
1341 suffixPlaceable?.placeRelative(
1342 width - trailingPlaceable.widthOrZero - suffixPlaceable.width,
1343 yOffset + textPosition,
1344 )
1345
1346 trailingPlaceable?.placeRelative(
1347 width - trailingPlaceable.width,
1348 yOffset + Alignment.CenterVertically.align(trailingPlaceable.height, height)
1349 )
1350
1351 supportingPlaceable?.placeRelative(0, yOffset + height)
1352 }
1353
1354 /**
1355 * Places the provided text field and placeholder in [TextField] when there is no label. When
1356 * there is a label, [placeWithLabel] is used
1357 */
Placeablenull1358 private fun Placeable.PlacementScope.placeWithoutLabel(
1359 width: Int,
1360 totalHeight: Int,
1361 textPlaceable: Placeable,
1362 placeholderPlaceable: Placeable?,
1363 leadingPlaceable: Placeable?,
1364 trailingPlaceable: Placeable?,
1365 prefixPlaceable: Placeable?,
1366 suffixPlaceable: Placeable?,
1367 containerPlaceable: Placeable,
1368 supportingPlaceable: Placeable?,
1369 density: Float,
1370 ) {
1371 // place container
1372 containerPlaceable.place(IntOffset.Zero)
1373
1374 // Most elements should be positioned w.r.t the text field's "visual" height, i.e.,
1375 // excluding the supporting text on bottom
1376 val height = totalHeight - supportingPlaceable.heightOrZero
1377 val topPadding = (paddingValues.calculateTopPadding().value * density).roundToInt()
1378
1379 leadingPlaceable?.placeRelative(
1380 0,
1381 Alignment.CenterVertically.align(leadingPlaceable.height, height)
1382 )
1383
1384 // Single line text field without label places its text components centered vertically.
1385 // Multiline text field without label places its text components at the top with padding.
1386 fun calculateVerticalPosition(placeable: Placeable): Int {
1387 return if (singleLine) {
1388 Alignment.CenterVertically.align(placeable.height, height)
1389 } else {
1390 topPadding
1391 }
1392 }
1393
1394 prefixPlaceable?.placeRelative(
1395 leadingPlaceable.widthOrZero,
1396 calculateVerticalPosition(prefixPlaceable)
1397 )
1398
1399 val textHorizontalPosition = leadingPlaceable.widthOrZero + prefixPlaceable.widthOrZero
1400
1401 textPlaceable.placeRelative(
1402 textHorizontalPosition,
1403 calculateVerticalPosition(textPlaceable)
1404 )
1405
1406 placeholderPlaceable?.placeRelative(
1407 textHorizontalPosition,
1408 calculateVerticalPosition(placeholderPlaceable)
1409 )
1410
1411 suffixPlaceable?.placeRelative(
1412 width - trailingPlaceable.widthOrZero - suffixPlaceable.width,
1413 calculateVerticalPosition(suffixPlaceable),
1414 )
1415
1416 trailingPlaceable?.placeRelative(
1417 width - trailingPlaceable.width,
1418 Alignment.CenterVertically.align(trailingPlaceable.height, height)
1419 )
1420
1421 supportingPlaceable?.placeRelative(0, height)
1422 }
1423 }
1424
1425 internal data class IndicatorLineElement(
1426 val enabled: Boolean,
1427 val isError: Boolean,
1428 val interactionSource: InteractionSource,
1429 val colors: TextFieldColors?,
1430 val textFieldShape: Shape?,
1431 val focusedIndicatorLineThickness: Dp,
1432 val unfocusedIndicatorLineThickness: Dp,
1433 ) : ModifierNodeElement<IndicatorLineNode>() {
createnull1434 override fun create(): IndicatorLineNode {
1435 return IndicatorLineNode(
1436 enabled = enabled,
1437 isError = isError,
1438 interactionSource = interactionSource,
1439 colors = colors,
1440 textFieldShape = textFieldShape,
1441 focusedIndicatorWidth = focusedIndicatorLineThickness,
1442 unfocusedIndicatorWidth = unfocusedIndicatorLineThickness,
1443 )
1444 }
1445
updatenull1446 override fun update(node: IndicatorLineNode) {
1447 node.update(
1448 enabled = enabled,
1449 isError = isError,
1450 interactionSource = interactionSource,
1451 colors = colors,
1452 textFieldShape = textFieldShape,
1453 focusedIndicatorWidth = focusedIndicatorLineThickness,
1454 unfocusedIndicatorWidth = unfocusedIndicatorLineThickness,
1455 )
1456 }
1457
inspectablePropertiesnull1458 override fun InspectorInfo.inspectableProperties() {
1459 name = "indicatorLine"
1460 properties["enabled"] = enabled
1461 properties["isError"] = isError
1462 properties["interactionSource"] = interactionSource
1463 properties["colors"] = colors
1464 properties["textFieldShape"] = textFieldShape
1465 properties["focusedIndicatorLineThickness"] = focusedIndicatorLineThickness
1466 properties["unfocusedIndicatorLineThickness"] = unfocusedIndicatorLineThickness
1467 }
1468 }
1469
1470 @OptIn(ExperimentalMaterial3ExpressiveApi::class)
1471 internal class IndicatorLineNode(
1472 private var enabled: Boolean,
1473 private var isError: Boolean,
1474 private var interactionSource: InteractionSource,
1475 colors: TextFieldColors?,
1476 textFieldShape: Shape?,
1477 private var focusedIndicatorWidth: Dp,
1478 private var unfocusedIndicatorWidth: Dp,
1479 ) : DelegatingNode(), CompositionLocalConsumerModifierNode {
1480 private var focused = false
1481 private var trackFocusStateJob: Job? = null
1482
1483 private var _colors: TextFieldColors? = colors
1484 private val colors: TextFieldColors
1485 get() =
1486 _colors
1487 ?: currentValueOf(LocalColorScheme)
1488 .defaultTextFieldColors(currentValueOf(LocalTextSelectionColors))
1489
1490 // Must be initialized in `onAttach` so `colors` can read from the `MaterialTheme`
1491 private var colorAnimatable: Animatable<Color, AnimationVector4D>? = null
1492
1493 private var _shape: Shape? = textFieldShape
1494 private set(value) {
1495 if (field != value) {
1496 field = value
1497 drawWithCacheModifierNode.invalidateDrawCache()
1498 }
1499 }
1500
1501 private val shape: Shape
1502 get() =
1503 _shape ?: currentValueOf(LocalShapes).fromToken(FilledTextFieldTokens.ContainerShape)
1504
1505 private val widthAnimatable: Animatable<Dp, AnimationVector1D> =
1506 Animatable(
1507 initialValue =
1508 if (focused && this.enabled) this.focusedIndicatorWidth
1509 else this.unfocusedIndicatorWidth,
1510 typeConverter = Dp.VectorConverter,
1511 )
1512
updatenull1513 fun update(
1514 enabled: Boolean,
1515 isError: Boolean,
1516 interactionSource: InteractionSource,
1517 colors: TextFieldColors?,
1518 textFieldShape: Shape?,
1519 focusedIndicatorWidth: Dp,
1520 unfocusedIndicatorWidth: Dp,
1521 ) {
1522 var shouldInvalidate = false
1523
1524 if (this.enabled != enabled) {
1525 this.enabled = enabled
1526 shouldInvalidate = true
1527 }
1528
1529 if (this.isError != isError) {
1530 this.isError = isError
1531 shouldInvalidate = true
1532 }
1533
1534 if (this.interactionSource !== interactionSource) {
1535 this.interactionSource = interactionSource
1536 trackFocusStateJob?.cancel()
1537 trackFocusStateJob = coroutineScope.launch { trackFocusState() }
1538 }
1539
1540 if (this._colors != colors) {
1541 this._colors = colors
1542 shouldInvalidate = true
1543 }
1544
1545 if (this._shape != textFieldShape) {
1546 this._shape = textFieldShape
1547 shouldInvalidate = true
1548 }
1549
1550 if (this.focusedIndicatorWidth != focusedIndicatorWidth) {
1551 this.focusedIndicatorWidth = focusedIndicatorWidth
1552 shouldInvalidate = true
1553 }
1554
1555 if (this.unfocusedIndicatorWidth != unfocusedIndicatorWidth) {
1556 this.unfocusedIndicatorWidth = unfocusedIndicatorWidth
1557 shouldInvalidate = true
1558 }
1559
1560 if (shouldInvalidate) {
1561 invalidateIndicator()
1562 }
1563 }
1564
1565 override val shouldAutoInvalidate: Boolean
1566 get() = false
1567
onAttachnull1568 override fun onAttach() {
1569 trackFocusStateJob = coroutineScope.launch { trackFocusState() }
1570 if (colorAnimatable == null) {
1571 val initialColor = colors.indicatorColor(enabled, isError, focused)
1572 colorAnimatable =
1573 Animatable(
1574 initialValue = initialColor,
1575 typeConverter = Color.VectorConverter(initialColor.colorSpace),
1576 )
1577 }
1578 }
1579
1580 /** Copied from [InteractionSource.collectIsFocusedAsState] */
trackFocusStatenull1581 private suspend fun trackFocusState() {
1582 focused = false
1583 val focusInteractions = mutableListOf<FocusInteraction.Focus>()
1584 interactionSource.interactions.collect { interaction ->
1585 when (interaction) {
1586 is FocusInteraction.Focus -> focusInteractions.add(interaction)
1587 is FocusInteraction.Unfocus -> focusInteractions.remove(interaction.focus)
1588 }
1589 val isFocused = focusInteractions.isNotEmpty()
1590 if (isFocused != focused) {
1591 focused = isFocused
1592 invalidateIndicator()
1593 }
1594 }
1595 }
1596
invalidateIndicatornull1597 private fun invalidateIndicator() {
1598 coroutineScope.launch {
1599 colorAnimatable?.animateTo(
1600 targetValue = colors.indicatorColor(enabled, isError, focused),
1601 animationSpec =
1602 if (enabled) {
1603 currentValueOf(LocalMotionScheme)
1604 .fromToken(MotionSchemeKeyTokens.FastEffects)
1605 } else {
1606 snap()
1607 },
1608 )
1609 }
1610 coroutineScope.launch {
1611 widthAnimatable.animateTo(
1612 targetValue =
1613 if (focused && enabled) focusedIndicatorWidth else unfocusedIndicatorWidth,
1614 animationSpec =
1615 if (enabled) {
1616 currentValueOf(LocalMotionScheme)
1617 .fromToken(MotionSchemeKeyTokens.FastSpatial)
1618 } else {
1619 snap()
1620 },
1621 )
1622 }
1623 }
1624
1625 private val drawWithCacheModifierNode =
1626 delegate(
<lambda>null1627 CacheDrawModifierNode {
1628 val strokeWidth = widthAnimatable.value.toPx()
1629 val textFieldShapePath =
1630 Path().apply {
1631 addOutline(
1632 this@IndicatorLineNode.shape.createOutline(
1633 size,
1634 layoutDirection,
1635 density = this@CacheDrawModifierNode
1636 )
1637 )
1638 }
1639 val linePath =
1640 Path().apply {
1641 addRect(
1642 Rect(
1643 left = 0f,
1644 top = size.height - strokeWidth,
1645 right = size.width,
1646 bottom = size.height,
1647 )
1648 )
1649 }
1650 val clippedLine = linePath and textFieldShapePath
1651
1652 onDrawWithContent {
1653 drawContent()
1654 drawPath(
1655 path = clippedLine,
1656 brush = SolidColor(colorAnimatable!!.value),
1657 )
1658 }
1659 }
1660 )
1661 }
1662
1663 /** Padding from text field top to label top, and from input field bottom to text field bottom */
1664 /*@VisibleForTesting*/
1665 internal val TextFieldWithLabelVerticalPadding = 8.dp
1666