How to show PIN field with custom drawables?

716 Views Asked by At

I have to show a custom PIN field, for each numeric keyboard entry. Instead of using BasicTextField I would like to show custom drawables for entered numeric values:

enter image description here

The example above shows a 2 digit pin (doen not make sense, just for example purpose), whereas the first digit (green) was entered:

  • If no numeric was entered for current digit --> red bordered circle (second digit not entered)
  • If a numeric was entered for current digit --> white circle with green border (first digit was entered)

I could not manage to customize PIN designs for BasicTextField with Jetpack Compose. What would be an easy way to do so? Is it possible to customize BasicTextField?

I managed to draw circles in a row (above screenshot for an example) via androidx.compose.ui.graphics.drawscope.drawCircle(). I could nto connect the keyboard to it?

2

There are 2 best solutions below

3
Gabriele Mariotti On

You can use a BasicTextField + OutlinedTextFieldDecorationBox:

var text by remember { mutableStateOf("") }
val interactionSource = remember { MutableInteractionSource() }
val enabled = true
val singleLine = true

var isError by remember { mutableStateOf(false) }
var circleColor  by remember { mutableStateOf(Gray) }

val colors = TextFieldDefaults.outlinedTextFieldColors(
    focusedBorderColor = circleColor,
    unfocusedBorderColor = circleColor
)

BasicTextField(
    value = text,
    onValueChange = {
        if (it.length <= 1) {
            text = it
            if (it.isDigitsOnly()) {
                circleColor = Green
                isError = false
            }
        }
        if (it.isEmpty()) isError = true
    },
    interactionSource = interactionSource,
    textStyle = TextStyle.Default.copy(textAlign = TextAlign.Center, fontSize = 16.sp),
    enabled = enabled,
    singleLine = singleLine,
    modifier = Modifier.width(50.dp).height(50.dp)
) {
    TextFieldDefaults.OutlinedTextFieldDecorationBox(
        value = text,
        visualTransformation = VisualTransformation.None,
        innerTextField = it,
        singleLine = singleLine,
        enabled = enabled,
        interactionSource = interactionSource,
        contentPadding = TextFieldDefaults.textFieldWithoutLabelPadding(
            start = 0.dp, end = 0.dp, top = 0.dp, bottom = 0.dp
        ),
        colors = colors,
        border = {
            TextFieldDefaults.BorderBox(
               enabled, 
               isError, 
               interactionSource, 
               colors,
               shape = CircleShape,
               focusedBorderThickness = 3.dp,
               unfocusedBorderThickness = 3.dp)
        }
    )
}

enter image description here enter image description here

1
Abhimanyu On

This is my understanding of the question.
Please comment if this is not the case.

Code

@Composable
fun CustomTextFieldVisual() {
    val focusManager = LocalFocusManager.current
    val (text, setText) = remember {
        mutableStateOf(TextFieldValue(""))
    }
    val charLimit = 6

    LaunchedEffect(
        key1 = text,
    ) {
        if (text.text.length == charLimit) {
            focusManager.clearFocus()
        }
    }

    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {
        Box(
            modifier = Modifier
                .width(
                    200.dp,
                ),
        ) {
            CompositionLocalProvider(
                LocalTextToolbar provides EmptyTextToolbar
            ) {
                BasicTextField(
                    value = text,
                    onValueChange = { newValue ->
                        if (newValue.text.length <= 6) {
                            setText(
                                if (newValue.selection.length > 0) {
                                    newValue.copy(
                                        selection = text.selection,
                                    )
                                } else {
                                    newValue
                                }
                            )
                        }
                    },
                    singleLine = true,
                    keyboardOptions = KeyboardOptions(
                        keyboardType = KeyboardType.NumberPassword,
                        imeAction = ImeAction.Done,
                    ),
                    textStyle = TextStyle(
                        color = Transparent,
                    ),
                    cursorBrush = SolidColor(Transparent),
                    modifier = Modifier
                        .clip(CircleShape)
                        .fillMaxWidth()
                        .background(Color(0xFFEEEEEE))
                        .padding(16.dp),
                )
            }
            Row(
                verticalAlignment = Alignment.CenterVertically,
                horizontalArrangement = Arrangement.SpaceEvenly,
                modifier = Modifier
                    .clip(CircleShape)
                    .fillMaxWidth()
                    .background(Color(0xFFEEEEEE))
                    .padding(16.dp),
            ) {
                for (i in 0 until charLimit) {
                    if (i < text.text.length) {
                        Dot(Green)
                    } else {
                        Dot(Red)
                    }
                }
            }
        }
        Text(
            text = "Entered Text : ${text.text}",
            modifier = Modifier
                .padding(
                    all = 16.dp,
                ),
        )
    }
}

// This can be replaced with any composable as per requirement.
@Composable
fun Dot(
    color: Color,
) {
    Box(
        modifier = Modifier
            .requiredSize(
                size = 16.dp,
            )
            .background(
                color = color,
                shape = CircleShape,
            ),
    )
}

Note:

  1. This does not seem like this the best solution. Looking to improvise this. All suggestions are welcome.