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.animation.animateColor
20 import androidx.compose.animation.core.LinearEasing
21 import androidx.compose.animation.core.animateFloat
22 import androidx.compose.animation.core.spring
23 import androidx.compose.animation.core.tween
24 import androidx.compose.animation.core.updateTransition
25 import androidx.compose.foundation.background
26 import androidx.compose.foundation.interaction.InteractionSource
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.runtime.Composable
31 import androidx.compose.runtime.ComposableOpenTarget
32 import androidx.compose.runtime.CompositionLocalProvider
33 import androidx.compose.runtime.getValue
34 import androidx.compose.runtime.mutableStateOf
35 import androidx.compose.runtime.remember
36 import androidx.compose.ui.Modifier
37 import androidx.compose.ui.draw.alpha
38 import androidx.compose.ui.geometry.Size
39 import androidx.compose.ui.graphics.Color
40 import androidx.compose.ui.graphics.Shape
41 import androidx.compose.ui.graphics.takeOrElse
42 import androidx.compose.ui.layout.IntrinsicMeasurable
43 import androidx.compose.ui.layout.LayoutIdParentData
44 import androidx.compose.ui.layout.Placeable
45 import androidx.compose.ui.layout.layoutId
46 import androidx.compose.ui.semantics.error
47 import androidx.compose.ui.semantics.semantics
48 import androidx.compose.ui.text.AnnotatedString
49 import androidx.compose.ui.text.TextStyle
50 import androidx.compose.ui.text.input.VisualTransformation
51 import androidx.compose.ui.text.lerp
52 import androidx.compose.ui.unit.dp
53
54 internal enum class TextFieldType {
55 Filled,
56 Outlined
57 }
58
59 /** Implementation of the [TextField] and [OutlinedTextField] */
60 @OptIn(ExperimentalMaterialApi::class)
61 @Composable
62 internal fun CommonDecorationBox(
63 type: TextFieldType,
64 value: String,
65 innerTextField: @Composable () -> Unit,
66 visualTransformation: VisualTransformation,
67 label: @Composable (() -> Unit)?,
68 placeholder: @Composable (() -> Unit)?,
69 leadingIcon: @Composable (() -> Unit)?,
70 trailingIcon: @Composable (() -> Unit)?,
71 singleLine: Boolean,
72 enabled: Boolean,
73 isError: Boolean,
74 interactionSource: InteractionSource,
75 contentPadding: PaddingValues,
76 shape: Shape,
77 colors: TextFieldColors,
78 border: @Composable (() -> Unit)?,
79 ) {
80 val transformedText =
<lambda>null81 remember(value, visualTransformation) {
82 visualTransformation.filter(AnnotatedString(value))
83 }
84 .text
85 .text
86
87 val isFocused = interactionSource.collectIsFocusedAsState().value
88 val inputState =
89 when {
90 isFocused -> InputPhase.Focused
91 transformedText.isEmpty() -> InputPhase.UnfocusedEmpty
92 else -> InputPhase.UnfocusedNotEmpty
93 }
94
95 val labelColor: @Composable (InputPhase) -> Color = {
96 colors
97 .labelColor(
98 enabled,
99 // if label is used as a placeholder (aka not as a small header
100 // at the top), we don't use an error color
101 if (it == InputPhase.UnfocusedEmpty) false else isError,
102 interactionSource
103 )
104 .value
105 }
106
107 val typography = MaterialTheme.typography
108 val subtitle1 = typography.subtitle1
109 val caption = typography.caption
110 val shouldOverrideTextStyleColor =
111 (subtitle1.color == Color.Unspecified && caption.color != Color.Unspecified) ||
112 (subtitle1.color != Color.Unspecified && caption.color == Color.Unspecified)
113
114 TextFieldTransitionScope.Transition(
115 inputState = inputState,
116 focusedTextStyleColor =
117 with(MaterialTheme.typography.caption.color) {
<lambda>null118 if (shouldOverrideTextStyleColor) this.takeOrElse { labelColor(inputState) }
119 else this
120 },
121 unfocusedTextStyleColor =
122 with(MaterialTheme.typography.subtitle1.color) {
<lambda>null123 if (shouldOverrideTextStyleColor) this.takeOrElse { labelColor(inputState) }
124 else this
125 },
126 contentColor = labelColor,
127 showLabel = label != null
128 ) { labelProgress, labelTextStyleColor, labelContentColor, placeholderAlphaProgress ->
129 val decoratedLabel: @Composable (() -> Unit)? =
<lambda>null130 label?.let {
131 @Composable {
132 val labelTextStyle =
133 lerp(
134 MaterialTheme.typography.subtitle1,
135 MaterialTheme.typography.caption,
136 labelProgress
137 )
138 .let {
139 if (shouldOverrideTextStyleColor)
140 it.copy(color = labelTextStyleColor)
141 else it
142 }
143 Decoration(labelContentColor, labelTextStyle, null, it)
144 }
145 }
146
147 // Transparent components interfere with Talkback (b/261061240), so if the placeholder has
148 // alpha == 0, we set the component to null instead.
149 val decoratedPlaceholder: @Composable ((Modifier) -> Unit)? =
150 if (placeholder != null && transformedText.isEmpty() && placeholderAlphaProgress > 0f) {
modifiernull151 @Composable { modifier ->
152 Box(modifier.alpha(placeholderAlphaProgress)) {
153 Decoration(
154 contentColor = colors.placeholderColor(enabled).value,
155 typography = MaterialTheme.typography.subtitle1,
156 content = placeholder
157 )
158 }
159 }
160 } else null
161
162 val leadingIconColor = colors.leadingIconColor(enabled, isError, interactionSource).value
163 val decoratedLeading: @Composable (() -> Unit)? =
<lambda>null164 leadingIcon?.let {
165 @Composable { Decoration(contentColor = leadingIconColor, content = it) }
166 }
167
168 val trailingIconColor = colors.trailingIconColor(enabled, isError, interactionSource).value
169 val decoratedTrailing: @Composable (() -> Unit)? =
<lambda>null170 trailingIcon?.let {
171 @Composable { Decoration(contentColor = trailingIconColor, content = it) }
172 }
173
174 val backgroundModifier = Modifier.background(colors.backgroundColor(enabled).value, shape)
175
176 when (type) {
177 TextFieldType.Filled -> {
178 TextFieldLayout(
179 modifier = backgroundModifier,
180 textField = innerTextField,
181 placeholder = decoratedPlaceholder,
182 label = decoratedLabel,
183 leading = decoratedLeading,
184 trailing = decoratedTrailing,
185 singleLine = singleLine,
186 animationProgress = labelProgress,
187 paddingValues = contentPadding
188 )
189 }
190 TextFieldType.Outlined -> {
191 // Outlined cutout
<lambda>null192 val labelSize = remember { mutableStateOf(Size.Zero) }
193 val drawBorder: @Composable () -> Unit = {
194 Box(
195 Modifier.layoutId(BorderId).outlineCutout(labelSize.value, contentPadding),
196 propagateMinConstraints = true
<lambda>null197 ) {
198 border?.invoke()
199 }
200 }
201
202 OutlinedTextFieldLayout(
203 modifier = backgroundModifier,
204 textField = innerTextField,
205 placeholder = decoratedPlaceholder,
206 label = decoratedLabel,
207 leading = decoratedLeading,
208 trailing = decoratedTrailing,
209 singleLine = singleLine,
<lambda>null210 onLabelMeasured = {
211 val labelWidth = it.width * labelProgress
212 val labelHeight = it.height * labelProgress
213 if (
214 labelSize.value.width != labelWidth ||
215 labelSize.value.height != labelHeight
216 ) {
217 labelSize.value = Size(labelWidth, labelHeight)
218 }
219 },
220 animationProgress = labelProgress,
221 border = drawBorder,
222 paddingValues = contentPadding
223 )
224 }
225 }
226 }
227 }
228
229 /** Set content color, typography and emphasis for [content] composable */
230 @Composable
231 @ComposableOpenTarget(index = 0)
Decorationnull232 internal fun Decoration(
233 contentColor: Color,
234 typography: TextStyle? = null,
235 contentAlpha: Float? = null,
236 content: @Composable @ComposableOpenTarget(index = 0) () -> Unit
237 ) {
238 val colorAndEmphasis: @Composable () -> Unit =
239 @Composable {
240 CompositionLocalProvider(LocalContentColor provides contentColor) {
241 if (contentAlpha != null) {
242 CompositionLocalProvider(
243 LocalContentAlpha provides contentAlpha,
244 content = content
245 )
246 } else {
247 CompositionLocalProvider(
248 LocalContentAlpha provides contentColor.alpha,
249 content = content
250 )
251 }
252 }
253 }
254 if (typography != null) ProvideTextStyle(typography, colorAndEmphasis) else colorAndEmphasis()
255 }
256
257 // Developers need to handle invalid input manually. But since we don't provide an error message
258 // slot API, we can set the default error message in case developers forget about it.
defaultErrorSemanticsnull259 internal fun Modifier.defaultErrorSemantics(
260 isError: Boolean,
261 defaultErrorMessage: String,
262 ): Modifier = if (isError) semantics { error(defaultErrorMessage) } else this
263
widthOrZeronull264 internal fun widthOrZero(placeable: Placeable?) = placeable?.width ?: 0
265
266 internal fun heightOrZero(placeable: Placeable?) = placeable?.height ?: 0
267
268 private object TextFieldTransitionScope {
269 @Composable
270 fun Transition(
271 inputState: InputPhase,
272 focusedTextStyleColor: Color,
273 unfocusedTextStyleColor: Color,
274 contentColor: @Composable (InputPhase) -> Color,
275 showLabel: Boolean,
276 content:
277 @Composable
278 (
279 labelProgress: Float,
280 labelTextStyleColor: Color,
281 labelContentColor: Color,
282 placeholderOpacity: Float
283 ) -> Unit
284 ) {
285 // Transitions from/to InputPhase.Focused are the most critical in the transition below.
286 // UnfocusedEmpty <-> UnfocusedNotEmpty are needed when a single state is used to control
287 // multiple text fields.
288 val transition = updateTransition(inputState, label = "TextFieldInputState")
289
290 val labelProgress by
291 transition.animateFloat(
292 label = "LabelProgress",
293 transitionSpec = { tween(durationMillis = AnimationDuration) }
294 ) {
295 when (it) {
296 InputPhase.Focused -> 1f
297 InputPhase.UnfocusedEmpty -> 0f
298 InputPhase.UnfocusedNotEmpty -> 1f
299 }
300 }
301
302 val placeholderOpacity by
303 transition.animateFloat(
304 label = "PlaceholderOpacity",
305 transitionSpec = {
306 if (InputPhase.Focused isTransitioningTo InputPhase.UnfocusedEmpty) {
307 tween(
308 durationMillis = PlaceholderAnimationDelayOrDuration,
309 easing = LinearEasing
310 )
311 } else if (
312 InputPhase.UnfocusedEmpty isTransitioningTo InputPhase.Focused ||
313 InputPhase.UnfocusedNotEmpty isTransitioningTo InputPhase.UnfocusedEmpty
314 ) {
315 tween(
316 durationMillis = PlaceholderAnimationDuration,
317 delayMillis = PlaceholderAnimationDelayOrDuration,
318 easing = LinearEasing
319 )
320 } else {
321 spring()
322 }
323 }
324 ) {
325 when (it) {
326 InputPhase.Focused -> 1f
327 InputPhase.UnfocusedEmpty -> if (showLabel) 0f else 1f
328 InputPhase.UnfocusedNotEmpty -> 0f
329 }
330 }
331
332 val labelTextStyleColor by
333 transition.animateColor(
334 transitionSpec = { tween(durationMillis = AnimationDuration) },
335 label = "LabelTextStyleColor"
336 ) {
337 when (it) {
338 InputPhase.Focused -> focusedTextStyleColor
339 else -> unfocusedTextStyleColor
340 }
341 }
342
343 val labelContentColor by
344 transition.animateColor(
345 transitionSpec = { tween(durationMillis = AnimationDuration) },
346 label = "LabelContentColor",
347 targetValueByState = contentColor
348 )
349
350 content(labelProgress, labelTextStyleColor, labelContentColor, placeholderOpacity)
351 }
352 }
353
354 /** An internal state used to animate a label and an indicator. */
355 private enum class InputPhase {
356 // Text field is focused
357 Focused,
358
359 // Text field is not focused and input text is empty
360 UnfocusedEmpty,
361
362 // Text field is not focused but input text is not empty
363 UnfocusedNotEmpty
364 }
365
366 internal val IntrinsicMeasurable.layoutId: Any?
367 get() = (parentData as? LayoutIdParentData)?.layoutId
368
369 internal const val TextFieldId = "TextField"
370 internal const val PlaceholderId = "Hint"
371 internal const val LabelId = "Label"
372 internal const val LeadingId = "Leading"
373 internal const val TrailingId = "Trailing"
374
375 internal const val AnimationDuration = 150
376 private const val PlaceholderAnimationDuration = 83
377 private const val PlaceholderAnimationDelayOrDuration = 67
378
379 internal val TextFieldPadding = 16.dp
380 internal val HorizontalIconPadding = 12.dp
381