我正在努力在我的应用程序中使用 BasicTextField 实现自定义 TextField,但我遇到了三个无法解决的问题。
颜色未应用:尽管设置了颜色,但它们并未按预期反映在 UI 中。
VisualTransformation 不修改文本:我尝试使用自定义的 VisualTransformation 来格式化或屏蔽文本输入,但它没有对文本应用任何转换。
isError 未更改颜色:isError 标志没有像出现错误时那样更新 BasicTextField 的颜色,即使我已将其设置为触发颜色更改。
我尝试过调试这些问题,但我一直无法弄清楚为什么参数没有按预期工作。任何指导或建议将不胜感激
/**
* A composable function that represents a styled text field, similar to the input fields in
Spotify's UI.
*
* @param value The current value of the text field.
* @param onValueChange A lambda function that handles changes to the text field's value.
* @param modifier Modifier to customize the layout and styling of the text field.
* @param trailingIcon An optional composable that adds an icon to the end of the text field
(e.g., a clear button or search icon).
* @param visualTransformation A transformation applied to the text for visual effects, like
masking (e.g., password input). Defaults to no transformation.
* @param keyboardType Specifies the type of keyboard to display (e.g., text, number, email).
Defaults to `KeyboardType.Text`.
* @param imeAction Specifies the action button on the keyboard (e.g., Done, Next). Defaults
to `ImeAction.Done`.
* @param readOnly Boolean that determines whether the text field is read-only. Defaults to
`false`.
*/
@SuppressLint("UnrememberedMutableInteractionSource")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SpotifyTextField(
value: TextFieldValue,
onValueChange: (TextFieldValue) -> Unit,
modifier: Modifier = Modifier,
trailingIcon: @Composable() (() -> Unit)? = null,
visualTransformation: VisualTransformation = VisualTransformation.None,
keyboardType: KeyboardType = KeyboardType.Text,
imeAction: ImeAction = ImeAction.Done,
keyboardActions: KeyboardActions = KeyboardActions.Default,
readOnly: Boolean = false,
isError: Boolean = false
) {
BasicTextField(
value = value,
onValueChange = onValueChange,
modifier = modifier
.fillMaxWidth()
.height(48.dp)
// .background(
// color = MaterialTheme.colorScheme.tertiary,
// shape = RoundedCornerShape(4.dp)
// )
.padding(start = 4.dp, end = 4.dp),
singleLine = true,
textStyle = MaterialTheme.typography.bodyLarge,
readOnly = readOnly,
keyboardOptions = KeyboardOptions(
keyboardType = keyboardType, imeAction = imeAction,
hintLocales = LocaleList(Locale("en"))
),
keyboardActions = keyboardActions,
decorationBox = {innerTextField ->
TextFieldDefaults.DecorationBox(
value = value.text,
isError = isError,
innerTextField = { innerTextField() },
visualTransformation = visualTransformation,
trailingIcon = trailingIcon,
shape = RoundedCornerShape(4.dp),
colors = TextFieldDefaults.colors(
unfocusedContainerColor = MaterialTheme.colorScheme.secondary,
cursorColor = MaterialTheme.colorScheme.onTertiary,
focusedTextColor = Color.White,
unfocusedTextColor = Color.White,
focusedContainerColor = MaterialTheme.colorScheme.tertiary,
disabledLabelColor = MaterialTheme.colorScheme.tertiary,
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
errorTextColor = MaterialTheme.colorScheme.onErrorContainer, //colorResource(id = R.color.brown),
errorContainerColor = MaterialTheme.colorScheme.errorContainer, //colorResource(id = R.color.white_smoke),
errorCursorColor = MaterialTheme.colorScheme.error,
errorTrailingIconColor = MaterialTheme.colorScheme.error,
errorIndicatorColor = Color.Transparent,
),
contentPadding = PaddingValues(8.dp),
container = { },
enabled = true,
singleLine = true,
interactionSource = remember { MutableInteractionSource() }
)
}
)
}
/**
* A composable function for a password input field with a toggleable visibility icon.
* The field allows users to show or hide their password by clicking an icon.
*
* @param value The current value of the password field.
* @param onValueChange A lambda function that handles changes to the password field's value.
*/
@Composable
fun SpotifyPasswordField(
modifier: Modifier = Modifier,
value: TextFieldValue,
onValueChange: (TextFieldValue) -> Unit,
imeAction: ImeAction = ImeAction.Done,
isError: Boolean = false,
keyboardActions: KeyboardActions = KeyboardActions.Default,
) {
/** showPassword -> A mutable state that determines whether the password should be shown
(true) or hidden (false).*/
val showPassword = remember { mutableStateOf(false) }
/** Calls the SpotifyTextField composable to create the password field. */
SpotifyTextField(
modifier = modifier,
value = value,
onValueChange = onValueChange,
isError = isError,
trailingIcon = {
/** Displays a toggle button to show or hide the password.*/
if (showPassword.value) {
/** When the password is visible, display the "visibility" icon. */
IconButton(onClick = { showPassword.value = false }) {
Icon(
imageVector = SpotifyIcons.Visibility,
contentDescription = "TAG_SHOW_PASSWORD_ICON"
)
}
} else {
/** When the password is hidden, display the "visibility off" icon. */
IconButton(onClick = { showPassword.value = true }) {
Icon(
imageVector = SpotifyIcons.VisibilityOff,
contentDescription = "TAG_HIDE_PASSWORD_ICON"
)
}
}
},
visualTransformation = if (showPassword.value) {
/** No transformation applied when the password is shown. */
VisualTransformation.None
} else {
/** Applies password masking transformation when the password is hidden. */
PasswordVisualTransformation()
},
imeAction = imeAction,
keyboardType = KeyboardType.Password,
keyboardActions = keyboardActions
)
}
如何使用
TextFieldDefaults.DecorationBox
有几个问题。
您用空 lambda 显式覆盖
container
。该容器用于实际布局 DecorationBox 中定义的所有各种元素。如果您没有定义布局,则不会显示任何内容。这就是为什么颜色、isError
以及定义的样式和布局的其余部分被忽略的原因。
默认容器是
Container(
enabled = enabled,
isError = isError,
interactionSource = interactionSource,
modifier = Modifier,
colors = colors,
shape = shape,
focusedIndicatorLineThickness = FocusedIndicatorThickness,
unfocusedIndicatorLineThickness = UnfocusedIndicatorThickness,
)
我不知道你为什么要覆盖它,但我建议你简单地删除整个
container
参数,以便使用默认值。
虽然现在显示了大多数颜色,但仍然不考虑 focused 文本字段的特定颜色。原因是装饰盒无法检测到聚焦状态。这是由
interactionSource
决定的。正如文档所说:
您必须首先创建您自己的
ed MutableInteractionSource 实例并将其传递到 BasicTextField 以便它调度事件。然后将相同的实例传递给这个装饰框[...]。remember
只需向 SpotifyTextField 添加另一个参数即可:
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
然后将此
interactionSource
传递给 BasicTextField 以及 DecorationBox。他们都必须使用相同MutableInteractionSource。
相同值:
value
enabled
singleLine
visualTransformation
interactionSource
必须指定它们。他们的文档明确指出,需要将它们设置为用于 BasicTextField 的相同值。
您需要为 BasicTextField 设置它们,以便它们具有所需的功能。
您需要为 DecorationBox 设置它们,以便它们可以影响
样式和布局。
BasicTextField(
value = value,
onValueChange = onValueChange,
modifier = modifier
.fillMaxWidth()
.height(48.dp)
// .background(
// color = MaterialTheme.colorScheme.tertiary,
// shape = RoundedCornerShape(4.dp)
// )
.padding(start = 4.dp, end = 4.dp),
singleLine = true,
textStyle = MaterialTheme.typography.bodyLarge,
readOnly = readOnly,
keyboardOptions = KeyboardOptions(
keyboardType = keyboardType, imeAction = imeAction,
hintLocales = LocaleList(Locale("en")),
),
keyboardActions = keyboardActions,
visualTransformation = visualTransformation,
interactionSource = interactionSource,
decorationBox = { innerTextField ->
TextFieldDefaults.DecorationBox(
value = value.text,
isError = isError,
innerTextField = innerTextField,
visualTransformation = visualTransformation,
trailingIcon = trailingIcon,
shape = RoundedCornerShape(4.dp),
colors = TextFieldDefaults.colors(
unfocusedContainerColor = MaterialTheme.colorScheme.secondary,
cursorColor = MaterialTheme.colorScheme.onTertiary,
focusedTextColor = Color.White,
unfocusedTextColor = Color.White,
focusedContainerColor = MaterialTheme.colorScheme.tertiary,
disabledLabelColor = MaterialTheme.colorScheme.tertiary,
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
errorTextColor = MaterialTheme.colorScheme.onErrorContainer, //colorResource(id = R.color.brown),
errorContainerColor = MaterialTheme.colorScheme.errorContainer, //colorResource(id = R.color.white_smoke),
errorCursorColor = MaterialTheme.colorScheme.error,
errorTrailingIconColor = MaterialTheme.colorScheme.error,
errorIndicatorColor = Color.Transparent,
),
contentPadding = PaddingValues(8.dp),
enabled = true,
singleLine = true,
interactionSource = interactionSource,
)
},
)