1 /*
2 * Copyright 2021 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.animation.animateColorAsState
20 import androidx.compose.animation.core.animateDpAsState
21 import androidx.compose.animation.core.tween
22 import androidx.compose.foundation.BorderStroke
23 import androidx.compose.foundation.border
24 import androidx.compose.foundation.interaction.Interaction
25 import androidx.compose.foundation.interaction.InteractionSource
26 import androidx.compose.foundation.interaction.MutableInteractionSource
27 import androidx.compose.foundation.interaction.collectIsFocusedAsState
28 import androidx.compose.foundation.layout.Box
29 import androidx.compose.foundation.layout.PaddingValues
30 import androidx.compose.foundation.shape.ZeroCornerSize
31 import androidx.compose.foundation.text.BasicTextField
32 import androidx.compose.material.TextFieldDefaults.OutlinedTextFieldDecorationBox
33 import androidx.compose.runtime.Composable
34 import androidx.compose.runtime.Immutable
35 import androidx.compose.runtime.ReadOnlyComposable
36 import androidx.compose.runtime.Stable
37 import androidx.compose.runtime.State
38 import androidx.compose.runtime.getValue
39 import androidx.compose.runtime.rememberUpdatedState
40 import androidx.compose.ui.Modifier
41 import androidx.compose.ui.composed
42 import androidx.compose.ui.graphics.Color
43 import androidx.compose.ui.graphics.Shape
44 import androidx.compose.ui.graphics.SolidColor
45 import androidx.compose.ui.platform.debugInspectorInfo
46 import androidx.compose.ui.text.input.VisualTransformation
47 import androidx.compose.ui.unit.Dp
48 import androidx.compose.ui.unit.dp
49
50 /**
51 * Represents the colors of the input text, background and content (including label, placeholder,
52 * leading and trailing icons) used in a text field in different states.
53 *
54 * See [TextFieldDefaults.textFieldColors] for the default colors used in [TextField]. See
55 * [TextFieldDefaults.outlinedTextFieldColors] for the default colors used in [OutlinedTextField].
56 */
57 @Stable
58 interface TextFieldColors {
59 /**
60 * Represents the color used for the input text of this text field.
61 *
62 * @param enabled whether the text field is enabled
63 */
textColornull64 @Composable fun textColor(enabled: Boolean): State<Color>
65
66 /**
67 * Represents the background color for this text field.
68 *
69 * @param enabled whether the text field is enabled
70 */
71 @Composable fun backgroundColor(enabled: Boolean): State<Color>
72
73 /**
74 * Represents the color used for the placeholder of this text field.
75 *
76 * @param enabled whether the text field is enabled
77 */
78 @Composable fun placeholderColor(enabled: Boolean): State<Color>
79
80 /**
81 * Represents the color used for the label of this text field.
82 *
83 * @param enabled whether the text field is enabled
84 * @param error whether the text field should show error color according to the Material
85 * specifications. If the label is being used as a placeholder, this will be false even if the
86 * input is invalid, as the placeholder should not use the error color
87 * @param interactionSource the [InteractionSource] of this text field. Helps to determine if
88 * the text field is in focus or not
89 */
90 @Composable
91 fun labelColor(
92 enabled: Boolean,
93 error: Boolean,
94 interactionSource: InteractionSource
95 ): State<Color>
96
97 /**
98 * Represents the color used for the leading icon of this text field.
99 *
100 * @param enabled whether the text field is enabled
101 * @param isError whether the text field's current value is in error
102 */
103 @Deprecated(
104 message = "Use/implement overload with interactionSource parameter",
105 replaceWith = ReplaceWith("leadingIconColor(enabled, isError, interactionSource)"),
106 level = DeprecationLevel.WARNING,
107 )
108 @Composable
109 fun leadingIconColor(enabled: Boolean, isError: Boolean): State<Color>
110
111 /**
112 * Represents the color used for the leading icon of this text field.
113 *
114 * @param enabled whether the text field is enabled
115 * @param isError whether the text field's current value is in error
116 * @param interactionSource the [InteractionSource] of this text field. Helps to determine if
117 * the text field is in focus or not
118 */
119 @Composable
120 fun leadingIconColor(
121 enabled: Boolean,
122 isError: Boolean,
123 interactionSource: InteractionSource
124 ): State<Color> {
125 @Suppress("DEPRECATION") return leadingIconColor(enabled, isError)
126 }
127
128 /**
129 * Represents the color used for the trailing icon of this text field.
130 *
131 * @param enabled whether the text field is enabled
132 * @param isError whether the text field's current value is in error
133 */
134 @Deprecated(
135 message = "Use/implement overload with interactionSource parameter",
136 replaceWith = ReplaceWith("trailingIconColor(enabled, isError, interactionSource)"),
137 level = DeprecationLevel.WARNING,
138 )
139 @Composable
trailingIconColornull140 fun trailingIconColor(enabled: Boolean, isError: Boolean): State<Color>
141
142 /**
143 * Represents the color used for the trailing icon of this text field.
144 *
145 * @param enabled whether the text field is enabled
146 * @param isError whether the text field's current value is in error
147 * @param interactionSource the [InteractionSource] of this text field. Helps to determine if
148 * the text field is in focus or not
149 */
150 @Composable
151 fun trailingIconColor(
152 enabled: Boolean,
153 isError: Boolean,
154 interactionSource: InteractionSource
155 ): State<Color> {
156 @Suppress("DEPRECATION") return trailingIconColor(enabled, isError)
157 }
158
159 /**
160 * Represents the color used for the border indicator of this text field.
161 *
162 * @param enabled whether the text field is enabled
163 * @param isError whether the text field's current value is in error
164 * @param interactionSource the [InteractionSource] of this text field. Helps to determine if
165 * the text field is in focus or not
166 */
167 @Composable
indicatorColornull168 fun indicatorColor(
169 enabled: Boolean,
170 isError: Boolean,
171 interactionSource: InteractionSource
172 ): State<Color>
173
174 /**
175 * Represents the color used for the cursor of this text field.
176 *
177 * @param isError whether the text field's current value is in error
178 */
179 @Composable fun cursorColor(isError: Boolean): State<Color>
180 }
181
182 /**
183 * Temporary experimental interface, to expose interactionSource to leadingIconColor and
184 * trailingIconColor.
185 */
186 @Deprecated(
187 message = "Empty interface; use parent TextFieldColors instead",
188 replaceWith =
189 ReplaceWith("TextFieldColors", imports = ["androidx.compose.material.TextFieldColors"])
190 )
191 @ExperimentalMaterialApi
192 interface TextFieldColorsWithIcons : TextFieldColors
193
194 /** Contains the default values used by [TextField] and [OutlinedTextField]. */
195 @Immutable
196 object TextFieldDefaults {
197 /**
198 * The default min height applied to a [TextField] and [OutlinedTextField]. Note that you can
199 * override it by applying Modifier.heightIn directly on a text field.
200 */
201 val MinHeight = 56.dp
202
203 /**
204 * The default min width applied to a [TextField] and [OutlinedTextField]. Note that you can
205 * override it by applying Modifier.widthIn directly on a text field.
206 */
207 val MinWidth = 280.dp
208
209 /**
210 * The default opacity used for a [TextField]'s and [OutlinedTextField]'s leading and trailing
211 * icons color.
212 */
213 const val IconOpacity = 0.54f
214
215 /** The default shape used for a [TextField]'s background */
216 val TextFieldShape: Shape
217 @Composable
218 @ReadOnlyComposable
219 get() =
220 MaterialTheme.shapes.small.copy(
221 bottomEnd = ZeroCornerSize,
222 bottomStart = ZeroCornerSize
223 )
224
225 /** The default shape used for a [OutlinedTextField]'s background and border */
226 val OutlinedTextFieldShape: Shape
227 @Composable @ReadOnlyComposable get() = MaterialTheme.shapes.small
228
229 /**
230 * The default thickness of the border in [OutlinedTextField] or indicator line in [TextField]
231 * in unfocused state.
232 */
233 val UnfocusedBorderThickness = 1.dp
234
235 /**
236 * The default thickness of the border in [OutlinedTextField] or indicator line in [TextField]
237 * in focused state.
238 */
239 val FocusedBorderThickness = 2.dp
240
241 /** The default opacity used for a [TextField]'s background color. */
242 const val BackgroundOpacity = 0.12f
243
244 // Filled text field uses 42% opacity to meet the contrast requirements for accessibility
245 // reasons
246 /**
247 * The default opacity used for a [TextField]'s indicator line color when text field is not
248 * focused.
249 */
250 const val UnfocusedIndicatorLineOpacity = 0.42f
251
252 /**
253 * A modifier to draw a default bottom indicator line for [TextField]. You can use this modifier
254 * if you build your custom text field using [TextFieldDecorationBox]. The [TextField] component
255 * applies it automatically.
256 *
257 * @param enabled whether the text field is enabled.
258 * @param isError whether the text field's current value is in error.
259 * @param interactionSource the [InteractionSource] of this text field. Used to determine if the
260 * text field is in focus or not.
261 * @param colors [TextFieldColors] used to resolve colors of the text field.
262 * @param focusedIndicatorLineThickness thickness of the indicator line when text field is
263 * focused.
264 * @param unfocusedIndicatorLineThickness thickness of the indicator line when text field is not
265 * focused.
266 */
267 fun Modifier.indicatorLine(
268 enabled: Boolean,
269 isError: Boolean,
270 interactionSource: InteractionSource,
271 colors: TextFieldColors,
272 focusedIndicatorLineThickness: Dp = FocusedBorderThickness,
273 unfocusedIndicatorLineThickness: Dp = UnfocusedBorderThickness
274 ) =
275 composed(
276 inspectorInfo =
277 debugInspectorInfo {
278 name = "indicatorLine"
279 properties["enabled"] = enabled
280 properties["isError"] = isError
281 properties["interactionSource"] = interactionSource
282 properties["colors"] = colors
283 properties["focusedIndicatorLineThickness"] = focusedIndicatorLineThickness
284 properties["unfocusedIndicatorLineThickness"] = unfocusedIndicatorLineThickness
285 }
286 ) {
287 val stroke =
288 animateBorderStrokeAsState(
289 enabled = enabled,
290 isError = isError,
291 interactionSource = interactionSource,
292 colors = colors,
293 focusedBorderThickness = focusedIndicatorLineThickness,
294 unfocusedBorderThickness = unfocusedIndicatorLineThickness,
295 )
296 Modifier.drawIndicatorLine(stroke.value)
297 }
298
299 /**
300 * Composable that draws a default border stroke in [OutlinedTextField]. You can use it to draw
301 * a border stroke in your custom text field based on [OutlinedTextFieldDecorationBox]. The
302 * [OutlinedTextField] component applies it automatically.
303 *
304 * @param enabled whether the text field is enabled.
305 * @param isError whether the text field's current value is in error.
306 * @param interactionSource the [InteractionSource] of this text field. Used to determine if the
307 * text field is in focus or not.
308 * @param colors [TextFieldColors] used to resolve colors of the text field.
309 * @param focusedBorderThickness thickness of the [OutlinedTextField]'s border when it is in
310 * focused state.
311 * @param unfocusedBorderThickness thickness of the [OutlinedTextField]'s border when it is not
312 * in focused state.
313 */
314 @Composable
315 fun BorderBox(
316 enabled: Boolean,
317 isError: Boolean,
318 interactionSource: InteractionSource,
319 colors: TextFieldColors,
320 shape: Shape = OutlinedTextFieldShape,
321 focusedBorderThickness: Dp = FocusedBorderThickness,
322 unfocusedBorderThickness: Dp = UnfocusedBorderThickness
323 ) {
324 val borderStroke =
325 animateBorderStrokeAsState(
326 enabled = enabled,
327 isError = isError,
328 interactionSource = interactionSource,
329 colors = colors,
330 focusedBorderThickness = focusedBorderThickness,
331 unfocusedBorderThickness = unfocusedBorderThickness
332 )
333 Box(Modifier.border(borderStroke.value, shape))
334 }
335
336 /**
337 * Default content padding applied to [TextField] when there is a label.
338 *
339 * Note that when the label is present, the "top" padding (unlike rest of the paddings) is a
340 * distance between the label's last baseline and the top edge of the [TextField]. If the "top"
341 * value is smaller than the last baseline of the label, then there will be no space between the
342 * label and top edge of the [TextField].
343 */
344 fun textFieldWithLabelPadding(
345 start: Dp = TextFieldPadding,
346 end: Dp = TextFieldPadding,
347 top: Dp = FirstBaselineOffset,
348 bottom: Dp = TextFieldBottomPadding
349 ): PaddingValues = PaddingValues(start, top, end, bottom)
350
351 /** Default content padding applied to [TextField] when the label is null. */
352 fun textFieldWithoutLabelPadding(
353 start: Dp = TextFieldPadding,
354 top: Dp = TextFieldPadding,
355 end: Dp = TextFieldPadding,
356 bottom: Dp = TextFieldPadding
357 ): PaddingValues = PaddingValues(start, top, end, bottom)
358
359 /** Default content padding applied to [OutlinedTextField]. */
360 fun outlinedTextFieldPadding(
361 start: Dp = TextFieldPadding,
362 top: Dp = TextFieldPadding,
363 end: Dp = TextFieldPadding,
364 bottom: Dp = TextFieldPadding
365 ): PaddingValues = PaddingValues(start, top, end, bottom)
366
367 /**
368 * Creates a [TextFieldColors] that represents the default input text, background and content
369 * (including label, placeholder, leading and trailing icons) colors used in a [TextField].
370 */
371 @Composable
372 fun textFieldColors(
373 textColor: Color = LocalContentColor.current.copy(LocalContentAlpha.current),
374 disabledTextColor: Color = textColor.copy(ContentAlpha.disabled),
375 backgroundColor: Color = MaterialTheme.colors.onSurface.copy(alpha = BackgroundOpacity),
376 cursorColor: Color = MaterialTheme.colors.primary,
377 errorCursorColor: Color = MaterialTheme.colors.error,
378 focusedIndicatorColor: Color = MaterialTheme.colors.primary.copy(alpha = ContentAlpha.high),
379 unfocusedIndicatorColor: Color =
380 MaterialTheme.colors.onSurface.copy(alpha = UnfocusedIndicatorLineOpacity),
381 disabledIndicatorColor: Color = unfocusedIndicatorColor.copy(alpha = ContentAlpha.disabled),
382 errorIndicatorColor: Color = MaterialTheme.colors.error,
383 leadingIconColor: Color = MaterialTheme.colors.onSurface.copy(alpha = IconOpacity),
384 disabledLeadingIconColor: Color = leadingIconColor.copy(alpha = ContentAlpha.disabled),
385 errorLeadingIconColor: Color = leadingIconColor,
386 trailingIconColor: Color = MaterialTheme.colors.onSurface.copy(alpha = IconOpacity),
387 disabledTrailingIconColor: Color = trailingIconColor.copy(alpha = ContentAlpha.disabled),
388 errorTrailingIconColor: Color = MaterialTheme.colors.error,
389 focusedLabelColor: Color = MaterialTheme.colors.primary.copy(alpha = ContentAlpha.high),
390 unfocusedLabelColor: Color = MaterialTheme.colors.onSurface.copy(ContentAlpha.medium),
391 disabledLabelColor: Color = unfocusedLabelColor.copy(ContentAlpha.disabled),
392 errorLabelColor: Color = MaterialTheme.colors.error,
393 placeholderColor: Color = MaterialTheme.colors.onSurface.copy(ContentAlpha.medium),
394 disabledPlaceholderColor: Color = placeholderColor.copy(ContentAlpha.disabled)
395 ): TextFieldColors =
396 DefaultTextFieldColors(
397 textColor = textColor,
398 disabledTextColor = disabledTextColor,
399 cursorColor = cursorColor,
400 errorCursorColor = errorCursorColor,
401 focusedIndicatorColor = focusedIndicatorColor,
402 unfocusedIndicatorColor = unfocusedIndicatorColor,
403 errorIndicatorColor = errorIndicatorColor,
404 disabledIndicatorColor = disabledIndicatorColor,
405 leadingIconColor = leadingIconColor,
406 disabledLeadingIconColor = disabledLeadingIconColor,
407 errorLeadingIconColor = errorLeadingIconColor,
408 trailingIconColor = trailingIconColor,
409 disabledTrailingIconColor = disabledTrailingIconColor,
410 errorTrailingIconColor = errorTrailingIconColor,
411 backgroundColor = backgroundColor,
412 focusedLabelColor = focusedLabelColor,
413 unfocusedLabelColor = unfocusedLabelColor,
414 disabledLabelColor = disabledLabelColor,
415 errorLabelColor = errorLabelColor,
416 placeholderColor = placeholderColor,
417 disabledPlaceholderColor = disabledPlaceholderColor
418 )
419
420 /**
421 * Creates a [TextFieldColors] that represents the default input text, background and content
422 * (including label, placeholder, leading and trailing icons) colors used in an
423 * [OutlinedTextField].
424 */
425 @Composable
426 fun outlinedTextFieldColors(
427 textColor: Color = LocalContentColor.current.copy(LocalContentAlpha.current),
428 disabledTextColor: Color = textColor.copy(ContentAlpha.disabled),
429 backgroundColor: Color = Color.Transparent,
430 cursorColor: Color = MaterialTheme.colors.primary,
431 errorCursorColor: Color = MaterialTheme.colors.error,
432 focusedBorderColor: Color = MaterialTheme.colors.primary.copy(alpha = ContentAlpha.high),
433 unfocusedBorderColor: Color =
434 MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.disabled),
435 disabledBorderColor: Color = unfocusedBorderColor.copy(alpha = ContentAlpha.disabled),
436 errorBorderColor: Color = MaterialTheme.colors.error,
437 leadingIconColor: Color = MaterialTheme.colors.onSurface.copy(alpha = IconOpacity),
438 disabledLeadingIconColor: Color = leadingIconColor.copy(alpha = ContentAlpha.disabled),
439 errorLeadingIconColor: Color = leadingIconColor,
440 trailingIconColor: Color = MaterialTheme.colors.onSurface.copy(alpha = IconOpacity),
441 disabledTrailingIconColor: Color = trailingIconColor.copy(alpha = ContentAlpha.disabled),
442 errorTrailingIconColor: Color = MaterialTheme.colors.error,
443 focusedLabelColor: Color = MaterialTheme.colors.primary.copy(alpha = ContentAlpha.high),
444 unfocusedLabelColor: Color = MaterialTheme.colors.onSurface.copy(ContentAlpha.medium),
445 disabledLabelColor: Color = unfocusedLabelColor.copy(ContentAlpha.disabled),
446 errorLabelColor: Color = MaterialTheme.colors.error,
447 placeholderColor: Color = MaterialTheme.colors.onSurface.copy(ContentAlpha.medium),
448 disabledPlaceholderColor: Color = placeholderColor.copy(ContentAlpha.disabled)
449 ): TextFieldColors =
450 DefaultTextFieldColors(
451 textColor = textColor,
452 disabledTextColor = disabledTextColor,
453 cursorColor = cursorColor,
454 errorCursorColor = errorCursorColor,
455 focusedIndicatorColor = focusedBorderColor,
456 unfocusedIndicatorColor = unfocusedBorderColor,
457 errorIndicatorColor = errorBorderColor,
458 disabledIndicatorColor = disabledBorderColor,
459 leadingIconColor = leadingIconColor,
460 disabledLeadingIconColor = disabledLeadingIconColor,
461 errorLeadingIconColor = errorLeadingIconColor,
462 trailingIconColor = trailingIconColor,
463 disabledTrailingIconColor = disabledTrailingIconColor,
464 errorTrailingIconColor = errorTrailingIconColor,
465 backgroundColor = backgroundColor,
466 focusedLabelColor = focusedLabelColor,
467 unfocusedLabelColor = unfocusedLabelColor,
468 disabledLabelColor = disabledLabelColor,
469 errorLabelColor = errorLabelColor,
470 placeholderColor = placeholderColor,
471 disabledPlaceholderColor = disabledPlaceholderColor
472 )
473
474 /**
475 * A decoration box used to create custom text fields based on
476 * [Material Design filled text field](https://m2.material.io/components/text-fields#filled-text-field).
477 *
478 * If your text field requires customising elements that aren't exposed by [TextField], consider
479 * using this decoration box to achieve the desired design.
480 *
481 * For example, if you need to create a dense text field, use [contentPadding] parameter to
482 * decrease the paddings around the input field. If you need to customise the bottom indicator,
483 * apply [indicatorLine] modifier to achieve that.
484 *
485 * Example of custom text field based on [TextFieldDecorationBox]:
486 *
487 * @sample androidx.compose.material.samples.CustomTextFieldBasedOnDecorationBox
488 * @param value the input [String] shown by the text field
489 * @param innerTextField input text field that this decoration box wraps. Pass the
490 * framework-controlled composable parameter `innerTextField` from the `decorationBox` lambda
491 * of the [BasicTextField].
492 * @param enabled the enabled state of the text field. When `false`, this decoration box will
493 * appear visually disabled. This must be the same value that is passed to [BasicTextField].
494 * @param singleLine indicates if this is a single line or multi line text field. This must be
495 * the same value that is passed to [BasicTextField].
496 * @param visualTransformation transforms the visual representation of the input [value]. This
497 * must be the same value that is passed to [BasicTextField].
498 * @param interactionSource the read-only [InteractionSource] representing the stream of
499 * [Interaction]s for this text field. You must first create and pass in your own `remember`ed
500 * [MutableInteractionSource] instance to the [BasicTextField] for it to dispatch events. And
501 * then pass the same instance to this decoration box to observe [Interaction]s and customize
502 * the appearance / behavior of this text field in different states.
503 * @param isError indicates if the text field's current value is in an error state. When `true`,
504 * this decoration box will display its contents in an error color.
505 * @param label the optional label to be displayed inside the text field container. The default
506 * text style uses [Typography.caption] when the label is minimized and [Typography.subtitle1]
507 * when the label is expanded.
508 * @param placeholder the optional placeholder to be displayed when the text field is in focus
509 * and the input text is empty. The default text style for internal [Text] is
510 * [Typography.subtitle1].
511 * @param leadingIcon the optional leading icon to be displayed at the beginning of the text
512 * field container.
513 * @param trailingIcon the optional trailing icon to be displayed at the end of the text field
514 * container.
515 * @param shape the shape of the text field's container.
516 * @param colors [TextFieldColors] that will be used to resolve the colors used for this text
517 * field in different states. See [TextFieldDefaults.textFieldColors].
518 * @param contentPadding the spacing values to apply internally between the internals of text
519 * field and the decoration box container. You can use it to implement dense text fields or
520 * simply to control horizontal padding. Note that the padding values may not be respected if
521 * they are incompatible with the text field's size constraints or layout. See
522 * [TextFieldDefaults.textFieldWithLabelPadding] and
523 * [TextFieldDefaults.textFieldWithoutLabelPadding].
524 */
525 @Composable
526 fun TextFieldDecorationBox(
527 value: String,
528 innerTextField: @Composable () -> Unit,
529 enabled: Boolean,
530 singleLine: Boolean,
531 visualTransformation: VisualTransformation,
532 interactionSource: InteractionSource,
533 isError: Boolean = false,
534 label: @Composable (() -> Unit)? = null,
535 placeholder: @Composable (() -> Unit)? = null,
536 leadingIcon: @Composable (() -> Unit)? = null,
537 trailingIcon: @Composable (() -> Unit)? = null,
538 shape: Shape = TextFieldShape,
539 colors: TextFieldColors = textFieldColors(),
540 contentPadding: PaddingValues =
541 if (label == null) {
542 textFieldWithoutLabelPadding()
543 } else {
544 textFieldWithLabelPadding()
545 }
546 ) {
547 CommonDecorationBox(
548 type = TextFieldType.Filled,
549 value = value,
550 innerTextField = innerTextField,
551 visualTransformation = visualTransformation,
552 placeholder = placeholder,
553 label = label,
554 leadingIcon = leadingIcon,
555 trailingIcon = trailingIcon,
556 singleLine = singleLine,
557 enabled = enabled,
558 isError = isError,
559 interactionSource = interactionSource,
560 shape = shape,
561 colors = colors,
562 contentPadding = contentPadding,
563 border = null,
564 )
565 }
566
567 /**
568 * A decoration box used to create custom text fields based on
569 * [Material Design outlined text field](https://m2.material.io/components/text-fields#outlined-text-field).
570 *
571 * If your text field requires customising elements that aren't exposed by [OutlinedTextField],
572 * consider using this decoration box to achieve the desired design.
573 *
574 * For example, if you need to create a dense outlined text field, use [contentPadding]
575 * parameter to decrease the paddings around the input field. If you need to change the
576 * thickness of the border, use [border] parameter to achieve that.
577 *
578 * Example of custom text field based on [OutlinedTextFieldDecorationBox]:
579 *
580 * @sample androidx.compose.material.samples.CustomOutlinedTextFieldBasedOnDecorationBox
581 * @param value the input [String] shown by the text field
582 * @param innerTextField input text field that this decoration box wraps. Pass the
583 * framework-controlled composable parameter `innerTextField` from the `decorationBox` lambda
584 * of the [BasicTextField].
585 * @param enabled the enabled state of the text field. When `false`, this decoration box will
586 * appear visually disabled. This must be the same value that is passed to [BasicTextField].
587 * @param singleLine indicates if this is a single line or multi line text field. This must be
588 * the same value that is passed to [BasicTextField].
589 * @param visualTransformation transforms the visual representation of the input [value]. This
590 * must be the same value that is passed to [BasicTextField].
591 * @param interactionSource the read-only [InteractionSource] representing the stream of
592 * [Interaction]s for this text field. You must first create and pass in your own `remember`ed
593 * [MutableInteractionSource] instance to the [BasicTextField] for it to dispatch events. And
594 * then pass the same instance to this decoration box to observe [Interaction]s and customize
595 * the appearance / behavior of this text field in different states.
596 * @param isError indicates if the text field's current value is in an error state. When `true`,
597 * this decoration box will display its contents in an error color.
598 * @param label the optional label to be displayed inside the text field container. The default
599 * text style uses [Typography.caption] when the label is minimized and [Typography.subtitle1]
600 * when the label is expanded.
601 * @param placeholder the optional placeholder to be displayed when the text field is in focus
602 * and the input text is empty. The default text style for internal [Text] is
603 * [Typography.subtitle1].
604 * @param leadingIcon the optional leading icon to be displayed at the beginning of the text
605 * field container.
606 * @param trailingIcon the optional trailing icon to be displayed at the end of the text field
607 * container.
608 * @param shape the shape of the text field's container and border.
609 * @param colors [TextFieldColors] that will be used to resolve the colors used for this text
610 * field in different states. See [TextFieldDefaults.outlinedTextFieldColors].
611 * @param border the border to be drawn around the text field. The cutout to fit the [label]
612 * will be automatically added by the framework. Note that by default the color of the border
613 * comes from the [colors].
614 * @param contentPadding the spacing values to apply internally between the internals of text
615 * field and the decoration box container. You can use it to implement dense text fields or
616 * simply to control horizontal padding. Note that the padding values may not be respected if
617 * they are incompatible with the text field's size constraints or layout. See
618 * [TextFieldDefaults.outlinedTextFieldPadding].
619 */
620 @Composable
621 fun OutlinedTextFieldDecorationBox(
622 value: String,
623 innerTextField: @Composable () -> Unit,
624 enabled: Boolean,
625 singleLine: Boolean,
626 visualTransformation: VisualTransformation,
627 interactionSource: InteractionSource,
628 isError: Boolean = false,
629 label: @Composable (() -> Unit)? = null,
630 placeholder: @Composable (() -> Unit)? = null,
631 leadingIcon: @Composable (() -> Unit)? = null,
632 trailingIcon: @Composable (() -> Unit)? = null,
633 shape: Shape = OutlinedTextFieldShape,
634 colors: TextFieldColors = outlinedTextFieldColors(),
635 contentPadding: PaddingValues = outlinedTextFieldPadding(),
636 border: @Composable () -> Unit = {
637 BorderBox(enabled, isError, interactionSource, colors, shape)
638 }
639 ) {
640 CommonDecorationBox(
641 type = TextFieldType.Outlined,
642 value = value,
643 visualTransformation = visualTransformation,
644 innerTextField = innerTextField,
645 placeholder = placeholder,
646 label = label,
647 leadingIcon = leadingIcon,
648 trailingIcon = trailingIcon,
649 singleLine = singleLine,
650 enabled = enabled,
651 isError = isError,
652 interactionSource = interactionSource,
653 shape = shape,
654 colors = colors,
655 contentPadding = contentPadding,
656 border = border,
657 )
658 }
659
660 @Deprecated(
661 level = DeprecationLevel.HIDDEN,
662 message = "Maintained for binary compatibility. Use overload with `shape` parameter."
663 )
664 @Composable
665 @ExperimentalMaterialApi
666 fun TextFieldDecorationBox(
667 value: String,
668 innerTextField: @Composable () -> Unit,
669 enabled: Boolean,
670 singleLine: Boolean,
671 visualTransformation: VisualTransformation,
672 interactionSource: InteractionSource,
673 isError: Boolean = false,
674 label: @Composable (() -> Unit)? = null,
675 placeholder: @Composable (() -> Unit)? = null,
676 leadingIcon: @Composable (() -> Unit)? = null,
677 trailingIcon: @Composable (() -> Unit)? = null,
678 colors: TextFieldColors = textFieldColors(),
679 contentPadding: PaddingValues =
680 if (label == null) {
681 textFieldWithoutLabelPadding()
682 } else {
683 textFieldWithLabelPadding()
684 }
685 ) =
686 TextFieldDecorationBox(
687 value = value,
688 innerTextField = innerTextField,
689 enabled = enabled,
690 singleLine = singleLine,
691 visualTransformation = visualTransformation,
692 interactionSource = interactionSource,
693 isError = isError,
694 label = label,
695 placeholder = placeholder,
696 leadingIcon = leadingIcon,
697 trailingIcon = trailingIcon,
698 shape = TextFieldShape,
699 colors = colors,
700 contentPadding = contentPadding,
701 )
702
703 @Deprecated(
704 level = DeprecationLevel.HIDDEN,
705 message = "Maintained for binary compatibility. Use overload with `shape` parameter."
706 )
707 @Composable
708 @ExperimentalMaterialApi
709 fun OutlinedTextFieldDecorationBox(
710 value: String,
711 innerTextField: @Composable () -> Unit,
712 enabled: Boolean,
713 singleLine: Boolean,
714 visualTransformation: VisualTransformation,
715 interactionSource: InteractionSource,
716 isError: Boolean = false,
717 label: @Composable (() -> Unit)? = null,
718 placeholder: @Composable (() -> Unit)? = null,
719 leadingIcon: @Composable (() -> Unit)? = null,
720 trailingIcon: @Composable (() -> Unit)? = null,
721 colors: TextFieldColors = outlinedTextFieldColors(),
722 contentPadding: PaddingValues = outlinedTextFieldPadding(),
723 border: @Composable () -> Unit = { BorderBox(enabled, isError, interactionSource, colors) }
724 ) =
725 OutlinedTextFieldDecorationBox(
726 value = value,
727 innerTextField = innerTextField,
728 enabled = enabled,
729 singleLine = singleLine,
730 visualTransformation = visualTransformation,
731 interactionSource = interactionSource,
732 isError = isError,
733 label = label,
734 placeholder = placeholder,
735 leadingIcon = leadingIcon,
736 trailingIcon = trailingIcon,
737 shape = OutlinedTextFieldShape,
738 colors = colors,
739 contentPadding = contentPadding,
740 border = border,
741 )
742 }
743
744 @Immutable
745 private class DefaultTextFieldColors(
746 private val textColor: Color,
747 private val disabledTextColor: Color,
748 private val cursorColor: Color,
749 private val errorCursorColor: Color,
750 private val focusedIndicatorColor: Color,
751 private val unfocusedIndicatorColor: Color,
752 private val errorIndicatorColor: Color,
753 private val disabledIndicatorColor: Color,
754 private val leadingIconColor: Color,
755 private val disabledLeadingIconColor: Color,
756 private val errorLeadingIconColor: Color,
757 private val trailingIconColor: Color,
758 private val disabledTrailingIconColor: Color,
759 private val errorTrailingIconColor: Color,
760 private val backgroundColor: Color,
761 private val focusedLabelColor: Color,
762 private val unfocusedLabelColor: Color,
763 private val disabledLabelColor: Color,
764 private val errorLabelColor: Color,
765 private val placeholderColor: Color,
766 private val disabledPlaceholderColor: Color
767 ) : TextFieldColors {
768
769 @Suppress("OVERRIDE_DEPRECATION") // b/407490794
770 @Composable
leadingIconColornull771 override fun leadingIconColor(enabled: Boolean, isError: Boolean): State<Color> {
772 return rememberUpdatedState(
773 when {
774 !enabled -> disabledLeadingIconColor
775 isError -> errorLeadingIconColor
776 else -> leadingIconColor
777 }
778 )
779 }
780
781 @Composable
leadingIconColornull782 override fun leadingIconColor(
783 enabled: Boolean,
784 isError: Boolean,
785 interactionSource: InteractionSource,
786 ): State<Color> {
787 return rememberUpdatedState(
788 when {
789 !enabled -> disabledLeadingIconColor
790 isError -> errorLeadingIconColor
791 else -> leadingIconColor
792 }
793 )
794 }
795
796 @Suppress("OVERRIDE_DEPRECATION") // b/407490794
797 @Composable
trailingIconColornull798 override fun trailingIconColor(enabled: Boolean, isError: Boolean): State<Color> {
799 return rememberUpdatedState(
800 when {
801 !enabled -> disabledTrailingIconColor
802 isError -> errorTrailingIconColor
803 else -> trailingIconColor
804 }
805 )
806 }
807
808 @Composable
trailingIconColornull809 override fun trailingIconColor(
810 enabled: Boolean,
811 isError: Boolean,
812 interactionSource: InteractionSource,
813 ): State<Color> {
814 return rememberUpdatedState(
815 when {
816 !enabled -> disabledTrailingIconColor
817 isError -> errorTrailingIconColor
818 else -> trailingIconColor
819 }
820 )
821 }
822
823 @Composable
indicatorColornull824 override fun indicatorColor(
825 enabled: Boolean,
826 isError: Boolean,
827 interactionSource: InteractionSource
828 ): State<Color> {
829 val focused by interactionSource.collectIsFocusedAsState()
830
831 val targetValue =
832 when {
833 !enabled -> disabledIndicatorColor
834 isError -> errorIndicatorColor
835 focused -> focusedIndicatorColor
836 else -> unfocusedIndicatorColor
837 }
838 return if (enabled) {
839 animateColorAsState(targetValue, tween(durationMillis = AnimationDuration))
840 } else {
841 rememberUpdatedState(targetValue)
842 }
843 }
844
845 @Composable
backgroundColornull846 override fun backgroundColor(enabled: Boolean): State<Color> {
847 return rememberUpdatedState(backgroundColor)
848 }
849
850 @Composable
placeholderColornull851 override fun placeholderColor(enabled: Boolean): State<Color> {
852 return rememberUpdatedState(if (enabled) placeholderColor else disabledPlaceholderColor)
853 }
854
855 @Composable
labelColornull856 override fun labelColor(
857 enabled: Boolean,
858 error: Boolean,
859 interactionSource: InteractionSource
860 ): State<Color> {
861 val focused by interactionSource.collectIsFocusedAsState()
862
863 val targetValue =
864 when {
865 !enabled -> disabledLabelColor
866 error -> errorLabelColor
867 focused -> focusedLabelColor
868 else -> unfocusedLabelColor
869 }
870 return rememberUpdatedState(targetValue)
871 }
872
873 @Composable
textColornull874 override fun textColor(enabled: Boolean): State<Color> {
875 return rememberUpdatedState(if (enabled) textColor else disabledTextColor)
876 }
877
878 @Composable
cursorColornull879 override fun cursorColor(isError: Boolean): State<Color> {
880 return rememberUpdatedState(if (isError) errorCursorColor else cursorColor)
881 }
882
equalsnull883 override fun equals(other: Any?): Boolean {
884 if (this === other) return true
885 if (other == null || this::class != other::class) return false
886
887 other as DefaultTextFieldColors
888
889 if (textColor != other.textColor) return false
890 if (disabledTextColor != other.disabledTextColor) return false
891 if (cursorColor != other.cursorColor) return false
892 if (errorCursorColor != other.errorCursorColor) return false
893 if (focusedIndicatorColor != other.focusedIndicatorColor) return false
894 if (unfocusedIndicatorColor != other.unfocusedIndicatorColor) return false
895 if (errorIndicatorColor != other.errorIndicatorColor) return false
896 if (disabledIndicatorColor != other.disabledIndicatorColor) return false
897 if (leadingIconColor != other.leadingIconColor) return false
898 if (disabledLeadingIconColor != other.disabledLeadingIconColor) return false
899 if (errorLeadingIconColor != other.errorLeadingIconColor) return false
900 if (trailingIconColor != other.trailingIconColor) return false
901 if (disabledTrailingIconColor != other.disabledTrailingIconColor) return false
902 if (errorTrailingIconColor != other.errorTrailingIconColor) return false
903 if (backgroundColor != other.backgroundColor) return false
904 if (focusedLabelColor != other.focusedLabelColor) return false
905 if (unfocusedLabelColor != other.unfocusedLabelColor) return false
906 if (disabledLabelColor != other.disabledLabelColor) return false
907 if (errorLabelColor != other.errorLabelColor) return false
908 if (placeholderColor != other.placeholderColor) return false
909 if (disabledPlaceholderColor != other.disabledPlaceholderColor) return false
910
911 return true
912 }
913
hashCodenull914 override fun hashCode(): Int {
915 var result = textColor.hashCode()
916 result = 31 * result + disabledTextColor.hashCode()
917 result = 31 * result + cursorColor.hashCode()
918 result = 31 * result + errorCursorColor.hashCode()
919 result = 31 * result + focusedIndicatorColor.hashCode()
920 result = 31 * result + unfocusedIndicatorColor.hashCode()
921 result = 31 * result + errorIndicatorColor.hashCode()
922 result = 31 * result + disabledIndicatorColor.hashCode()
923 result = 31 * result + leadingIconColor.hashCode()
924 result = 31 * result + disabledLeadingIconColor.hashCode()
925 result = 31 * result + errorLeadingIconColor.hashCode()
926 result = 31 * result + trailingIconColor.hashCode()
927 result = 31 * result + disabledTrailingIconColor.hashCode()
928 result = 31 * result + errorTrailingIconColor.hashCode()
929 result = 31 * result + backgroundColor.hashCode()
930 result = 31 * result + focusedLabelColor.hashCode()
931 result = 31 * result + unfocusedLabelColor.hashCode()
932 result = 31 * result + disabledLabelColor.hashCode()
933 result = 31 * result + errorLabelColor.hashCode()
934 result = 31 * result + placeholderColor.hashCode()
935 result = 31 * result + disabledPlaceholderColor.hashCode()
936 return result
937 }
938 }
939
940 @Composable
animateBorderStrokeAsStatenull941 private fun animateBorderStrokeAsState(
942 enabled: Boolean,
943 isError: Boolean,
944 interactionSource: InteractionSource,
945 colors: TextFieldColors,
946 focusedBorderThickness: Dp,
947 unfocusedBorderThickness: Dp
948 ): State<BorderStroke> {
949 val focused by interactionSource.collectIsFocusedAsState()
950 val indicatorColor = colors.indicatorColor(enabled, isError, interactionSource)
951 val targetThickness = if (focused) focusedBorderThickness else unfocusedBorderThickness
952 val animatedThickness =
953 if (enabled) {
954 animateDpAsState(targetThickness, tween(durationMillis = AnimationDuration))
955 } else {
956 rememberUpdatedState(unfocusedBorderThickness)
957 }
958 return rememberUpdatedState(
959 BorderStroke(animatedThickness.value, SolidColor(indicatorColor.value))
960 )
961 }
962