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