/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.compose.material
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.interaction.FocusInteraction
import androidx.compose.foundation.interaction.Interaction
import androidx.compose.foundation.interaction.InteractionSource
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.flow.collect
/**
* Represents the colors of the input text, background and content (including label, placeholder,
* leading and trailing icons) used in a text field in different states.
*
* See [TextFieldDefaults.textFieldColors] for the default colors used in [TextField].
* See [TextFieldDefaults.outlinedTextFieldColors] for the default colors used in
* [OutlinedTextField].
*/
@Stable
interface TextFieldColors {
/**
* Represents the color used for the input text of this text field.
*
* @param enabled whether the text field is enabled
*/
@Composable
fun textColor(enabled: Boolean): State<Color>
/**
* Represents the background color for this text field.
*
* @param enabled whether the text field is enabled
*/
@Composable
fun backgroundColor(enabled: Boolean): State<Color>
/**
* Represents the color used for the placeholder of this text field.
*
* @param enabled whether the text field is enabled
*/
@Composable
fun placeholderColor(enabled: Boolean): State<Color>
/**
* Represents the color used for the label of this text field.
*
* @param enabled whether the text field is enabled
* @param error whether the text field's current value is in error
* @param interactionSource the [InteractionSource] of this text field. Helps to determine if
* the text field is in focus or not
*/
@Composable
fun labelColor(
enabled: Boolean,
error: Boolean,
interactionSource: InteractionSource
): State<Color>
/**
* Represents the color used for the leading icon of this text field.
*
* @param enabled whether the text field is enabled
* @param isError whether the text field's current value is in error
*/
@Composable
fun leadingIconColor(enabled: Boolean, isError: Boolean): State<Color>
/**
* Represents the color used for the trailing icon of this text field.
*
* @param enabled whether the text field is enabled
* @param isError whether the text field's current value is in error
*/
@Composable
fun trailingIconColor(enabled: Boolean, isError: Boolean): State<Color>
/**
* Represents the color used for the border indicator of this text field.
*
* @param enabled whether the text field is enabled
* @param isError whether the text field's current value is in error
* @param interactionSource the [InteractionSource] of this text field. Helps to determine if
* the text field is in focus or not
*/
@Composable
fun indicatorColor(
enabled: Boolean,
isError: Boolean,
interactionSource: InteractionSource
): State<Color>
/**
* Represents the color used for the cursor of this text field.
*
* @param isError whether the text field's current value is in error
*/
@Composable
fun cursorColor(isError: Boolean): State<Color>
}
/**
* Contains the default values used by [TextField] and [OutlinedTextField].
*/
object TextFieldDefaults {
/**
* The default min width applied for a [TextField] and [OutlinedTextField].
* Note that you can override it by applying Modifier.heightIn directly on a text field.
*/
val MinHeight = 56.dp
/**
* The default min width applied for a [TextField] and [OutlinedTextField].
* Note that you can override it by applying Modifier.widthIn directly on a text field.
*/
val MinWidth = 280.dp
/**
* The default opacity used for a [TextField]'s and [OutlinedTextField]'s leading and
* trailing icons color.
*/
const val IconOpacity = 0.54f
/**
* The default opacity used for a [TextField]'s background color.
*/
const val BackgroundOpacity = 0.12f
// Filled text field uses 42% opacity to meet the contrast requirements for accessibility reasons
/**
* The default opacity used for a [TextField]'s indicator line color when text field is
* not focused.
*/
const val UnfocusedIndicatorLineOpacity = 0.42f
/**
* Creates a [TextFieldColors] that represents the default input text, background and content
* (including label, placeholder, leading and trailing icons) colors used in a [TextField].
*/
@Composable
fun textFieldColors(
textColor: Color = LocalContentColor.current.copy(LocalContentAlpha.current),
disabledTextColor: Color = textColor.copy(ContentAlpha.disabled),
backgroundColor: Color = MaterialTheme.colors.onSurface.copy(alpha = BackgroundOpacity),
cursorColor: Color = MaterialTheme.colors.primary,
errorCursorColor: Color = MaterialTheme.colors.error,
focusedIndicatorColor: Color =
MaterialTheme.colors.primary.copy(alpha = ContentAlpha.high),
unfocusedIndicatorColor: Color =
MaterialTheme.colors.onSurface.copy(alpha = UnfocusedIndicatorLineOpacity),
disabledIndicatorColor: Color = unfocusedIndicatorColor.copy(alpha = ContentAlpha.disabled),
errorIndicatorColor: Color = MaterialTheme.colors.error,
leadingIconColor: Color =
MaterialTheme.colors.onSurface.copy(alpha = IconOpacity),
disabledLeadingIconColor: Color = leadingIconColor.copy(alpha = ContentAlpha.disabled),
errorLeadingIconColor: Color = leadingIconColor,
trailingIconColor: Color =
MaterialTheme.colors.onSurface.copy(alpha = IconOpacity),
disabledTrailingIconColor: Color = trailingIconColor.copy(alpha = ContentAlpha.disabled),
errorTrailingIconColor: Color = MaterialTheme.colors.error,
focusedLabelColor: Color =
MaterialTheme.colors.primary.copy(alpha = ContentAlpha.high),
unfocusedLabelColor: Color = MaterialTheme.colors.onSurface.copy(ContentAlpha.medium),
disabledLabelColor: Color = unfocusedLabelColor.copy(ContentAlpha.disabled),
errorLabelColor: Color = MaterialTheme.colors.error,
placeholderColor: Color = MaterialTheme.colors.onSurface.copy(ContentAlpha.medium),
disabledPlaceholderColor: Color = placeholderColor.copy(ContentAlpha.disabled)
): TextFieldColors =
DefaultTextFieldColors(
textColor = textColor,
disabledTextColor = disabledTextColor,
cursorColor = cursorColor,
errorCursorColor = errorCursorColor,
focusedIndicatorColor = focusedIndicatorColor,
unfocusedIndicatorColor = unfocusedIndicatorColor,
errorIndicatorColor = errorIndicatorColor,
disabledIndicatorColor = disabledIndicatorColor,
leadingIconColor = leadingIconColor,
disabledLeadingIconColor = disabledLeadingIconColor,
errorLeadingIconColor = errorLeadingIconColor,
trailingIconColor = trailingIconColor,
disabledTrailingIconColor = disabledTrailingIconColor,
errorTrailingIconColor = errorTrailingIconColor,
backgroundColor = backgroundColor,
focusedLabelColor = focusedLabelColor,
unfocusedLabelColor = unfocusedLabelColor,
disabledLabelColor = disabledLabelColor,
errorLabelColor = errorLabelColor,
placeholderColor = placeholderColor,
disabledPlaceholderColor = disabledPlaceholderColor
)
/**
* Creates a [TextFieldColors] that represents the default input text, background and content
* (including label, placeholder, leading and trailing icons) colors used in an
* [OutlinedTextField].
*/
@Composable
fun outlinedTextFieldColors(
textColor: Color = LocalContentColor.current.copy(LocalContentAlpha.current),
disabledTextColor: Color = textColor.copy(ContentAlpha.disabled),
backgroundColor: Color = Color.Transparent,
cursorColor: Color = MaterialTheme.colors.primary,
errorCursorColor: Color = MaterialTheme.colors.error,
focusedBorderColor: Color =
MaterialTheme.colors.primary.copy(alpha = ContentAlpha.high),
unfocusedBorderColor: Color =
MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.disabled),
disabledBorderColor: Color = unfocusedBorderColor.copy(alpha = ContentAlpha.disabled),
errorBorderColor: Color = MaterialTheme.colors.error,
leadingIconColor: Color =
MaterialTheme.colors.onSurface.copy(alpha = IconOpacity),
disabledLeadingIconColor: Color = leadingIconColor.copy(alpha = ContentAlpha.disabled),
errorLeadingIconColor: Color = leadingIconColor,
trailingIconColor: Color =
MaterialTheme.colors.onSurface.copy(alpha = IconOpacity),
disabledTrailingIconColor: Color = trailingIconColor.copy(alpha = ContentAlpha.disabled),
errorTrailingIconColor: Color = MaterialTheme.colors.error,
focusedLabelColor: Color =
MaterialTheme.colors.primary.copy(alpha = ContentAlpha.high),
unfocusedLabelColor: Color = MaterialTheme.colors.onSurface.copy(ContentAlpha.medium),
disabledLabelColor: Color = unfocusedLabelColor.copy(ContentAlpha.disabled),
errorLabelColor: Color = MaterialTheme.colors.error,
placeholderColor: Color = MaterialTheme.colors.onSurface.copy(ContentAlpha.medium),
disabledPlaceholderColor: Color = placeholderColor.copy(ContentAlpha.disabled)
): TextFieldColors =
DefaultTextFieldColors(
textColor = textColor,
disabledTextColor = disabledTextColor,
cursorColor = cursorColor,
errorCursorColor = errorCursorColor,
focusedIndicatorColor = focusedBorderColor,
unfocusedIndicatorColor = unfocusedBorderColor,
errorIndicatorColor = errorBorderColor,
disabledIndicatorColor = disabledBorderColor,
leadingIconColor = leadingIconColor,
disabledLeadingIconColor = disabledLeadingIconColor,
errorLeadingIconColor = errorLeadingIconColor,
trailingIconColor = trailingIconColor,
disabledTrailingIconColor = disabledTrailingIconColor,
errorTrailingIconColor = errorTrailingIconColor,
backgroundColor = backgroundColor,
focusedLabelColor = focusedLabelColor,
unfocusedLabelColor = unfocusedLabelColor,
disabledLabelColor = disabledLabelColor,
errorLabelColor = errorLabelColor,
placeholderColor = placeholderColor,
disabledPlaceholderColor = disabledPlaceholderColor
)
}
@Immutable
private class DefaultTextFieldColors(
private val textColor: Color,
private val disabledTextColor: Color,
private val cursorColor: Color,
private val errorCursorColor: Color,
private val focusedIndicatorColor: Color,
private val unfocusedIndicatorColor: Color,
private val errorIndicatorColor: Color,
private val disabledIndicatorColor: Color,
private val leadingIconColor: Color,
private val disabledLeadingIconColor: Color,
private val errorLeadingIconColor: Color,
private val trailingIconColor: Color,
private val disabledTrailingIconColor: Color,
private val errorTrailingIconColor: Color,
private val backgroundColor: Color,
private val focusedLabelColor: Color,
private val unfocusedLabelColor: Color,
private val disabledLabelColor: Color,
private val errorLabelColor: Color,
private val placeholderColor: Color,
private val disabledPlaceholderColor: Color
) : TextFieldColors {
@Composable
override fun leadingIconColor(enabled: Boolean, isError: Boolean): State<Color> {
return rememberUpdatedState(
when {
!enabled -> disabledLeadingIconColor
isError -> errorLeadingIconColor
else -> leadingIconColor
}
)
}
@Composable
override fun trailingIconColor(enabled: Boolean, isError: Boolean): State<Color> {
return rememberUpdatedState(
when {
!enabled -> disabledTrailingIconColor
isError -> errorTrailingIconColor
else -> trailingIconColor
}
)
}
@Composable
override fun indicatorColor(
enabled: Boolean,
isError: Boolean,
interactionSource: InteractionSource
): State<Color> {
val interactions = remember { mutableStateListOf<Interaction>() }
LaunchedEffect(interactionSource) {
interactionSource.interactions.collect { interaction ->
when (interaction) {
is FocusInteraction.Focus -> {
interactions.add(interaction)
}
is FocusInteraction.Unfocus -> {
interactions.remove(interaction.focus)
}
}
}
}
val interaction = interactions.lastOrNull()
val targetValue = when {
!enabled -> disabledIndicatorColor
isError -> errorIndicatorColor
interaction is FocusInteraction.Focus -> focusedIndicatorColor
else -> unfocusedIndicatorColor
}
return if (enabled) {
animateColorAsState(targetValue, tween(durationMillis = AnimationDuration))
} else {
rememberUpdatedState(targetValue)
}
}
@Composable
override fun backgroundColor(enabled: Boolean): State<Color> {
return rememberUpdatedState(backgroundColor)
}
@Composable
override fun placeholderColor(enabled: Boolean): State<Color> {
return rememberUpdatedState(if (enabled) placeholderColor else disabledPlaceholderColor)
}
@Composable
override fun labelColor(
enabled: Boolean,
error: Boolean,
interactionSource: InteractionSource
): State<Color> {
val interactions = remember { mutableStateListOf<Interaction>() }
LaunchedEffect(interactionSource) {
interactionSource.interactions.collect { interaction ->
when (interaction) {
is FocusInteraction.Focus -> {
interactions.add(interaction)
}
is FocusInteraction.Unfocus -> {
interactions.remove(interaction.focus)
}
}
}
}
val interaction = interactions.lastOrNull()
val targetValue = when {
!enabled -> disabledLabelColor
error -> errorLabelColor
interaction is FocusInteraction.Focus -> focusedLabelColor
else -> unfocusedLabelColor
}
return if (enabled) {
animateColorAsState(targetValue, tween(durationMillis = AnimationDuration))
} else {
rememberUpdatedState(targetValue)
}
}
@Composable
override fun textColor(enabled: Boolean): State<Color> {
return rememberUpdatedState(if (enabled) textColor else disabledTextColor)
}
@Composable
override fun cursorColor(isError: Boolean): State<Color> {
return rememberUpdatedState(if (isError) errorCursorColor else cursorColor)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
other as DefaultTextFieldColors
if (textColor != other.textColor) return false
if (disabledTextColor != other.disabledTextColor) return false
if (cursorColor != other.cursorColor) return false
if (errorCursorColor != other.errorCursorColor) return false
if (focusedIndicatorColor != other.focusedIndicatorColor) return false
if (unfocusedIndicatorColor != other.unfocusedIndicatorColor) return false
if (errorIndicatorColor != other.errorIndicatorColor) return false
if (disabledIndicatorColor != other.disabledIndicatorColor) return false
if (leadingIconColor != other.leadingIconColor) return false
if (disabledLeadingIconColor != other.disabledLeadingIconColor) return false
if (errorLeadingIconColor != other.errorLeadingIconColor) return false
if (trailingIconColor != other.trailingIconColor) return false
if (disabledTrailingIconColor != other.disabledTrailingIconColor) return false
if (errorTrailingIconColor != other.errorTrailingIconColor) return false
if (backgroundColor != other.backgroundColor) return false
if (focusedLabelColor != other.focusedLabelColor) return false
if (unfocusedLabelColor != other.unfocusedLabelColor) return false
if (disabledLabelColor != other.disabledLabelColor) return false
if (errorLabelColor != other.errorLabelColor) return false
if (placeholderColor != other.placeholderColor) return false
if (disabledPlaceholderColor != other.disabledPlaceholderColor) return false
return true
}
override fun hashCode(): Int {
var result = textColor.hashCode()
result = 31 * result + disabledTextColor.hashCode()
result = 31 * result + cursorColor.hashCode()
result = 31 * result + errorCursorColor.hashCode()
result = 31 * result + focusedIndicatorColor.hashCode()
result = 31 * result + unfocusedIndicatorColor.hashCode()
result = 31 * result + errorIndicatorColor.hashCode()
result = 31 * result + disabledIndicatorColor.hashCode()
result = 31 * result + leadingIconColor.hashCode()
result = 31 * result + disabledLeadingIconColor.hashCode()
result = 31 * result + errorLeadingIconColor.hashCode()
result = 31 * result + trailingIconColor.hashCode()
result = 31 * result + disabledTrailingIconColor.hashCode()
result = 31 * result + errorTrailingIconColor.hashCode()
result = 31 * result + backgroundColor.hashCode()
result = 31 * result + focusedLabelColor.hashCode()
result = 31 * result + unfocusedLabelColor.hashCode()
result = 31 * result + disabledLabelColor.hashCode()
result = 31 * result + errorLabelColor.hashCode()
result = 31 * result + placeholderColor.hashCode()
result = 31 * result + disabledPlaceholderColor.hashCode()
return result
}
}