1 /*
<lambda>null2 * Copyright 2020 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.material
18
19 import androidx.compose.foundation.BorderStroke
20 import androidx.compose.foundation.ScrollState
21 import androidx.compose.foundation.interaction.Interaction
22 import androidx.compose.foundation.interaction.MutableInteractionSource
23 import androidx.compose.foundation.layout.Box
24 import androidx.compose.foundation.layout.PaddingValues
25 import androidx.compose.foundation.layout.calculateEndPadding
26 import androidx.compose.foundation.layout.calculateStartPadding
27 import androidx.compose.foundation.layout.defaultMinSize
28 import androidx.compose.foundation.layout.padding
29 import androidx.compose.foundation.rememberScrollState
30 import androidx.compose.foundation.shape.ZeroCornerSize
31 import androidx.compose.foundation.text.BasicTextField
32 import androidx.compose.foundation.text.KeyboardActions
33 import androidx.compose.foundation.text.KeyboardOptions
34 import androidx.compose.foundation.text.input.InputTransformation
35 import androidx.compose.foundation.text.input.KeyboardActionHandler
36 import androidx.compose.foundation.text.input.OutputTransformation
37 import androidx.compose.foundation.text.input.TextFieldBuffer
38 import androidx.compose.foundation.text.input.TextFieldLineLimits
39 import androidx.compose.foundation.text.input.TextFieldLineLimits.MultiLine
40 import androidx.compose.foundation.text.input.TextFieldLineLimits.SingleLine
41 import androidx.compose.foundation.text.input.TextFieldState
42 import androidx.compose.material.TextFieldDefaults.indicatorLine
43 import androidx.compose.material.internal.subtractConstraintSafely
44 import androidx.compose.runtime.Composable
45 import androidx.compose.runtime.remember
46 import androidx.compose.ui.Alignment
47 import androidx.compose.ui.Modifier
48 import androidx.compose.ui.draw.drawWithContent
49 import androidx.compose.ui.geometry.Offset
50 import androidx.compose.ui.graphics.Shape
51 import androidx.compose.ui.graphics.SolidColor
52 import androidx.compose.ui.graphics.takeOrElse
53 import androidx.compose.ui.layout.AlignmentLine
54 import androidx.compose.ui.layout.IntrinsicMeasurable
55 import androidx.compose.ui.layout.IntrinsicMeasureScope
56 import androidx.compose.ui.layout.LastBaseline
57 import androidx.compose.ui.layout.Layout
58 import androidx.compose.ui.layout.Measurable
59 import androidx.compose.ui.layout.MeasurePolicy
60 import androidx.compose.ui.layout.MeasureResult
61 import androidx.compose.ui.layout.MeasureScope
62 import androidx.compose.ui.layout.Placeable
63 import androidx.compose.ui.layout.layoutId
64 import androidx.compose.ui.platform.LocalLayoutDirection
65 import androidx.compose.ui.text.TextStyle
66 import androidx.compose.ui.text.input.ImeAction
67 import androidx.compose.ui.text.input.KeyboardType
68 import androidx.compose.ui.text.input.TextFieldValue
69 import androidx.compose.ui.text.input.VisualTransformation
70 import androidx.compose.ui.unit.Constraints
71 import androidx.compose.ui.unit.Dp
72 import androidx.compose.ui.unit.coerceAtLeast
73 import androidx.compose.ui.unit.constrainHeight
74 import androidx.compose.ui.unit.constrainWidth
75 import androidx.compose.ui.unit.dp
76 import androidx.compose.ui.unit.offset
77 import androidx.compose.ui.util.fastFirst
78 import androidx.compose.ui.util.fastFirstOrNull
79 import kotlin.math.max
80 import kotlin.math.roundToInt
81
82 /**
83 * [Material Design filled text
84 * field](https://m2.material.io/components/text-fields#filled-text-field).
85 *
86 * Filled text fields have more visual emphasis than outlined text fields, making them stand out
87 * when surrounded by other content and components.
88 *
89 * 
91 *
92 * If you are looking for an outlined version, see [OutlinedTextField].
93 *
94 * This overload of [TextField] uses [TextFieldState] to keep track of its text content and position
95 * of the cursor or selection.
96 *
97 * A simple single line text field looks like:
98 *
99 * @sample androidx.compose.material.samples.SimpleTextFieldSample
100 *
101 * You can control the initial text input and selection:
102 *
103 * @sample androidx.compose.material.samples.TextFieldWithInitialValueAndSelection
104 *
105 * You may provide a placeholder:
106 *
107 * @sample androidx.compose.material.samples.TextFieldWithPlaceholder
108 *
109 * You can also provide leading and trailing icons:
110 *
111 * @sample androidx.compose.material.samples.TextFieldWithIcons
112 *
113 * To handle the error input state, use [isError] parameter:
114 *
115 * @sample androidx.compose.material.samples.TextFieldWithErrorState
116 *
117 * Additionally, you may provide additional message at the bottom:
118 *
119 * @sample androidx.compose.material.samples.TextFieldWithHelperMessage
120 *
121 * Hiding a software keyboard on IME action performed:
122 *
123 * @sample androidx.compose.material.samples.TextFieldWithHideKeyboardOnImeAction
124 * @param state [TextFieldState] object that holds the internal editing state of this text field.
125 * @param modifier a [Modifier] for this text field
126 * @param enabled controls the enabled state of the [TextField]. When `false`, the text field will
127 * be neither editable nor focusable, the input of the text field will not be selectable, visually
128 * text field will appear in the disabled UI state
129 * @param readOnly controls the editable state of the [TextField]. When `true`, the text field can
130 * not be modified, however, a user can focus it and copy text from it. Read-only text fields are
131 * usually used to display pre-filled forms that user can not edit
132 * @param textStyle the style to be applied to the input text. The default [textStyle] uses the
133 * [LocalTextStyle] defined by the theme
134 * @param label the optional label to be displayed inside the text field container. The default text
135 * style for internal [Text] is [Typography.caption] when the text field is in focus and
136 * [Typography.subtitle1] when the text field is not in focus
137 * @param placeholder the optional placeholder to be displayed when the text field is in focus and
138 * the input text is empty. The default text style for internal [Text] is [Typography.subtitle1]
139 * @param leadingIcon the optional leading icon to be displayed at the beginning of the text field
140 * container
141 * @param trailingIcon the optional trailing icon to be displayed at the end of the text field
142 * container
143 * @param isError indicates if the text field's current value is in error. If set to true, the
144 * label, bottom indicator and trailing icon by default will be displayed in error color
145 * @param inputTransformation Optional [InputTransformation] that will be used to transform changes
146 * to the [TextFieldState] made by the user. The transformation will be applied to changes made by
147 * hardware and software keyboard events, pasting or dropping text, accessibility services, and
148 * tests. The transformation will _not_ be applied when changing the [state] programmatically, or
149 * when the transformation is changed. If the transformation is changed on an existing text field,
150 * it will be applied to the next user edit. the transformation will not immediately affect the
151 * current [state].
152 * @param outputTransformation An [OutputTransformation] that transforms how the contents of the
153 * text field are presented.
154 * @param keyboardOptions software keyboard options that contains configuration such as
155 * [KeyboardType] and [ImeAction].
156 * @param onKeyboardAction Called when the user presses the action button in the input method editor
157 * (IME), or by pressing the enter key on a hardware keyboard. By default this parameter is null,
158 * and would execute the default behavior for a received IME Action e.g., [ImeAction.Done] would
159 * close the keyboard, [ImeAction.Next] would switch the focus to the next focusable item on the
160 * screen.
161 * @param lineLimits Whether the text field should be [SingleLine], scroll horizontally, and ignore
162 * newlines; or [MultiLine] and grow and scroll vertically. If [SingleLine] is passed, all newline
163 * characters ('\n') within the text will be replaced with regular whitespace (' '), ensuring that
164 * the contents of the text field are presented in a single line.
165 * @param scrollState Scroll state that manages either horizontal or vertical scroll of the text
166 * field. If [lineLimits] is [SingleLine], this text field is treated as single line with
167 * horizontal scroll behavior. In other cases the text field becomes vertically scrollable.
168 * @param shape the shape of the text field's container
169 * @param colors [TextFieldColors] that will be used to resolve color of the text, content
170 * (including label, placeholder, leading and trailing icons, indicator line) and background for
171 * this text field in different states. See [TextFieldDefaults.textFieldColors]
172 * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
173 * emitting [Interaction]s for this text field. You can use this to change the text field's
174 * appearance or preview the text field in different states. Note that if `null` is provided,
175 * interactions will still happen internally.
176 */
177 @Composable
178 fun TextField(
179 state: TextFieldState,
180 modifier: Modifier = Modifier,
181 enabled: Boolean = true,
182 readOnly: Boolean = false,
183 textStyle: TextStyle = LocalTextStyle.current,
184 label: @Composable (() -> Unit)? = null,
185 placeholder: @Composable (() -> Unit)? = null,
186 leadingIcon: @Composable (() -> Unit)? = null,
187 trailingIcon: @Composable (() -> Unit)? = null,
188 isError: Boolean = false,
189 inputTransformation: InputTransformation? = null,
190 outputTransformation: OutputTransformation? = null,
191 keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
192 onKeyboardAction: KeyboardActionHandler? = null,
193 lineLimits: TextFieldLineLimits = TextFieldLineLimits.Default,
194 scrollState: ScrollState = rememberScrollState(),
195 shape: Shape = TextFieldDefaults.TextFieldShape,
196 colors: TextFieldColors = TextFieldDefaults.textFieldColors(),
197 interactionSource: MutableInteractionSource? = null,
198 ) {
199 @Suppress("NAME_SHADOWING")
200 val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
201 // If color is not provided via the text style, use content color as a default
202 val textColor = textStyle.color.takeOrElse { colors.textColor(enabled).value }
203 val mergedTextStyle = textStyle.merge(TextStyle(color = textColor))
204
205 BasicTextField(
206 state = state,
207 modifier =
208 modifier
209 .indicatorLine(enabled, isError, interactionSource, colors)
210 .defaultErrorSemantics(isError, getString(Strings.DefaultErrorMessage))
211 .defaultMinSize(
212 minWidth = TextFieldDefaults.MinWidth,
213 minHeight = TextFieldDefaults.MinHeight
214 ),
215 enabled = enabled,
216 readOnly = readOnly,
217 textStyle = mergedTextStyle,
218 cursorBrush = SolidColor(colors.cursorColor(isError).value),
219 inputTransformation = inputTransformation,
220 outputTransformation = outputTransformation,
221 keyboardOptions = keyboardOptions,
222 onKeyboardAction = onKeyboardAction,
223 interactionSource = interactionSource,
224 scrollState = scrollState,
225 lineLimits = lineLimits,
226 decorator = { innerTextField ->
227 val textPostTransformation =
228 if (outputTransformation == null) {
229 state.text.toString()
230 } else {
231 // TODO: use constructor to create TextFieldBuffer from TextFieldState when
232 // available
233 lateinit var buffer: TextFieldBuffer
234 state.edit { buffer = this }
235 // after edit completes, mutations on buffer are ineffective
236 with(outputTransformation) { buffer.transformOutput() }
237 buffer.asCharSequence().toString()
238 }
239
240 TextFieldDefaults.TextFieldDecorationBox(
241 value = textPostTransformation,
242 visualTransformation = VisualTransformation.None,
243 innerTextField = innerTextField,
244 placeholder = placeholder,
245 label = label,
246 leadingIcon = leadingIcon,
247 trailingIcon = trailingIcon,
248 singleLine = lineLimits == SingleLine,
249 enabled = enabled,
250 isError = isError,
251 interactionSource = interactionSource,
252 shape = shape,
253 colors = colors,
254 )
255 }
256 )
257 }
258
259 /**
260 * [Material Design filled text
261 * field](https://m2.material.io/components/text-fields#filled-text-field).
262 *
263 * Filled text fields have more visual emphasis than outlined text fields, making them stand out
264 * when surrounded by other content and components.
265 *
266 * 
268 *
269 * If you are looking for an outlined version, see [OutlinedTextField].
270 *
271 * If apart from input text change you also want to observe the cursor location, selection range, or
272 * IME composition use the TextField overload with the [TextFieldValue] parameter instead.
273 *
274 * @param value the input text to be shown in the text field
275 * @param onValueChange the callback that is triggered when the input service updates the text. An
276 * updated text comes as a parameter of the callback
277 * @param modifier a [Modifier] for this text field
278 * @param enabled controls the enabled state of the [TextField]. When `false`, the text field will
279 * be neither editable nor focusable, the input of the text field will not be selectable, visually
280 * text field will appear in the disabled UI state
281 * @param readOnly controls the editable state of the [TextField]. When `true`, the text field can
282 * not be modified, however, a user can focus it and copy text from it. Read-only text fields are
283 * usually used to display pre-filled forms that user can not edit
284 * @param textStyle the style to be applied to the input text. The default [textStyle] uses the
285 * [LocalTextStyle] defined by the theme
286 * @param label the optional label to be displayed inside the text field container. The default text
287 * style for internal [Text] is [Typography.caption] when the text field is in focus and
288 * [Typography.subtitle1] when the text field is not in focus
289 * @param placeholder the optional placeholder to be displayed when the text field is in focus and
290 * the input text is empty. The default text style for internal [Text] is [Typography.subtitle1]
291 * @param leadingIcon the optional leading icon to be displayed at the beginning of the text field
292 * container
293 * @param trailingIcon the optional trailing icon to be displayed at the end of the text field
294 * container
295 * @param isError indicates if the text field's current value is in error. If set to true, the
296 * label, bottom indicator and trailing icon by default will be displayed in error color
297 * @param visualTransformation transforms the visual representation of the input [value] For
298 * example, you can use
299 * [PasswordVisualTransformation][androidx.compose.ui.text.input.PasswordVisualTransformation] to
300 * create a password text field. By default no visual transformation is applied
301 * @param keyboardOptions software keyboard options that contains configuration such as
302 * [KeyboardType] and [ImeAction].
303 * @param keyboardActions when the input service emits an IME action, the corresponding callback is
304 * called. Note that this IME action may be different from what you specified in
305 * [KeyboardOptions.imeAction].
306 * @param singleLine when set to true, this text field becomes a single horizontally scrolling text
307 * field instead of wrapping onto multiple lines. The keyboard will be informed to not show the
308 * return key as the [ImeAction]. Note that [maxLines] parameter will be ignored as the maxLines
309 * attribute will be automatically set to 1.
310 * @param maxLines the maximum height in terms of maximum number of visible lines. It is required
311 * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true.
312 * @param minLines the minimum height in terms of minimum number of visible lines. It is required
313 * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true.
314 * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
315 * emitting [Interaction]s for this text field. You can use this to change the text field's
316 * appearance or preview the text field in different states. Note that if `null` is provided,
317 * interactions will still happen internally.
318 * @param shape the shape of the text field's container
319 * @param colors [TextFieldColors] that will be used to resolve color of the text, content
320 * (including label, placeholder, leading and trailing icons, indicator line) and background for
321 * this text field in different states. See [TextFieldDefaults.textFieldColors]
322 */
323 @Composable
TextFieldnull324 fun TextField(
325 value: String,
326 onValueChange: (String) -> Unit,
327 modifier: Modifier = Modifier,
328 enabled: Boolean = true,
329 readOnly: Boolean = false,
330 textStyle: TextStyle = LocalTextStyle.current,
331 label: @Composable (() -> Unit)? = null,
332 placeholder: @Composable (() -> Unit)? = null,
333 leadingIcon: @Composable (() -> Unit)? = null,
334 trailingIcon: @Composable (() -> Unit)? = null,
335 isError: Boolean = false,
336 visualTransformation: VisualTransformation = VisualTransformation.None,
337 keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
338 keyboardActions: KeyboardActions = KeyboardActions(),
339 singleLine: Boolean = false,
340 maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
341 minLines: Int = 1,
342 interactionSource: MutableInteractionSource? = null,
343 shape: Shape = TextFieldDefaults.TextFieldShape,
344 colors: TextFieldColors = TextFieldDefaults.textFieldColors()
345 ) {
346 @Suppress("NAME_SHADOWING")
347 val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
348 // If color is not provided via the text style, use content color as a default
349 val textColor = textStyle.color.takeOrElse { colors.textColor(enabled).value }
350 val mergedTextStyle = textStyle.merge(TextStyle(color = textColor))
351
352 BasicTextField(
353 value = value,
354 modifier =
355 modifier
356 .indicatorLine(enabled, isError, interactionSource, colors)
357 .defaultErrorSemantics(isError, getString(Strings.DefaultErrorMessage))
358 .defaultMinSize(
359 minWidth = TextFieldDefaults.MinWidth,
360 minHeight = TextFieldDefaults.MinHeight
361 ),
362 onValueChange = onValueChange,
363 enabled = enabled,
364 readOnly = readOnly,
365 textStyle = mergedTextStyle,
366 cursorBrush = SolidColor(colors.cursorColor(isError).value),
367 visualTransformation = visualTransformation,
368 keyboardOptions = keyboardOptions,
369 keyboardActions = keyboardActions,
370 interactionSource = interactionSource,
371 singleLine = singleLine,
372 maxLines = maxLines,
373 minLines = minLines,
374 decorationBox =
375 @Composable { innerTextField ->
376 // places leading icon, text field with label and placeholder, trailing icon
377 TextFieldDefaults.TextFieldDecorationBox(
378 value = value,
379 visualTransformation = visualTransformation,
380 innerTextField = innerTextField,
381 placeholder = placeholder,
382 label = label,
383 leadingIcon = leadingIcon,
384 trailingIcon = trailingIcon,
385 singleLine = singleLine,
386 enabled = enabled,
387 isError = isError,
388 interactionSource = interactionSource,
389 shape = shape,
390 colors = colors,
391 )
392 }
393 )
394 }
395
396 @Deprecated(
397 "Maintained for binary compatibility. Use version with minLines instead",
398 level = DeprecationLevel.HIDDEN
399 )
400 @Composable
TextFieldnull401 fun TextField(
402 value: String,
403 onValueChange: (String) -> Unit,
404 modifier: Modifier = Modifier,
405 enabled: Boolean = true,
406 readOnly: Boolean = false,
407 textStyle: TextStyle = LocalTextStyle.current,
408 label: @Composable (() -> Unit)? = null,
409 placeholder: @Composable (() -> Unit)? = null,
410 leadingIcon: @Composable (() -> Unit)? = null,
411 trailingIcon: @Composable (() -> Unit)? = null,
412 isError: Boolean = false,
413 visualTransformation: VisualTransformation = VisualTransformation.None,
414 keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
415 keyboardActions: KeyboardActions = KeyboardActions(),
416 singleLine: Boolean = false,
417 maxLines: Int = Int.MAX_VALUE,
418 interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
419 shape: Shape =
420 MaterialTheme.shapes.small.copy(bottomEnd = ZeroCornerSize, bottomStart = ZeroCornerSize),
421 colors: TextFieldColors = TextFieldDefaults.textFieldColors()
422 ) {
423 TextField(
424 value,
425 onValueChange,
426 modifier,
427 enabled,
428 readOnly,
429 textStyle,
430 label,
431 placeholder,
432 leadingIcon,
433 trailingIcon,
434 isError,
435 visualTransformation,
436 keyboardOptions,
437 keyboardActions,
438 singleLine,
439 maxLines,
440 1,
441 interactionSource,
442 shape,
443 colors
444 )
445 }
446
447 /**
448 * [Material Design filled text
449 * field](https://m2.material.io/components/text-fields#filled-text-field).
450 *
451 * Filled text fields have more visual emphasis than outlined text fields, making them stand out
452 * when surrounded by other content and components.
453 *
454 * 
456 *
457 * If you are looking for an outlined version, see [OutlinedTextField]. For a text field
458 * specifically designed for passwords or other secure content, see [SecureTextField].
459 *
460 * This overload provides access to the input text, cursor position, selection range and IME
461 * composition. If you only want to observe an input text change, use the TextField overload with
462 * the [String] parameter instead.
463 *
464 * @param value the input [TextFieldValue] to be shown in the text field
465 * @param onValueChange the callback that is triggered when the input service updates values in
466 * [TextFieldValue]. An updated [TextFieldValue] comes as a parameter of the callback
467 * @param modifier a [Modifier] for this text field
468 * @param enabled controls the enabled state of the [TextField]. When `false`, the text field will
469 * be neither editable nor focusable, the input of the text field will not be selectable, visually
470 * text field will appear in the disabled UI state
471 * @param readOnly controls the editable state of the [TextField]. When `true`, the text field can
472 * not be modified, however, a user can focus it and copy text from it. Read-only text fields are
473 * usually used to display pre-filled forms that user can not edit
474 * @param textStyle the style to be applied to the input text. The default [textStyle] uses the
475 * [LocalTextStyle] defined by the theme
476 * @param label the optional label to be displayed inside the text field container. The default text
477 * style for internal [Text] is [Typography.caption] when the text field is in focus and
478 * [Typography.subtitle1] when the text field is not in focus
479 * @param placeholder the optional placeholder to be displayed when the text field is in focus and
480 * the input text is empty. The default text style for internal [Text] is [Typography.subtitle1]
481 * @param leadingIcon the optional leading icon to be displayed at the beginning of the text field
482 * container
483 * @param trailingIcon the optional trailing icon to be displayed at the end of the text field
484 * container
485 * @param isError indicates if the text field's current value is in error state. If set to true, the
486 * label, bottom indicator and trailing icon by default will be displayed in error color
487 * @param visualTransformation transforms the visual representation of the input [value]. For
488 * example, you can use
489 * [PasswordVisualTransformation][androidx.compose.ui.text.input.PasswordVisualTransformation] to
490 * create a password text field. By default no visual transformation is applied
491 * @param keyboardOptions software keyboard options that contains configuration such as
492 * [KeyboardType] and [ImeAction].
493 * @param keyboardActions when the input service emits an IME action, the corresponding callback is
494 * called. Note that this IME action may be different from what you specified in
495 * [KeyboardOptions.imeAction].
496 * @param singleLine when set to true, this text field becomes a single horizontally scrolling text
497 * field instead of wrapping onto multiple lines. The keyboard will be informed to not show the
498 * return key as the [ImeAction]. Note that [maxLines] parameter will be ignored as the maxLines
499 * attribute will be automatically set to 1.
500 * @param maxLines the maximum height in terms of maximum number of visible lines. It is required
501 * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true.
502 * @param minLines the minimum height in terms of minimum number of visible lines. It is required
503 * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true.
504 * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
505 * emitting [Interaction]s for this text field. You can use this to change the text field's
506 * appearance or preview the text field in different states. Note that if `null` is provided,
507 * interactions will still happen internally.
508 * @param shape the shape of the text field's container
509 * @param colors [TextFieldColors] that will be used to resolve color of the text, content
510 * (including label, placeholder, leading and trailing icons, indicator line) and background for
511 * this text field in different states. See [TextFieldDefaults.textFieldColors]
512 */
513 @Composable
TextFieldnull514 fun TextField(
515 value: TextFieldValue,
516 onValueChange: (TextFieldValue) -> Unit,
517 modifier: Modifier = Modifier,
518 enabled: Boolean = true,
519 readOnly: Boolean = false,
520 textStyle: TextStyle = LocalTextStyle.current,
521 label: @Composable (() -> Unit)? = null,
522 placeholder: @Composable (() -> Unit)? = null,
523 leadingIcon: @Composable (() -> Unit)? = null,
524 trailingIcon: @Composable (() -> Unit)? = null,
525 isError: Boolean = false,
526 visualTransformation: VisualTransformation = VisualTransformation.None,
527 keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
528 keyboardActions: KeyboardActions = KeyboardActions(),
529 singleLine: Boolean = false,
530 maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
531 minLines: Int = 1,
532 interactionSource: MutableInteractionSource? = null,
533 shape: Shape = TextFieldDefaults.TextFieldShape,
534 colors: TextFieldColors = TextFieldDefaults.textFieldColors()
535 ) {
536 @Suppress("NAME_SHADOWING")
537 val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
538 // If color is not provided via the text style, use content color as a default
539 val textColor = textStyle.color.takeOrElse { colors.textColor(enabled).value }
540 val mergedTextStyle = textStyle.merge(TextStyle(color = textColor))
541
542 BasicTextField(
543 value = value,
544 modifier =
545 modifier
546 .indicatorLine(enabled, isError, interactionSource, colors)
547 .defaultErrorSemantics(isError, getString(Strings.DefaultErrorMessage))
548 .defaultMinSize(
549 minWidth = TextFieldDefaults.MinWidth,
550 minHeight = TextFieldDefaults.MinHeight
551 ),
552 onValueChange = onValueChange,
553 enabled = enabled,
554 readOnly = readOnly,
555 textStyle = mergedTextStyle,
556 cursorBrush = SolidColor(colors.cursorColor(isError).value),
557 visualTransformation = visualTransformation,
558 keyboardOptions = keyboardOptions,
559 keyboardActions = keyboardActions,
560 interactionSource = interactionSource,
561 singleLine = singleLine,
562 maxLines = maxLines,
563 minLines = minLines,
564 decorationBox =
565 @Composable { innerTextField ->
566 // places leading icon, text field with label and placeholder, trailing icon
567 TextFieldDefaults.TextFieldDecorationBox(
568 value = value.text,
569 visualTransformation = visualTransformation,
570 innerTextField = innerTextField,
571 placeholder = placeholder,
572 label = label,
573 leadingIcon = leadingIcon,
574 trailingIcon = trailingIcon,
575 singleLine = singleLine,
576 enabled = enabled,
577 isError = isError,
578 interactionSource = interactionSource,
579 shape = shape,
580 colors = colors,
581 )
582 }
583 )
584 }
585
586 @Deprecated(
587 "Maintained for binary compatibility. Use version with minLines instead",
588 level = DeprecationLevel.HIDDEN
589 )
590 @Composable
TextFieldnull591 fun TextField(
592 value: TextFieldValue,
593 onValueChange: (TextFieldValue) -> Unit,
594 modifier: Modifier = Modifier,
595 enabled: Boolean = true,
596 readOnly: Boolean = false,
597 textStyle: TextStyle = LocalTextStyle.current,
598 label: @Composable (() -> Unit)? = null,
599 placeholder: @Composable (() -> Unit)? = null,
600 leadingIcon: @Composable (() -> Unit)? = null,
601 trailingIcon: @Composable (() -> Unit)? = null,
602 isError: Boolean = false,
603 visualTransformation: VisualTransformation = VisualTransformation.None,
604 keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
605 keyboardActions: KeyboardActions = KeyboardActions(),
606 singleLine: Boolean = false,
607 maxLines: Int = Int.MAX_VALUE,
608 interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
609 shape: Shape = TextFieldDefaults.TextFieldShape,
610 colors: TextFieldColors = TextFieldDefaults.textFieldColors()
611 ) {
612 TextField(
613 value,
614 onValueChange,
615 modifier,
616 enabled,
617 readOnly,
618 textStyle,
619 label,
620 placeholder,
621 leadingIcon,
622 trailingIcon,
623 isError,
624 visualTransformation,
625 keyboardOptions,
626 keyboardActions,
627 singleLine,
628 maxLines,
629 1,
630 interactionSource,
631 shape,
632 colors
633 )
634 }
635
636 /**
637 * Composable responsible for measuring and laying out leading and trailing icons, label,
638 * placeholder and the input field.
639 */
640 @Composable
641 internal fun TextFieldLayout(
642 modifier: Modifier,
643 textField: @Composable () -> Unit,
644 label: @Composable (() -> Unit)?,
645 placeholder: @Composable ((Modifier) -> Unit)?,
646 leading: @Composable (() -> Unit)?,
647 trailing: @Composable (() -> Unit)?,
648 singleLine: Boolean,
649 animationProgress: Float,
650 paddingValues: PaddingValues
651 ) {
652 val measurePolicy =
<lambda>null653 remember(singleLine, animationProgress, paddingValues) {
654 TextFieldMeasurePolicy(singleLine, animationProgress, paddingValues)
655 }
656 val layoutDirection = LocalLayoutDirection.current
657 Layout(
658 modifier = modifier,
<lambda>null659 content = {
660 if (leading != null) {
661 Box(
662 modifier = Modifier.layoutId(LeadingId).minimumInteractiveComponentSize(),
663 contentAlignment = Alignment.Center
664 ) {
665 leading()
666 }
667 }
668 if (trailing != null) {
669 Box(
670 modifier = Modifier.layoutId(TrailingId).minimumInteractiveComponentSize(),
671 contentAlignment = Alignment.Center
672 ) {
673 trailing()
674 }
675 }
676
677 val startTextFieldPadding = paddingValues.calculateStartPadding(layoutDirection)
678 val endTextFieldPadding = paddingValues.calculateEndPadding(layoutDirection)
679 val padding =
680 Modifier.padding(
681 start =
682 if (leading != null) {
683 (startTextFieldPadding - HorizontalIconPadding).coerceAtLeast(0.dp)
684 } else {
685 startTextFieldPadding
686 },
687 end =
688 if (trailing != null) {
689 (endTextFieldPadding - HorizontalIconPadding).coerceAtLeast(0.dp)
690 } else {
691 endTextFieldPadding
692 }
693 )
694 if (placeholder != null) {
695 placeholder(Modifier.layoutId(PlaceholderId).then(padding))
696 }
697 if (label != null) {
698 Box(Modifier.layoutId(LabelId).then(padding)) { label() }
699 }
700 Box(
701 modifier = Modifier.layoutId(TextFieldId).then(padding),
702 propagateMinConstraints = true,
703 ) {
704 textField()
705 }
706 },
707 measurePolicy = measurePolicy
708 )
709 }
710
711 private class TextFieldMeasurePolicy(
712 private val singleLine: Boolean,
713 private val animationProgress: Float,
714 private val paddingValues: PaddingValues
715 ) : MeasurePolicy {
measurenull716 override fun MeasureScope.measure(
717 measurables: List<Measurable>,
718 constraints: Constraints
719 ): MeasureResult {
720 val topPaddingValue = paddingValues.calculateTopPadding().roundToPx()
721 val bottomPaddingValue = paddingValues.calculateBottomPadding().roundToPx()
722
723 // padding between label and input text
724 val topPadding = TextFieldTopPadding.roundToPx()
725 var occupiedSpaceHorizontally = 0
726
727 // measure leading icon
728 val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)
729 val leadingPlaceable =
730 measurables.fastFirstOrNull { it.layoutId == LeadingId }?.measure(looseConstraints)
731 occupiedSpaceHorizontally += widthOrZero(leadingPlaceable)
732
733 // measure trailing icon
734 val trailingPlaceable =
735 measurables
736 .fastFirstOrNull { it.layoutId == TrailingId }
737 ?.measure(looseConstraints.offset(horizontal = -occupiedSpaceHorizontally))
738 occupiedSpaceHorizontally += widthOrZero(trailingPlaceable)
739
740 // measure label
741 val labelConstraints =
742 looseConstraints.offset(
743 vertical = -bottomPaddingValue,
744 horizontal = -occupiedSpaceHorizontally
745 )
746 val labelPlaceable =
747 measurables.fastFirstOrNull { it.layoutId == LabelId }?.measure(labelConstraints)
748 val lastBaseline =
749 labelPlaceable?.get(LastBaseline)?.let {
750 if (it != AlignmentLine.Unspecified) it else labelPlaceable.height
751 } ?: 0
752 val effectiveLabelBaseline = max(lastBaseline, topPaddingValue)
753
754 // measure input field
755 // input field is laid out differently depending on whether the label is present or not
756 val verticalConstraintOffset =
757 if (labelPlaceable != null) {
758 -bottomPaddingValue - topPadding - effectiveLabelBaseline
759 } else {
760 -topPaddingValue - bottomPaddingValue
761 }
762 val textFieldConstraints =
763 constraints
764 .copy(minHeight = 0)
765 .offset(
766 vertical = verticalConstraintOffset,
767 horizontal = -occupiedSpaceHorizontally
768 )
769 val textFieldPlaceable =
770 measurables.fastFirst { it.layoutId == TextFieldId }.measure(textFieldConstraints)
771
772 // measure placeholder
773 val placeholderConstraints = textFieldConstraints.copy(minWidth = 0)
774 val placeholderPlaceable =
775 measurables
776 .fastFirstOrNull { it.layoutId == PlaceholderId }
777 ?.measure(placeholderConstraints)
778
779 val width =
780 calculateWidth(
781 widthOrZero(leadingPlaceable),
782 widthOrZero(trailingPlaceable),
783 textFieldPlaceable.width,
784 widthOrZero(labelPlaceable),
785 widthOrZero(placeholderPlaceable),
786 constraints
787 )
788 val height =
789 calculateHeight(
790 textFieldPlaceable.height,
791 labelPlaceable != null,
792 effectiveLabelBaseline,
793 heightOrZero(leadingPlaceable),
794 heightOrZero(trailingPlaceable),
795 heightOrZero(placeholderPlaceable),
796 constraints,
797 density,
798 paddingValues
799 )
800
801 return layout(width, height) {
802 if (labelPlaceable != null) {
803 // label's final position is always relative to the baseline
804 val labelEndPosition = (topPaddingValue - lastBaseline).coerceAtLeast(0)
805 placeWithLabel(
806 width,
807 height,
808 textFieldPlaceable,
809 labelPlaceable,
810 placeholderPlaceable,
811 leadingPlaceable,
812 trailingPlaceable,
813 singleLine,
814 labelEndPosition,
815 effectiveLabelBaseline + topPadding,
816 animationProgress,
817 density
818 )
819 } else {
820 placeWithoutLabel(
821 width,
822 height,
823 textFieldPlaceable,
824 placeholderPlaceable,
825 leadingPlaceable,
826 trailingPlaceable,
827 singleLine,
828 density,
829 paddingValues
830 )
831 }
832 }
833 }
834
maxIntrinsicHeightnull835 override fun IntrinsicMeasureScope.maxIntrinsicHeight(
836 measurables: List<IntrinsicMeasurable>,
837 width: Int
838 ): Int {
839 return intrinsicHeight(measurables, width) { intrinsicMeasurable, w ->
840 intrinsicMeasurable.maxIntrinsicHeight(w)
841 }
842 }
843
minIntrinsicHeightnull844 override fun IntrinsicMeasureScope.minIntrinsicHeight(
845 measurables: List<IntrinsicMeasurable>,
846 width: Int
847 ): Int {
848 return intrinsicHeight(measurables, width) { intrinsicMeasurable, w ->
849 intrinsicMeasurable.minIntrinsicHeight(w)
850 }
851 }
852
maxIntrinsicWidthnull853 override fun IntrinsicMeasureScope.maxIntrinsicWidth(
854 measurables: List<IntrinsicMeasurable>,
855 height: Int
856 ): Int {
857 return intrinsicWidth(measurables, height) { intrinsicMeasurable, h ->
858 intrinsicMeasurable.maxIntrinsicWidth(h)
859 }
860 }
861
minIntrinsicWidthnull862 override fun IntrinsicMeasureScope.minIntrinsicWidth(
863 measurables: List<IntrinsicMeasurable>,
864 height: Int
865 ): Int {
866 return intrinsicWidth(measurables, height) { intrinsicMeasurable, h ->
867 intrinsicMeasurable.minIntrinsicWidth(h)
868 }
869 }
870
intrinsicWidthnull871 private fun intrinsicWidth(
872 measurables: List<IntrinsicMeasurable>,
873 height: Int,
874 intrinsicMeasurer: (IntrinsicMeasurable, Int) -> Int
875 ): Int {
876 val textFieldWidth =
877 intrinsicMeasurer(measurables.fastFirst { it.layoutId == TextFieldId }, height)
878 val labelWidth =
879 measurables
880 .fastFirstOrNull { it.layoutId == LabelId }
881 ?.let { intrinsicMeasurer(it, height) } ?: 0
882 val trailingWidth =
883 measurables
884 .fastFirstOrNull { it.layoutId == TrailingId }
885 ?.let { intrinsicMeasurer(it, height) } ?: 0
886 val leadingWidth =
887 measurables
888 .fastFirstOrNull { it.layoutId == LeadingId }
889 ?.let { intrinsicMeasurer(it, height) } ?: 0
890 val placeholderWidth =
891 measurables
892 .fastFirstOrNull { it.layoutId == PlaceholderId }
893 ?.let { intrinsicMeasurer(it, height) } ?: 0
894 return calculateWidth(
895 leadingWidth = leadingWidth,
896 trailingWidth = trailingWidth,
897 textFieldWidth = textFieldWidth,
898 labelWidth = labelWidth,
899 placeholderWidth = placeholderWidth,
900 constraints = Constraints()
901 )
902 }
903
intrinsicHeightnull904 private fun IntrinsicMeasureScope.intrinsicHeight(
905 measurables: List<IntrinsicMeasurable>,
906 width: Int,
907 intrinsicMeasurer: (IntrinsicMeasurable, Int) -> Int
908 ): Int {
909 var remainingWidth = width
910 val leadingHeight =
911 measurables
912 .fastFirstOrNull { it.layoutId == LeadingId }
913 ?.let {
914 remainingWidth =
915 remainingWidth.subtractConstraintSafely(
916 it.maxIntrinsicWidth(Constraints.Infinity)
917 )
918 intrinsicMeasurer(it, width)
919 } ?: 0
920 val trailingHeight =
921 measurables
922 .fastFirstOrNull { it.layoutId == TrailingId }
923 ?.let {
924 remainingWidth =
925 remainingWidth.subtractConstraintSafely(
926 it.maxIntrinsicWidth(Constraints.Infinity)
927 )
928 intrinsicMeasurer(it, width)
929 } ?: 0
930
931 val labelHeight =
932 measurables
933 .fastFirstOrNull { it.layoutId == LabelId }
934 ?.let { intrinsicMeasurer(it, remainingWidth) } ?: 0
935
936 val textFieldHeight =
937 intrinsicMeasurer(measurables.fastFirst { it.layoutId == TextFieldId }, remainingWidth)
938 val placeholderHeight =
939 measurables
940 .fastFirstOrNull { it.layoutId == PlaceholderId }
941 ?.let { intrinsicMeasurer(it, remainingWidth) } ?: 0
942
943 return calculateHeight(
944 textFieldHeight = textFieldHeight,
945 hasLabel = labelHeight > 0,
946 labelBaseline = labelHeight,
947 leadingHeight = leadingHeight,
948 trailingHeight = trailingHeight,
949 placeholderHeight = placeholderHeight,
950 constraints = Constraints(),
951 density = density,
952 paddingValues = paddingValues
953 )
954 }
955 }
956
calculateWidthnull957 private fun calculateWidth(
958 leadingWidth: Int,
959 trailingWidth: Int,
960 textFieldWidth: Int,
961 labelWidth: Int,
962 placeholderWidth: Int,
963 constraints: Constraints
964 ): Int {
965 val middleSection = maxOf(textFieldWidth, labelWidth, placeholderWidth)
966 val wrappedWidth = leadingWidth + middleSection + trailingWidth
967 return constraints.constrainWidth(wrappedWidth)
968 }
969
calculateHeightnull970 private fun calculateHeight(
971 textFieldHeight: Int,
972 hasLabel: Boolean,
973 labelBaseline: Int,
974 leadingHeight: Int,
975 trailingHeight: Int,
976 placeholderHeight: Int,
977 constraints: Constraints,
978 density: Float,
979 paddingValues: PaddingValues
980 ): Int {
981 val paddingToLabel = TextFieldTopPadding.value * density
982 val topPaddingValue = paddingValues.calculateTopPadding().value * density
983 val bottomPaddingValue = paddingValues.calculateBottomPadding().value * density
984
985 val inputFieldHeight = max(textFieldHeight, placeholderHeight)
986 val middleSectionHeight =
987 if (hasLabel) {
988 labelBaseline + paddingToLabel + inputFieldHeight + bottomPaddingValue
989 } else {
990 topPaddingValue + inputFieldHeight + bottomPaddingValue
991 }
992 return constraints.constrainHeight(
993 maxOf(middleSectionHeight.roundToInt(), leadingHeight, trailingHeight)
994 )
995 }
996
997 /**
998 * Places the provided text field, placeholder and label with respect to the baseline offsets in
999 * [TextField] when there is a label. When there is no label, [placeWithoutLabel] is used.
1000 */
placeWithLabelnull1001 private fun Placeable.PlacementScope.placeWithLabel(
1002 width: Int,
1003 height: Int,
1004 textfieldPlaceable: Placeable,
1005 labelPlaceable: Placeable?,
1006 placeholderPlaceable: Placeable?,
1007 leadingPlaceable: Placeable?,
1008 trailingPlaceable: Placeable?,
1009 singleLine: Boolean,
1010 labelEndPosition: Int,
1011 textPosition: Int,
1012 animationProgress: Float,
1013 density: Float
1014 ) {
1015 leadingPlaceable?.placeRelative(
1016 0,
1017 Alignment.CenterVertically.align(leadingPlaceable.height, height)
1018 )
1019 trailingPlaceable?.placeRelative(
1020 width - trailingPlaceable.width,
1021 Alignment.CenterVertically.align(trailingPlaceable.height, height)
1022 )
1023 labelPlaceable?.let {
1024 // if it's a single line, the label's start position is in the center of the
1025 // container. When it's a multiline text field, the label's start position is at the
1026 // top with padding
1027 val startPosition =
1028 if (singleLine) {
1029 Alignment.CenterVertically.align(it.height, height)
1030 } else {
1031 // even though the padding is defined by developer, it only affects text field when
1032 // animation progress == 1,
1033 // which is when text field is focused or non-empty input text. The start position
1034 // of the label is always 16.dp.
1035 (TextFieldPadding.value * density).roundToInt()
1036 }
1037 val distance = startPosition - labelEndPosition
1038 val positionY = startPosition - (distance * animationProgress).roundToInt()
1039 it.placeRelative(widthOrZero(leadingPlaceable), positionY)
1040 }
1041 textfieldPlaceable.placeRelative(widthOrZero(leadingPlaceable), textPosition)
1042 placeholderPlaceable?.placeRelative(widthOrZero(leadingPlaceable), textPosition)
1043 }
1044
1045 /**
1046 * Places the provided text field and placeholder in [TextField] when there is no label. When there
1047 * is a label, [placeWithLabel] is used
1048 */
Placeablenull1049 private fun Placeable.PlacementScope.placeWithoutLabel(
1050 width: Int,
1051 height: Int,
1052 textPlaceable: Placeable,
1053 placeholderPlaceable: Placeable?,
1054 leadingPlaceable: Placeable?,
1055 trailingPlaceable: Placeable?,
1056 singleLine: Boolean,
1057 density: Float,
1058 paddingValues: PaddingValues
1059 ) {
1060 val topPadding = (paddingValues.calculateTopPadding().value * density).roundToInt()
1061
1062 leadingPlaceable?.placeRelative(
1063 0,
1064 Alignment.CenterVertically.align(leadingPlaceable.height, height)
1065 )
1066 trailingPlaceable?.placeRelative(
1067 width - trailingPlaceable.width,
1068 Alignment.CenterVertically.align(trailingPlaceable.height, height)
1069 )
1070
1071 // Single line text field without label places its input center vertically. Multiline text
1072 // field without label places its input at the top with padding
1073 val textVerticalPosition =
1074 if (singleLine) {
1075 Alignment.CenterVertically.align(textPlaceable.height, height)
1076 } else {
1077 topPadding
1078 }
1079 textPlaceable.placeRelative(widthOrZero(leadingPlaceable), textVerticalPosition)
1080
1081 // placeholder is placed similar to the text input above
1082 placeholderPlaceable?.let {
1083 val placeholderVerticalPosition =
1084 if (singleLine) {
1085 Alignment.CenterVertically.align(placeholderPlaceable.height, height)
1086 } else {
1087 topPadding
1088 }
1089 it.placeRelative(widthOrZero(leadingPlaceable), placeholderVerticalPosition)
1090 }
1091 }
1092
1093 /** A draw modifier that draws a bottom indicator line in [TextField] */
drawIndicatorLinenull1094 internal fun Modifier.drawIndicatorLine(indicatorBorder: BorderStroke): Modifier {
1095 val strokeWidthDp = indicatorBorder.width
1096 return drawWithContent {
1097 drawContent()
1098 if (strokeWidthDp == Dp.Hairline) return@drawWithContent
1099 val strokeWidth = strokeWidthDp.value * density
1100 val y = size.height - strokeWidth / 2
1101 drawLine(indicatorBorder.brush, Offset(0f, y), Offset(size.width, y), strokeWidth)
1102 }
1103 }
1104
1105 /** Padding from the label's baseline to the top */
1106 internal val FirstBaselineOffset = 20.dp
1107
1108 /** Padding from input field to the bottom */
1109 internal val TextFieldBottomPadding = 10.dp
1110
1111 /** Padding from label's baseline (or FirstBaselineOffset) to the input field */
1112 /*@VisibleForTesting*/
1113 internal val TextFieldTopPadding = 2.dp
1114