I’m working on creating a password field for my ap...
# compose
j
I’m working on creating a password field for my app using a TextField with a leading and trailing icon. My UI design calls for a different color for the leadingIcon background than the rest of the TextField. I tried to implement that by setting the background color on my IconButton in my LeadingIcon compose function, but it looks like there is some padding above and below the icon which is using the TextField’s background color. Is there any way to remove that padding or set the leadingIcon background color somewhere else so the entire leading section is one background color? I could build it out myself by moving the icon out of the TextField, but then I’ll lose a bunch of little default visual behaviors so I’d like to avoid that if possible. See code within the thread. The attached screenshot has what my preview looks like below.
🧵 1
1
Moving code into the thread:
Copy code
@Composable
private fun PasswordField() {
    var password by rememberSaveable { mutableStateOf("")}
    var passwordVisibility: Boolean by remember { mutableStateOf(false) }
    val iconColor = if(isSystemInDarkTheme()) steel02 else steel08
    TextField(
        value = password,
        onValueChange = {password = it},
        visualTransformation = if (passwordVisibility) VisualTransformation.None else PasswordVisualTransformation(),
        keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
        leadingIcon = {
            val image = if(passwordVisibility) Icons.Filled.Visibility else Icons.Filled.VisibilityOff
            val description = if(passwordVisibility) stringResource(R.string.password_visible)
                            else stringResource(R.string.password_not_visible)
            val iconBackground = if(isSystemInDarkTheme()) steel09 else steel02
            IconButton(
                onClick = { passwordVisibility = !passwordVisibility },
                modifier = Modifier.background(color = iconBackground)
            ) {
                Icon(imageVector = image, contentDescription = description)
            }
        },
        trailingIcon = {
            IconButton(onClick = { password = "" }) {
                Icon(imageVector = Icons.Filled.Clear, contentDescription = "Clear password")
            }
        },
        colors = TextFieldDefaults.textFieldColors(
            backgroundColor = MaterialTheme.colors.surface,
            leadingIconColor = iconColor,
            trailingIconColor = iconColor
        ),
        shape = MaterialTheme.shapes.small
    )
}
c
I think for these customizations, it may be simpler in the long run to recreate this with layers on top of`BasicTextField`. The current
TextField
is designed to follow the Material spec and it seems some of the changes would be challenging to customize within the current slots approach (seems this way after skimming the implementation code).
👍 1
I’ll lose a bunch of little default visual behaviors
What behaviors would you lose?
@Anastasia [G] might have more tips as well
j
As far as the behaviors I’d lose, the main one I’m thinking of is the border color change onfocus.
I guess that’s probably it, though. So not a huge deal.
I suppose if I can pad things and layer correctly I can still utilize the border for the underlying BasicTextField, though
K 2
Yeah, wasn’t too bad to keep the onFocus behavior actually. Barely more code than the way I started out with so that’s pretty awesome. Here’s what I landed on if anyone is curious. If you’re bored, I welcome constructive criticism 😁
Copy code
@Composable
private fun PasswordField() {
    var password by rememberSaveable { mutableStateOf("")}
    var passwordVisibility: Boolean by remember { mutableStateOf(false) }
    var isFocused by remember { mutableStateOf(false) }
    val iconColor = if(isSystemInDarkTheme()) steel02 else steel08
    val borderColor = when {
        isFocused -> MaterialTheme.colors.primary
        isSystemInDarkTheme() -> steel05
        else -> steel03
    }

    Surface(
        shape = MaterialTheme.shapes.small,
        modifier = Modifier
            .fillMaxWidth()
            .border(1.dp, borderColor, MaterialTheme.shapes.small)
    ) {
        Row(
            verticalAlignment = Alignment.CenterVertically
        ) {
            val image =
                if (passwordVisibility) Icons.Filled.Visibility else Icons.Filled.VisibilityOff
            val description = if (passwordVisibility) stringResource(R.string.password_visible)
            else stringResource(R.string.password_not_visible)
            val iconBackground = if (isSystemInDarkTheme()) steel09 else steel02
            IconButton(
                onClick = { passwordVisibility = !passwordVisibility },
                modifier = Modifier.background(color = iconBackground)
            ) {
                Icon(
                    imageVector = image,
                    contentDescription = description,
                    tint = iconColor
                )
            }
            BasicTextField(
                value = password,
                onValueChange = { password = it },
                visualTransformation = if (passwordVisibility) VisualTransformation.None else PasswordVisualTransformation(),
                keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
                singleLine = true,
                modifier = Modifier
                    .weight(1f)
                    .padding(4.dp)
                    .onFocusChanged { isFocused = it.isFocused }
            )
            IconButton(onClick = { password = "" }) {
                Icon(
                    imageVector = Icons.Filled.Clear,
                    contentDescription = stringResource(R.string.clear_password)
                )
            }
        }
    }
}
Final result
🎉 1
Thanks for the help!
🙏 1
c
Well done!! Yeah that code doesn’t look too bad at all - very readable! One thought - did you want to hide/show the Clear icon depending on if there’s an input value? I can’t recall off the top of my head if the Material TextField works that way…
j
That is a good suggestion. I’ll check with our UX team on that. I don’t think the material textfield would do that by default because the trailing icon could be anything so that can’t be default behavior.
Just wrapped my call to IconButton in a check for an empty password to hide it when there’s no text. Very nice
c
Nice! Yeah good point on the trailing icon being flexible. That is one of the nice aspects of it using a content slot in
TextField
so you can customize that part.
think smart 1
a
Creating custom text field based on
BasicTextField
is exactly the thing we expect you to do in this case. Regarding your code, did you consider using
decorationBox
parameter of the BasicTextField? It should give you better accessibility support, for example. Roughly, instead of
Row { Icon(); BasicTextField(); Icon() }
you could do
BasicTextField() { Icon(); it(); Icon() }
j
ah very nice, the docs even detail that that is what decorationBox is for, haha. I’ll try that out. Thanks!
👍 2
hmm, I’m having trouble getting the inner text field to fill the width inbetween my two buttons when I go this route
ok. I think I figured out it. I wrapped it() in a Box and then gave the box a weight so it’ll fill the space. The preview still shows the inner text field as being smaller inside the box, but it expands as you add text so that works fine.
a
Box
has a
propagateMinConstraints
or something like that parameter which might need to be set to
true
in your case
🙌 1
j
ah nice, it seems to work without setting that to true, but that does make the internalTextField expand to fill the box. I’ll go with that to avoid weird interaction issues outside of the internalTextField.