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.ScrollState
20 import androidx.compose.foundation.interaction.Interaction
21 import androidx.compose.foundation.interaction.MutableInteractionSource
22 import androidx.compose.foundation.layout.Box
23 import androidx.compose.foundation.layout.PaddingValues
24 import androidx.compose.foundation.layout.calculateEndPadding
25 import androidx.compose.foundation.layout.calculateStartPadding
26 import androidx.compose.foundation.layout.defaultMinSize
27 import androidx.compose.foundation.layout.padding
28 import androidx.compose.foundation.rememberScrollState
29 import androidx.compose.foundation.text.BasicTextField
30 import androidx.compose.foundation.text.KeyboardActions
31 import androidx.compose.foundation.text.KeyboardOptions
32 import androidx.compose.foundation.text.input.InputTransformation
33 import androidx.compose.foundation.text.input.KeyboardActionHandler
34 import androidx.compose.foundation.text.input.OutputTransformation
35 import androidx.compose.foundation.text.input.TextFieldBuffer
36 import androidx.compose.foundation.text.input.TextFieldLineLimits
37 import androidx.compose.foundation.text.input.TextFieldLineLimits.MultiLine
38 import androidx.compose.foundation.text.input.TextFieldLineLimits.SingleLine
39 import androidx.compose.foundation.text.input.TextFieldState
40 import androidx.compose.material.internal.subtractConstraintSafely
41 import androidx.compose.runtime.Composable
42 import androidx.compose.runtime.remember
43 import androidx.compose.ui.Alignment
44 import androidx.compose.ui.Modifier
45 import androidx.compose.ui.draw.drawWithContent
46 import androidx.compose.ui.geometry.Size
47 import androidx.compose.ui.graphics.ClipOp
48 import androidx.compose.ui.graphics.Shape
49 import androidx.compose.ui.graphics.SolidColor
50 import androidx.compose.ui.graphics.drawscope.clipRect
51 import androidx.compose.ui.graphics.takeOrElse
52 import androidx.compose.ui.layout.IntrinsicMeasurable
53 import androidx.compose.ui.layout.IntrinsicMeasureScope
54 import androidx.compose.ui.layout.Layout
55 import androidx.compose.ui.layout.Measurable
56 import androidx.compose.ui.layout.MeasurePolicy
57 import androidx.compose.ui.layout.MeasureResult
58 import androidx.compose.ui.layout.MeasureScope
59 import androidx.compose.ui.layout.Placeable
60 import androidx.compose.ui.layout.layoutId
61 import androidx.compose.ui.platform.LocalDensity
62 import androidx.compose.ui.platform.LocalLayoutDirection
63 import androidx.compose.ui.semantics.semantics
64 import androidx.compose.ui.text.TextStyle
65 import androidx.compose.ui.text.input.ImeAction
66 import androidx.compose.ui.text.input.KeyboardType
67 import androidx.compose.ui.text.input.TextFieldValue
68 import androidx.compose.ui.text.input.VisualTransformation
69 import androidx.compose.ui.unit.Constraints
70 import androidx.compose.ui.unit.IntOffset
71 import androidx.compose.ui.unit.LayoutDirection
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.unit.sp
78 import androidx.compose.ui.util.fastFirst
79 import androidx.compose.ui.util.fastFirstOrNull
80 import androidx.compose.ui.util.lerp
81 import kotlin.math.max
82 import kotlin.math.roundToInt
83
84 /**
85 * [Material Design outlined text
86 * field](https://m2.material.io/components/text-fields#outlined-text-field).
87 *
88 * Outlined text fields have less visual emphasis than filled text fields. When they appear in
89 * places like forms, where many text fields are placed together, their reduced emphasis helps
90 * simplify the layout.
91 *
92 * 
94 *
95 * If you are looking for a filled version, see [TextField]. For a text field specifically designed
96 * for passwords or other secure content, see [OutlinedSecureTextField].
97 *
98 * This overload of [OutlinedTextField] uses [TextFieldState] to keep track of its text content and
99 * position of the cursor or selection.
100 *
101 * See example usage:
102 *
103 * @sample androidx.compose.material.samples.SimpleOutlinedTextFieldSample
104 * @sample androidx.compose.material.samples.OutlinedTextFieldWithInitialValueAndSelection
105 * @param state [TextFieldState] object that holds the internal editing state of this text field.
106 * @param modifier a [Modifier] for this text field
107 * @param enabled controls the enabled state of the [OutlinedTextField]. When `false`, the text
108 * field will be neither editable nor focusable, the input of the text field will not be
109 * selectable, visually text field will appear in the disabled UI state
110 * @param readOnly controls the editable state of the [OutlinedTextField]. When `true`, the text
111 * field can not be modified, however, a user can focus it and copy text from it. Read-only text
112 * fields are usually used to display pre-filled forms that user can not edit
113 * @param textStyle the style to be applied to the input text. The default [textStyle] uses the
114 * [LocalTextStyle] defined by the theme
115 * @param label the optional label to be displayed inside the text field container. The default text
116 * style for internal [Text] is [Typography.caption] when the text field is in focus and
117 * [Typography.subtitle1] when the text field is not in focus
118 * @param placeholder the optional placeholder to be displayed when the text field is in focus and
119 * the input text is empty. The default text style for internal [Text] is [Typography.subtitle1]
120 * @param leadingIcon the optional leading icon to be displayed at the beginning of the text field
121 * container
122 * @param trailingIcon the optional trailing icon to be displayed at the end of the text field
123 * container
124 * @param isError indicates if the text field's current value is in error. If set to true, the
125 * label, bottom indicator and trailing icon by default will be displayed in error color
126 * @param inputTransformation Optional [InputTransformation] that will be used to transform changes
127 * to the [TextFieldState] made by the user. The transformation will be applied to changes made by
128 * hardware and software keyboard events, pasting or dropping text, accessibility services, and
129 * tests. The transformation will _not_ be applied when changing the [state] programmatically, or
130 * when the transformation is changed. If the transformation is changed on an existing text field,
131 * it will be applied to the next user edit. the transformation will not immediately affect the
132 * current [state].
133 * @param outputTransformation An [OutputTransformation] that transforms how the contents of the
134 * text field are presented.
135 * @param keyboardOptions software keyboard options that contains configuration such as
136 * [KeyboardType] and [ImeAction]
137 * @param onKeyboardAction Called when the user presses the action button in the input method editor
138 * (IME), or by pressing the enter key on a hardware keyboard. By default this parameter is null,
139 * and would execute the default behavior for a received IME Action e.g., [ImeAction.Done] would
140 * close the keyboard, [ImeAction.Next] would switch the focus to the next focusable item on the
141 * screen.
142 * @param lineLimits Whether the text field should be [SingleLine], scroll horizontally, and ignore
143 * newlines; or [MultiLine] and grow and scroll vertically. If [SingleLine] is passed, all newline
144 * characters ('\n') within the text will be replaced with regular whitespace (' '), ensuring that
145 * the contents of the text field are presented in a single line.
146 * @param scrollState Scroll state that manages either horizontal or vertical scroll of the text
147 * field. If [lineLimits] is [SingleLine], this text field is treated as single line with
148 * horizontal scroll behavior. In other cases the text field becomes vertically scrollable.
149 * @param shape the shape of the text field's border
150 * @param colors [TextFieldColors] that will be used to resolve color of the text and content
151 * (including label, placeholder, leading and trailing icons, border) for this text field in
152 * different states. See [TextFieldDefaults.outlinedTextFieldColors]
153 * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
154 * emitting [Interaction]s for this text field. You can use this to change the text field's
155 * appearance or preview the text field in different states. Note that if `null` is provided,
156 * interactions will still happen internally.
157 */
158 @Composable
159 fun OutlinedTextField(
160 state: TextFieldState,
161 modifier: Modifier = Modifier,
162 enabled: Boolean = true,
163 readOnly: Boolean = false,
164 textStyle: TextStyle = LocalTextStyle.current,
165 label: @Composable (() -> Unit)? = null,
166 placeholder: @Composable (() -> Unit)? = null,
167 leadingIcon: @Composable (() -> Unit)? = null,
168 trailingIcon: @Composable (() -> Unit)? = null,
169 isError: Boolean = false,
170 inputTransformation: InputTransformation? = null,
171 outputTransformation: OutputTransformation? = null,
172 keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
173 onKeyboardAction: KeyboardActionHandler? = null,
174 lineLimits: TextFieldLineLimits = TextFieldLineLimits.Default,
175 scrollState: ScrollState = rememberScrollState(),
176 shape: Shape = TextFieldDefaults.OutlinedTextFieldShape,
177 colors: TextFieldColors = TextFieldDefaults.outlinedTextFieldColors(),
178 interactionSource: MutableInteractionSource? = null,
179 ) {
180 @Suppress("NAME_SHADOWING")
181 val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
182 // If color is not provided via the text style, use content color as a default
183 val textColor = textStyle.color.takeOrElse { colors.textColor(enabled).value }
184 val mergedTextStyle = textStyle.merge(TextStyle(color = textColor))
185
186 val density = LocalDensity.current
187
188 BasicTextField(
189 state = state,
190 modifier =
191 modifier
192 .then(
193 if (label != null) {
194 Modifier
195 // Merge semantics at the beginning of the modifier chain to ensure
196 // padding is considered part of the text field.
197 .semantics(mergeDescendants = true) {}
198 .padding(top = with(density) { OutlinedTextFieldTopPadding.toDp() })
199 } else {
200 Modifier
201 }
202 )
203 .defaultErrorSemantics(isError, getString(Strings.DefaultErrorMessage))
204 .defaultMinSize(
205 minWidth = TextFieldDefaults.MinWidth,
206 minHeight = TextFieldDefaults.MinHeight
207 ),
208 enabled = enabled,
209 readOnly = readOnly,
210 textStyle = mergedTextStyle,
211 cursorBrush = SolidColor(colors.cursorColor(isError).value),
212 inputTransformation = inputTransformation,
213 outputTransformation = outputTransformation,
214 keyboardOptions = keyboardOptions,
215 onKeyboardAction = onKeyboardAction,
216 interactionSource = interactionSource,
217 scrollState = scrollState,
218 lineLimits = lineLimits,
219 decorator = { innerTextField ->
220 val textPostTransformation =
221 if (outputTransformation == null) {
222 state.text.toString()
223 } else {
224 // TODO: use constructor to create TextFieldBuffer from TextFieldState when
225 // available
226 lateinit var buffer: TextFieldBuffer
227 state.edit { buffer = this }
228 // after edit completes, mutations on buffer are ineffective
229 with(outputTransformation) { buffer.transformOutput() }
230 buffer.asCharSequence().toString()
231 }
232
233 TextFieldDefaults.OutlinedTextFieldDecorationBox(
234 value = textPostTransformation,
235 visualTransformation = VisualTransformation.None,
236 innerTextField = innerTextField,
237 placeholder = placeholder,
238 label = label,
239 leadingIcon = leadingIcon,
240 trailingIcon = trailingIcon,
241 singleLine = lineLimits == SingleLine,
242 enabled = enabled,
243 isError = isError,
244 interactionSource = interactionSource,
245 shape = shape,
246 colors = colors,
247 border = {
248 TextFieldDefaults.BorderBox(enabled, isError, interactionSource, colors, shape)
249 }
250 )
251 }
252 )
253 }
254
255 /**
256 * [Material Design outlined text
257 * field](https://m2.material.io/components/text-fields#outlined-text-field).
258 *
259 * Outlined text fields have less visual emphasis than filled text fields. When they appear in
260 * places like forms, where many text fields are placed together, their reduced emphasis helps
261 * simplify the layout.
262 *
263 * 
265 *
266 * If apart from input text change you also want to observe the cursor location, selection range, or
267 * IME composition use the OutlinedTextField overload with the [TextFieldValue] parameter instead.
268 *
269 * @param value the input text to be shown in the text field
270 * @param onValueChange the callback that is triggered when the input service updates the text. An
271 * updated text comes as a parameter of the callback
272 * @param modifier a [Modifier] for this text field
273 * @param enabled controls the enabled state of the [OutlinedTextField]. When `false`, the text
274 * field will be neither editable nor focusable, the input of the text field will not be
275 * selectable, visually text field will appear in the disabled UI state
276 * @param readOnly controls the editable state of the [OutlinedTextField]. When `true`, the text
277 * field can not be modified, however, a user can focus it and copy text from it. Read-only text
278 * fields are usually used to display pre-filled forms that user can not edit
279 * @param textStyle the style to be applied to the input text. The default [textStyle] uses the
280 * [LocalTextStyle] defined by the theme
281 * @param label the optional label to be displayed inside the text field container. The default text
282 * style for internal [Text] is [Typography.caption] when the text field is in focus and
283 * [Typography.subtitle1] when the text field is not in focus
284 * @param placeholder the optional placeholder to be displayed when the text field is in focus and
285 * the input text is empty. The default text style for internal [Text] is [Typography.subtitle1]
286 * @param leadingIcon the optional leading icon to be displayed at the beginning of the text field
287 * container
288 * @param trailingIcon the optional trailing icon to be displayed at the end of the text field
289 * container
290 * @param isError indicates if the text field's current value is in error. If set to true, the
291 * label, bottom indicator and trailing icon by default will be displayed in error color
292 * @param visualTransformation transforms the visual representation of the input [value] For
293 * example, you can use
294 * [PasswordVisualTransformation][androidx.compose.ui.text.input.PasswordVisualTransformation] to
295 * create a password text field. By default no visual transformation is applied
296 * @param keyboardOptions software keyboard options that contains configuration such as
297 * [KeyboardType] and [ImeAction]
298 * @param keyboardActions when the input service emits an IME action, the corresponding callback is
299 * called. Note that this IME action may be different from what you specified in
300 * [KeyboardOptions.imeAction]
301 * @param singleLine when set to true, this text field becomes a single horizontally scrolling text
302 * field instead of wrapping onto multiple lines. The keyboard will be informed to not show the
303 * return key as the [ImeAction]. Note that [maxLines] parameter will be ignored as the maxLines
304 * attribute will be automatically set to 1
305 * @param maxLines the maximum height in terms of maximum number of visible lines. It is required
306 * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true.
307 * @param minLines the minimum height in terms of minimum number of visible lines. It is required
308 * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true.
309 * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
310 * emitting [Interaction]s for this text field. You can use this to change the text field's
311 * appearance or preview the text field in different states. Note that if `null` is provided,
312 * interactions will still happen internally.
313 * @param shape the shape of the text field's border
314 * @param colors [TextFieldColors] that will be used to resolve color of the text and content
315 * (including label, placeholder, leading and trailing icons, border) for this text field in
316 * different states. See [TextFieldDefaults.outlinedTextFieldColors]
317 */
318 @Composable
OutlinedTextFieldnull319 fun OutlinedTextField(
320 value: String,
321 onValueChange: (String) -> Unit,
322 modifier: Modifier = Modifier,
323 enabled: Boolean = true,
324 readOnly: Boolean = false,
325 textStyle: TextStyle = LocalTextStyle.current,
326 label: @Composable (() -> Unit)? = null,
327 placeholder: @Composable (() -> Unit)? = null,
328 leadingIcon: @Composable (() -> Unit)? = null,
329 trailingIcon: @Composable (() -> Unit)? = null,
330 isError: Boolean = false,
331 visualTransformation: VisualTransformation = VisualTransformation.None,
332 keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
333 keyboardActions: KeyboardActions = KeyboardActions.Default,
334 singleLine: Boolean = false,
335 maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
336 minLines: Int = 1,
337 interactionSource: MutableInteractionSource? = null,
338 shape: Shape = TextFieldDefaults.OutlinedTextFieldShape,
339 colors: TextFieldColors = TextFieldDefaults.outlinedTextFieldColors()
340 ) {
341 @Suppress("NAME_SHADOWING")
342 val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
343 // If color is not provided via the text style, use content color as a default
344 val textColor = textStyle.color.takeOrElse { colors.textColor(enabled).value }
345 val mergedTextStyle = textStyle.merge(TextStyle(color = textColor))
346
347 val density = LocalDensity.current
348
349 BasicTextField(
350 value = value,
351 modifier =
352 modifier
353 .then(
354 if (label != null) {
355 Modifier
356 // Merge semantics at the beginning of the modifier chain to ensure
357 // padding is considered part of the text field.
358 .semantics(mergeDescendants = true) {}
359 .padding(top = with(density) { OutlinedTextFieldTopPadding.toDp() })
360 } else {
361 Modifier
362 }
363 )
364 .defaultErrorSemantics(isError, getString(Strings.DefaultErrorMessage))
365 .defaultMinSize(
366 minWidth = TextFieldDefaults.MinWidth,
367 minHeight = TextFieldDefaults.MinHeight
368 ),
369 onValueChange = onValueChange,
370 enabled = enabled,
371 readOnly = readOnly,
372 textStyle = mergedTextStyle,
373 cursorBrush = SolidColor(colors.cursorColor(isError).value),
374 visualTransformation = visualTransformation,
375 keyboardOptions = keyboardOptions,
376 keyboardActions = keyboardActions,
377 interactionSource = interactionSource,
378 singleLine = singleLine,
379 maxLines = maxLines,
380 minLines = minLines,
381 decorationBox =
382 @Composable { innerTextField ->
383 TextFieldDefaults.OutlinedTextFieldDecorationBox(
384 value = value,
385 visualTransformation = visualTransformation,
386 innerTextField = innerTextField,
387 placeholder = placeholder,
388 label = label,
389 leadingIcon = leadingIcon,
390 trailingIcon = trailingIcon,
391 singleLine = singleLine,
392 enabled = enabled,
393 isError = isError,
394 interactionSource = interactionSource,
395 shape = shape,
396 colors = colors,
397 border = {
398 TextFieldDefaults.BorderBox(
399 enabled,
400 isError,
401 interactionSource,
402 colors,
403 shape
404 )
405 }
406 )
407 }
408 )
409 }
410
411 @Deprecated(
412 "Maintained for binary compatibility. Use version with minLines instead",
413 level = DeprecationLevel.HIDDEN
414 )
415 @Composable
OutlinedTextFieldnull416 fun OutlinedTextField(
417 value: String,
418 onValueChange: (String) -> Unit,
419 modifier: Modifier = Modifier,
420 enabled: Boolean = true,
421 readOnly: Boolean = false,
422 textStyle: TextStyle = LocalTextStyle.current,
423 label: @Composable (() -> Unit)? = null,
424 placeholder: @Composable (() -> Unit)? = null,
425 leadingIcon: @Composable (() -> Unit)? = null,
426 trailingIcon: @Composable (() -> Unit)? = null,
427 isError: Boolean = false,
428 visualTransformation: VisualTransformation = VisualTransformation.None,
429 keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
430 keyboardActions: KeyboardActions = KeyboardActions.Default,
431 singleLine: Boolean = false,
432 maxLines: Int = Int.MAX_VALUE,
433 interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
434 shape: Shape = TextFieldDefaults.OutlinedTextFieldShape,
435 colors: TextFieldColors = TextFieldDefaults.outlinedTextFieldColors()
436 ) {
437 OutlinedTextField(
438 value,
439 onValueChange,
440 modifier,
441 enabled,
442 readOnly,
443 textStyle,
444 label,
445 placeholder,
446 leadingIcon,
447 trailingIcon,
448 isError,
449 visualTransformation,
450 keyboardOptions,
451 keyboardActions,
452 singleLine,
453 maxLines,
454 1,
455 interactionSource,
456 shape,
457 colors
458 )
459 }
460
461 /**
462 * [Material Design outlined text
463 * field](https://m2.material.io/components/text-fields#outlined-text-field).
464 *
465 * Outlined text fields have less visual emphasis than filled text fields. When they appear in
466 * places like forms, where many text fields are placed together, their reduced emphasis helps
467 * simplify the layout.
468 *
469 * 
471 *
472 * This overload provides access to the input text, cursor position and selection range and IME
473 * composition. If you only want to observe an input text change, use the OutlinedTextField overload
474 * with the [String] parameter instead.
475 *
476 * @param value the input [TextFieldValue] to be shown in the text field
477 * @param onValueChange the callback that is triggered when the input service updates values in
478 * [TextFieldValue]. An updated [TextFieldValue] comes as a parameter of the callback
479 * @param modifier a [Modifier] for this text field
480 * @param enabled controls the enabled state of the [OutlinedTextField]. When `false`, the text
481 * field will be neither editable nor focusable, the input of the text field will not be
482 * selectable, visually text field will appear in the disabled UI state
483 * @param readOnly controls the editable state of the [OutlinedTextField]. When `true`, the text
484 * field can not be modified, however, a user can focus it and copy text from it. Read-only text
485 * fields are usually used to display pre-filled forms that user can not edit
486 * @param textStyle the style to be applied to the input text. The default [textStyle] uses the
487 * [LocalTextStyle] defined by the theme
488 * @param label the optional label to be displayed inside the text field container. The default text
489 * style for internal [Text] is [Typography.caption] when the text field is in focus and
490 * [Typography.subtitle1] when the text field is not in focus
491 * @param placeholder the optional placeholder to be displayed when the text field is in focus and
492 * the input text is empty. The default text style for internal [Text] is [Typography.subtitle1]
493 * @param leadingIcon the optional leading icon to be displayed at the beginning of the text field
494 * container
495 * @param trailingIcon the optional trailing icon to be displayed at the end of the text field
496 * container
497 * @param isError indicates if the text field's current value is in error state. If set to true, the
498 * label, bottom indicator and trailing icon by default will be displayed in error color
499 * @param visualTransformation transforms the visual representation of the input [value] For
500 * example, you can use
501 * [PasswordVisualTransformation][androidx.compose.ui.text.input.PasswordVisualTransformation] to
502 * create a password text field. By default no visual transformation is applied
503 * @param keyboardOptions software keyboard options that contains configuration such as
504 * [KeyboardType] and [ImeAction]
505 * @param keyboardActions when the input service emits an IME action, the corresponding callback is
506 * called. Note that this IME action may be different from what you specified in
507 * [KeyboardOptions.imeAction]
508 * @param singleLine when set to true, this text field becomes a single horizontally scrolling text
509 * field instead of wrapping onto multiple lines. The keyboard will be informed to not show the
510 * return key as the [ImeAction]. Note that [maxLines] parameter will be ignored as the maxLines
511 * attribute will be automatically set to 1
512 * @param maxLines the maximum height in terms of maximum number of visible lines. It is required
513 * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true.
514 * @param minLines the minimum height in terms of minimum number of visible lines. It is required
515 * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true.
516 * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and
517 * emitting [Interaction]s for this text field. You can use this to change the text field's
518 * appearance or preview the text field in different states. Note that if `null` is provided,
519 * interactions will still happen internally.
520 * @param shape the shape of the text field's border
521 * @param colors [TextFieldColors] that will be used to resolve color of the text and content
522 * (including label, placeholder, leading and trailing icons, border) for this text field in
523 * different states. See [TextFieldDefaults.outlinedTextFieldColors]
524 */
525 @Composable
OutlinedTextFieldnull526 fun OutlinedTextField(
527 value: TextFieldValue,
528 onValueChange: (TextFieldValue) -> Unit,
529 modifier: Modifier = Modifier,
530 enabled: Boolean = true,
531 readOnly: Boolean = false,
532 textStyle: TextStyle = LocalTextStyle.current,
533 label: @Composable (() -> Unit)? = null,
534 placeholder: @Composable (() -> Unit)? = null,
535 leadingIcon: @Composable (() -> Unit)? = null,
536 trailingIcon: @Composable (() -> Unit)? = null,
537 isError: Boolean = false,
538 visualTransformation: VisualTransformation = VisualTransformation.None,
539 keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
540 keyboardActions: KeyboardActions = KeyboardActions(),
541 singleLine: Boolean = false,
542 maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
543 minLines: Int = 1,
544 interactionSource: MutableInteractionSource? = null,
545 shape: Shape = TextFieldDefaults.OutlinedTextFieldShape,
546 colors: TextFieldColors = TextFieldDefaults.outlinedTextFieldColors()
547 ) {
548 @Suppress("NAME_SHADOWING")
549 val interactionSource = interactionSource ?: remember { MutableInteractionSource() }
550 // If color is not provided via the text style, use content color as a default
551 val textColor = textStyle.color.takeOrElse { colors.textColor(enabled).value }
552 val mergedTextStyle = textStyle.merge(TextStyle(color = textColor))
553
554 val density = LocalDensity.current
555
556 BasicTextField(
557 value = value,
558 modifier =
559 modifier
560 .then(
561 if (label != null) {
562 Modifier
563 // Merge semantics at the beginning of the modifier chain to ensure
564 // padding is considered part of the text field.
565 .semantics(mergeDescendants = true) {}
566 .padding(top = with(density) { OutlinedTextFieldTopPadding.toDp() })
567 } else {
568 Modifier
569 }
570 )
571 .defaultErrorSemantics(isError, getString(Strings.DefaultErrorMessage))
572 .defaultMinSize(
573 minWidth = TextFieldDefaults.MinWidth,
574 minHeight = TextFieldDefaults.MinHeight
575 ),
576 onValueChange = onValueChange,
577 enabled = enabled,
578 readOnly = readOnly,
579 textStyle = mergedTextStyle,
580 cursorBrush = SolidColor(colors.cursorColor(isError).value),
581 visualTransformation = visualTransformation,
582 keyboardOptions = keyboardOptions,
583 keyboardActions = keyboardActions,
584 interactionSource = interactionSource,
585 singleLine = singleLine,
586 maxLines = maxLines,
587 minLines = minLines,
588 decorationBox =
589 @Composable { innerTextField ->
590 TextFieldDefaults.OutlinedTextFieldDecorationBox(
591 value = value.text,
592 visualTransformation = visualTransformation,
593 innerTextField = innerTextField,
594 placeholder = placeholder,
595 label = label,
596 leadingIcon = leadingIcon,
597 trailingIcon = trailingIcon,
598 singleLine = singleLine,
599 enabled = enabled,
600 isError = isError,
601 interactionSource = interactionSource,
602 shape = shape,
603 colors = colors,
604 border = {
605 TextFieldDefaults.BorderBox(
606 enabled,
607 isError,
608 interactionSource,
609 colors,
610 shape
611 )
612 }
613 )
614 }
615 )
616 }
617
618 @Deprecated(
619 "Maintained for binary compatibility. Use version with minLines instead",
620 level = DeprecationLevel.HIDDEN
621 )
622 @Composable
OutlinedTextFieldnull623 fun OutlinedTextField(
624 value: TextFieldValue,
625 onValueChange: (TextFieldValue) -> Unit,
626 modifier: Modifier = Modifier,
627 enabled: Boolean = true,
628 readOnly: Boolean = false,
629 textStyle: TextStyle = LocalTextStyle.current,
630 label: @Composable (() -> Unit)? = null,
631 placeholder: @Composable (() -> Unit)? = null,
632 leadingIcon: @Composable (() -> Unit)? = null,
633 trailingIcon: @Composable (() -> Unit)? = null,
634 isError: Boolean = false,
635 visualTransformation: VisualTransformation = VisualTransformation.None,
636 keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
637 keyboardActions: KeyboardActions = KeyboardActions(),
638 singleLine: Boolean = false,
639 maxLines: Int = Int.MAX_VALUE,
640 interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
641 shape: Shape = TextFieldDefaults.OutlinedTextFieldShape,
642 colors: TextFieldColors = TextFieldDefaults.outlinedTextFieldColors()
643 ) {
644 OutlinedTextField(
645 value,
646 onValueChange,
647 modifier,
648 enabled,
649 readOnly,
650 textStyle,
651 label,
652 placeholder,
653 leadingIcon,
654 trailingIcon,
655 isError,
656 visualTransformation,
657 keyboardOptions,
658 keyboardActions,
659 singleLine,
660 maxLines,
661 1,
662 interactionSource,
663 shape,
664 colors
665 )
666 }
667
668 /**
669 * Layout of the leading and trailing icons and the text field, label and placeholder in
670 * [OutlinedTextField]. It doesn't use Row to position the icons and middle part because label
671 * should not be positioned in the middle part. \
672 */
673 @Composable
674 internal fun OutlinedTextFieldLayout(
675 modifier: Modifier,
676 textField: @Composable () -> Unit,
677 placeholder: @Composable ((Modifier) -> Unit)?,
678 label: @Composable (() -> Unit)?,
679 leading: @Composable (() -> Unit)?,
680 trailing: @Composable (() -> Unit)?,
681 singleLine: Boolean,
682 animationProgress: Float,
683 onLabelMeasured: (Size) -> Unit,
684 border: @Composable () -> Unit,
685 paddingValues: PaddingValues
686 ) {
687 val measurePolicy =
<lambda>null688 remember(onLabelMeasured, singleLine, animationProgress, paddingValues) {
689 OutlinedTextFieldMeasurePolicy(
690 onLabelMeasured,
691 singleLine,
692 animationProgress,
693 paddingValues
694 )
695 }
696 val layoutDirection = LocalLayoutDirection.current
697 Layout(
698 modifier = modifier,
<lambda>null699 content = {
700 // We use additional box here to place an outlined cutout border as a sibling after the
701 // rest of UI. This allows us to use Modifier.border to draw an outline on top of the
702 // text field. We can't use the border modifier directly on the IconsWithTextFieldLayout
703 // as we also need to do the clipping (to form the cutout) which should not affect
704 // the rest of text field UI
705 border()
706
707 if (leading != null) {
708 Box(
709 modifier = Modifier.layoutId(LeadingId).minimumInteractiveComponentSize(),
710 contentAlignment = Alignment.Center
711 ) {
712 leading()
713 }
714 }
715 if (trailing != null) {
716 Box(
717 modifier = Modifier.layoutId(TrailingId).minimumInteractiveComponentSize(),
718 contentAlignment = Alignment.Center
719 ) {
720 trailing()
721 }
722 }
723
724 val startTextFieldPadding = paddingValues.calculateStartPadding(layoutDirection)
725 val endTextFieldPadding = paddingValues.calculateEndPadding(layoutDirection)
726 val padding =
727 Modifier.padding(
728 start =
729 if (leading != null) {
730 (startTextFieldPadding - HorizontalIconPadding).coerceAtLeast(0.dp)
731 } else {
732 startTextFieldPadding
733 },
734 end =
735 if (trailing != null) {
736 (endTextFieldPadding - HorizontalIconPadding).coerceAtLeast(0.dp)
737 } else {
738 endTextFieldPadding
739 }
740 )
741 if (placeholder != null) {
742 placeholder(Modifier.layoutId(PlaceholderId).then(padding))
743 }
744
745 Box(
746 modifier = Modifier.layoutId(TextFieldId).then(padding),
747 propagateMinConstraints = true
748 ) {
749 textField()
750 }
751
752 if (label != null) {
753 Box(modifier = Modifier.layoutId(LabelId)) { label() }
754 }
755 },
756 measurePolicy = measurePolicy
757 )
758 }
759
760 private class OutlinedTextFieldMeasurePolicy(
761 private val onLabelMeasured: (Size) -> Unit,
762 private val singleLine: Boolean,
763 private val animationProgress: Float,
764 private val paddingValues: PaddingValues
765 ) : MeasurePolicy {
measurenull766 override fun MeasureScope.measure(
767 measurables: List<Measurable>,
768 constraints: Constraints
769 ): MeasureResult {
770 // used to calculate the constraints for measuring elements that will be placed in a row
771 var occupiedSpaceHorizontally = 0
772 val bottomPadding = paddingValues.calculateBottomPadding().roundToPx()
773
774 // measure leading icon
775 val relaxedConstraints = constraints.copy(minWidth = 0, minHeight = 0)
776 val leadingPlaceable =
777 measurables.fastFirstOrNull { it.layoutId == LeadingId }?.measure(relaxedConstraints)
778 occupiedSpaceHorizontally += widthOrZero(leadingPlaceable)
779
780 // measure trailing icon
781 val trailingPlaceable =
782 measurables
783 .fastFirstOrNull { it.layoutId == TrailingId }
784 ?.measure(relaxedConstraints.offset(horizontal = -occupiedSpaceHorizontally))
785 occupiedSpaceHorizontally += widthOrZero(trailingPlaceable)
786
787 // measure label
788 val labelHorizontalPaddingOffset =
789 paddingValues.calculateLeftPadding(layoutDirection).roundToPx() +
790 paddingValues.calculateRightPadding(layoutDirection).roundToPx()
791 val labelConstraints =
792 relaxedConstraints.offset(
793 horizontal =
794 lerp(
795 -occupiedSpaceHorizontally - labelHorizontalPaddingOffset,
796 -labelHorizontalPaddingOffset,
797 animationProgress,
798 ),
799 vertical = -bottomPadding
800 )
801 val labelPlaceable =
802 measurables.fastFirstOrNull { it.layoutId == LabelId }?.measure(labelConstraints)
803 val labelSize =
804 labelPlaceable?.let { Size(it.width.toFloat(), it.height.toFloat()) } ?: Size.Zero
805 onLabelMeasured(labelSize)
806
807 // measure text field
808 // on top we offset either by default padding or by label's half height if its too big
809 // minHeight must not be set to 0 due to how foundation TextField treats zero minHeight
810 val topPadding =
811 max(heightOrZero(labelPlaceable) / 2, paddingValues.calculateTopPadding().roundToPx())
812 val textConstraints =
813 constraints
814 .offset(
815 horizontal = -occupiedSpaceHorizontally,
816 vertical = -bottomPadding - topPadding
817 )
818 .copy(minHeight = 0)
819 val textFieldPlaceable =
820 measurables.fastFirst { it.layoutId == TextFieldId }.measure(textConstraints)
821
822 // measure placeholder
823 val placeholderConstraints = textConstraints.copy(minWidth = 0)
824 val placeholderPlaceable =
825 measurables
826 .fastFirstOrNull { it.layoutId == PlaceholderId }
827 ?.measure(placeholderConstraints)
828
829 val width =
830 calculateWidth(
831 leadingPlaceableWidth = widthOrZero(leadingPlaceable),
832 trailingPlaceableWidth = widthOrZero(trailingPlaceable),
833 textFieldPlaceableWidth = textFieldPlaceable.width,
834 labelPlaceableWidth = widthOrZero(labelPlaceable),
835 placeholderPlaceableWidth = widthOrZero(placeholderPlaceable),
836 animationProgress = animationProgress,
837 constraints = constraints,
838 density = density,
839 paddingValues = paddingValues,
840 )
841 val height =
842 calculateHeight(
843 leadingPlaceableHeight = heightOrZero(leadingPlaceable),
844 trailingPlaceableHeight = heightOrZero(trailingPlaceable),
845 textFieldPlaceableHeight = textFieldPlaceable.height,
846 labelPlaceableHeight = heightOrZero(labelPlaceable),
847 placeholderPlaceableHeight = heightOrZero(placeholderPlaceable),
848 animationProgress = animationProgress,
849 constraints = constraints,
850 density = density,
851 paddingValues = paddingValues,
852 )
853
854 val borderPlaceable =
855 measurables
856 .fastFirst { it.layoutId == BorderId }
857 .measure(
858 Constraints(
859 minWidth = if (width != Constraints.Infinity) width else 0,
860 maxWidth = width,
861 minHeight = if (height != Constraints.Infinity) height else 0,
862 maxHeight = height
863 )
864 )
865 return layout(width, height) {
866 place(
867 height,
868 width,
869 leadingPlaceable,
870 trailingPlaceable,
871 textFieldPlaceable,
872 labelPlaceable,
873 placeholderPlaceable,
874 borderPlaceable,
875 animationProgress,
876 singleLine,
877 density,
878 layoutDirection,
879 paddingValues
880 )
881 }
882 }
883
maxIntrinsicHeightnull884 override fun IntrinsicMeasureScope.maxIntrinsicHeight(
885 measurables: List<IntrinsicMeasurable>,
886 width: Int
887 ): Int {
888 return intrinsicHeight(measurables, width) { intrinsicMeasurable, w ->
889 intrinsicMeasurable.maxIntrinsicHeight(w)
890 }
891 }
892
minIntrinsicHeightnull893 override fun IntrinsicMeasureScope.minIntrinsicHeight(
894 measurables: List<IntrinsicMeasurable>,
895 width: Int
896 ): Int {
897 return intrinsicHeight(measurables, width) { intrinsicMeasurable, w ->
898 intrinsicMeasurable.minIntrinsicHeight(w)
899 }
900 }
901
maxIntrinsicWidthnull902 override fun IntrinsicMeasureScope.maxIntrinsicWidth(
903 measurables: List<IntrinsicMeasurable>,
904 height: Int
905 ): Int {
906 return intrinsicWidth(measurables, height) { intrinsicMeasurable, h ->
907 intrinsicMeasurable.maxIntrinsicWidth(h)
908 }
909 }
910
minIntrinsicWidthnull911 override fun IntrinsicMeasureScope.minIntrinsicWidth(
912 measurables: List<IntrinsicMeasurable>,
913 height: Int
914 ): Int {
915 return intrinsicWidth(measurables, height) { intrinsicMeasurable, h ->
916 intrinsicMeasurable.minIntrinsicWidth(h)
917 }
918 }
919
intrinsicWidthnull920 private fun IntrinsicMeasureScope.intrinsicWidth(
921 measurables: List<IntrinsicMeasurable>,
922 height: Int,
923 intrinsicMeasurer: (IntrinsicMeasurable, Int) -> Int
924 ): Int {
925 val textFieldWidth =
926 intrinsicMeasurer(measurables.fastFirst { it.layoutId == TextFieldId }, height)
927 val labelWidth =
928 measurables
929 .fastFirstOrNull { it.layoutId == LabelId }
930 ?.let { intrinsicMeasurer(it, height) } ?: 0
931 val trailingWidth =
932 measurables
933 .fastFirstOrNull { it.layoutId == TrailingId }
934 ?.let { intrinsicMeasurer(it, height) } ?: 0
935 val leadingWidth =
936 measurables
937 .fastFirstOrNull { it.layoutId == LeadingId }
938 ?.let { intrinsicMeasurer(it, height) } ?: 0
939 val placeholderWidth =
940 measurables
941 .fastFirstOrNull { it.layoutId == PlaceholderId }
942 ?.let { intrinsicMeasurer(it, height) } ?: 0
943 return calculateWidth(
944 leadingPlaceableWidth = leadingWidth,
945 trailingPlaceableWidth = trailingWidth,
946 textFieldPlaceableWidth = textFieldWidth,
947 labelPlaceableWidth = labelWidth,
948 placeholderPlaceableWidth = placeholderWidth,
949 animationProgress = animationProgress,
950 constraints = Constraints(),
951 density = density,
952 paddingValues = paddingValues,
953 )
954 }
955
intrinsicHeightnull956 private fun IntrinsicMeasureScope.intrinsicHeight(
957 measurables: List<IntrinsicMeasurable>,
958 width: Int,
959 intrinsicMeasurer: (IntrinsicMeasurable, Int) -> Int
960 ): Int {
961 var remainingWidth = width
962 val leadingHeight =
963 measurables
964 .fastFirstOrNull { it.layoutId == LeadingId }
965 ?.let {
966 remainingWidth =
967 remainingWidth.subtractConstraintSafely(
968 it.maxIntrinsicWidth(Constraints.Infinity)
969 )
970 intrinsicMeasurer(it, width)
971 } ?: 0
972 val trailingHeight =
973 measurables
974 .fastFirstOrNull { it.layoutId == TrailingId }
975 ?.let {
976 remainingWidth =
977 remainingWidth.subtractConstraintSafely(
978 it.maxIntrinsicWidth(Constraints.Infinity)
979 )
980 intrinsicMeasurer(it, width)
981 } ?: 0
982
983 val labelHeight =
984 measurables
985 .fastFirstOrNull { it.layoutId == LabelId }
986 ?.let { intrinsicMeasurer(it, lerp(remainingWidth, width, animationProgress)) } ?: 0
987
988 val textFieldHeight =
989 intrinsicMeasurer(measurables.fastFirst { it.layoutId == TextFieldId }, remainingWidth)
990 val placeholderHeight =
991 measurables
992 .fastFirstOrNull { it.layoutId == PlaceholderId }
993 ?.let { intrinsicMeasurer(it, remainingWidth) } ?: 0
994
995 return calculateHeight(
996 leadingPlaceableHeight = leadingHeight,
997 trailingPlaceableHeight = trailingHeight,
998 textFieldPlaceableHeight = textFieldHeight,
999 labelPlaceableHeight = labelHeight,
1000 placeholderPlaceableHeight = placeholderHeight,
1001 animationProgress = animationProgress,
1002 constraints = Constraints(),
1003 density = density,
1004 paddingValues = paddingValues
1005 )
1006 }
1007 }
1008
1009 /**
1010 * Calculate the width of the [OutlinedTextField] given all elements that should be placed inside
1011 */
calculateWidthnull1012 private fun calculateWidth(
1013 leadingPlaceableWidth: Int,
1014 trailingPlaceableWidth: Int,
1015 textFieldPlaceableWidth: Int,
1016 labelPlaceableWidth: Int,
1017 placeholderPlaceableWidth: Int,
1018 animationProgress: Float,
1019 constraints: Constraints,
1020 density: Float,
1021 paddingValues: PaddingValues,
1022 ): Int {
1023 val middleSection =
1024 maxOf(
1025 textFieldPlaceableWidth,
1026 lerp(labelPlaceableWidth, 0, animationProgress),
1027 placeholderPlaceableWidth
1028 )
1029 val wrappedWidth = leadingPlaceableWidth + middleSection + trailingPlaceableWidth
1030
1031 // Actual LayoutDirection doesn't matter; we only need the sum
1032 val labelHorizontalPadding =
1033 (paddingValues.calculateLeftPadding(LayoutDirection.Ltr) +
1034 paddingValues.calculateRightPadding(LayoutDirection.Ltr))
1035 .value * density
1036 val focusedLabelWidth =
1037 ((labelPlaceableWidth + labelHorizontalPadding) * animationProgress).roundToInt()
1038
1039 return constraints.constrainWidth(max(wrappedWidth, focusedLabelWidth))
1040 }
1041
1042 /**
1043 * Calculate the height of the [OutlinedTextField] given all elements that should be placed inside
1044 */
calculateHeightnull1045 private fun calculateHeight(
1046 leadingPlaceableHeight: Int,
1047 trailingPlaceableHeight: Int,
1048 textFieldPlaceableHeight: Int,
1049 labelPlaceableHeight: Int,
1050 placeholderPlaceableHeight: Int,
1051 animationProgress: Float,
1052 constraints: Constraints,
1053 density: Float,
1054 paddingValues: PaddingValues
1055 ): Int {
1056 val inputFieldHeight =
1057 maxOf(
1058 textFieldPlaceableHeight,
1059 placeholderPlaceableHeight,
1060 lerp(labelPlaceableHeight, 0, animationProgress),
1061 )
1062 val topPadding = paddingValues.calculateTopPadding().value * density
1063 val actualTopPadding =
1064 lerp(
1065 topPadding,
1066 max(topPadding, labelPlaceableHeight / 2f),
1067 animationProgress,
1068 )
1069 val bottomPadding = paddingValues.calculateBottomPadding().value * density
1070 val middleSectionHeight = actualTopPadding + inputFieldHeight + bottomPadding
1071
1072 return constraints.constrainHeight(
1073 maxOf(leadingPlaceableHeight, trailingPlaceableHeight, middleSectionHeight.roundToInt())
1074 )
1075 }
1076
1077 /**
1078 * Places the provided text field, placeholder, label, optional leading and trailing icons inside
1079 * the [OutlinedTextField]
1080 */
Placeablenull1081 private fun Placeable.PlacementScope.place(
1082 height: Int,
1083 width: Int,
1084 leadingPlaceable: Placeable?,
1085 trailingPlaceable: Placeable?,
1086 textFieldPlaceable: Placeable,
1087 labelPlaceable: Placeable?,
1088 placeholderPlaceable: Placeable?,
1089 borderPlaceable: Placeable,
1090 animationProgress: Float,
1091 singleLine: Boolean,
1092 density: Float,
1093 layoutDirection: LayoutDirection,
1094 paddingValues: PaddingValues
1095 ) {
1096 val topPadding = (paddingValues.calculateTopPadding().value * density).roundToInt()
1097 val startPadding =
1098 (paddingValues.calculateStartPadding(layoutDirection).value * density).roundToInt()
1099
1100 val iconPadding = HorizontalIconPadding.value * density
1101
1102 // placed center vertically and to the start edge horizontally
1103 leadingPlaceable?.placeRelative(
1104 0,
1105 Alignment.CenterVertically.align(leadingPlaceable.height, height)
1106 )
1107
1108 // placed center vertically and to the end edge horizontally
1109 trailingPlaceable?.placeRelative(
1110 width - trailingPlaceable.width,
1111 Alignment.CenterVertically.align(trailingPlaceable.height, height)
1112 )
1113
1114 // label position is animated
1115 // in single line text field label is centered vertically before animation starts
1116 labelPlaceable?.let {
1117 val startPositionY =
1118 if (singleLine) {
1119 Alignment.CenterVertically.align(it.height, height)
1120 } else {
1121 topPadding
1122 }
1123 val positionY = lerp(startPositionY, -(it.height / 2), animationProgress)
1124 val positionX =
1125 (if (leadingPlaceable == null) {
1126 0f
1127 } else {
1128 (widthOrZero(leadingPlaceable) - iconPadding) * (1 - animationProgress)
1129 })
1130 .roundToInt() + startPadding
1131 it.placeRelative(positionX, positionY)
1132 }
1133
1134 // placed center vertically and after the leading icon horizontally if single line text field
1135 // placed to the top with padding for multi line text field
1136 val textVerticalPosition =
1137 max(
1138 if (singleLine) {
1139 Alignment.CenterVertically.align(textFieldPlaceable.height, height)
1140 } else {
1141 topPadding
1142 },
1143 heightOrZero(labelPlaceable) / 2
1144 )
1145 textFieldPlaceable.placeRelative(widthOrZero(leadingPlaceable), textVerticalPosition)
1146
1147 // placed similar to the input text above
1148 placeholderPlaceable?.let {
1149 val placeholderVerticalPosition =
1150 max(
1151 if (singleLine) {
1152 Alignment.CenterVertically.align(it.height, height)
1153 } else {
1154 topPadding
1155 },
1156 heightOrZero(labelPlaceable) / 2
1157 )
1158 it.placeRelative(widthOrZero(leadingPlaceable), placeholderVerticalPosition)
1159 }
1160
1161 // place border
1162 borderPlaceable.place(IntOffset.Zero)
1163 }
1164
outlineCutoutnull1165 internal fun Modifier.outlineCutout(labelSize: Size, paddingValues: PaddingValues) =
1166 this.drawWithContent {
1167 val labelWidth = labelSize.width
1168 if (labelWidth > 0f) {
1169 val innerPadding = OutlinedTextFieldInnerPadding.toPx()
1170 val leftLtr = paddingValues.calculateLeftPadding(layoutDirection).toPx() - innerPadding
1171 val rightLtr = leftLtr + labelWidth + 2 * innerPadding
1172 val left =
1173 when (layoutDirection) {
1174 LayoutDirection.Rtl -> size.width - rightLtr
1175 else -> leftLtr.coerceAtLeast(0f)
1176 }
1177 val right =
1178 when (layoutDirection) {
1179 LayoutDirection.Rtl -> size.width - leftLtr.coerceAtLeast(0f)
1180 else -> rightLtr
1181 }
1182 val labelHeight = labelSize.height
1183 // using label height as a cutout area to make sure that no hairline artifacts are
1184 // left when we clip the border
1185 clipRect(left, -labelHeight / 2, right, labelHeight / 2, ClipOp.Difference) {
1186 this@drawWithContent.drawContent()
1187 }
1188 } else {
1189 this@drawWithContent.drawContent()
1190 }
1191 }
1192
1193 private val OutlinedTextFieldInnerPadding = 4.dp
1194
1195 /**
1196 * In the focused state, the top half of the label sticks out above the text field. This default
1197 * padding is a best-effort approximation to keep the label from overlapping with the content above
1198 * it. It is sufficient when the label is a single line and developers do not override the label's
1199 * font size/style. Otherwise, developers will need to add additional padding themselves.
1200 */
1201 internal val OutlinedTextFieldTopPadding = 8.sp
1202
1203 internal const val BorderId = "border"
1204