如何在 android compose 中创建 OTP 布局?

Gau*_*ika 10 android android-jetpack-compose

我正在新的 android jetpack 的 compose 中登录我的应用程序。

我想要制作一个像给定照片中那样的 OTP 布局。

一次性可编程布局

Gab*_*tti 11

您可以对 otp 中的每个字符使用非常简单的布局。

就像是

   @Composable
   fun OtpChar(){
       var text by remember { mutableStateOf("1") }
       val maxChar = 1

       Column(Modifier.background(DarkGray),
              horizontalAlignment = Alignment.CenterHorizontally){
           TextField(
               value =text,
               onValueChange = {if (it.length <= maxChar) text = it},
               modifier = Modifier.width(50.dp),
               singleLine = true,
               textStyle = LocalTextStyle.current.copy(
                   fontSize = 20.sp, 
                   textAlign= TextAlign.Center),
               colors= TextFieldDefaults.textFieldColors(
                   textColor = White,
                   backgroundColor = Transparent,
                   unfocusedIndicatorColor = Transparent,
                   focusedIndicatorColor = Transparent)
           )
           Divider(Modifier
                    .width(28.dp)
                    .padding(bottom = 2.dp)
                    .offset(y=-10.dp), 
             color = White,
             thickness = 1.dp)
       }
   }
Run Code Online (Sandbox Code Playgroud)

在此输入图像描述

您可以添加一些功能,例如:

  • 使用 TAB 键管理下一个方向的焦点
  • 使用 BACK SPACE 键管理上一个方向的焦点
  • 输入数字时如何移动到下一个文本字段

就像是:

fun OtpChar(
    modifier: Modifier = Modifier
){
    val pattern = remember { Regex("^[^\\t]*\$") } //to not accept the tab key as value
    var (text,setText) = remember { mutableStateOf("") }
    val maxChar = 1
    val focusManager = LocalFocusManager.current

    LaunchedEffect(
        key1 = text,
    ) {
        if (text.isNotEmpty()) {
            focusManager.moveFocus(
                focusDirection = FocusDirection.Next,
            )
        }
    }

    Column(
        horizontalAlignment = Alignment.CenterHorizontally
    ){
        TextField(
            value =text,
            onValueChange = {
                if (it.length <= maxChar &&
                    ((it.isEmpty() || it.matches(pattern))))
                        setText(it)
            },
            modifier = modifier
                .width(50.dp)
                .onKeyEvent {
                    if (it.key == Key.Tab) {
                        focusManager.moveFocus(FocusDirection.Next)
                        true
                    }
                    if (text.isEmpty() && it.key == Key.Backspace) {
                        focusManager.moveFocus(FocusDirection.Previous)
                    }
                    false
                },
            textStyle = LocalTextStyle.current.copy(
                fontSize = 20.sp,
                textAlign= TextAlign.Center),
            keyboardOptions = KeyboardOptions(
                imeAction = ImeAction.Next
            ),
            colors= TextFieldDefaults.textFieldColors(
                backgroundColor = Transparent,
                unfocusedIndicatorColor = Transparent,
                focusedIndicatorColor = Transparent),

        )
        Divider(
            Modifier
                .width(28.dp)
                .padding(bottom = 2.dp)
                .offset(y = -10.dp),
            color = Teal200,
            thickness = 1.dp)
    }
}
Run Code Online (Sandbox Code Playgroud)

然后只需使用类似 a 的东西Row来显示 4 OtpChars

val (item1, item2, item3, item4) = FocusRequester.createRefs()

Row(horizontalArrangement = Arrangement.SpaceBetween){
    OtpChar(
        modifier = Modifier
            .focusRequester(item1)
            .focusProperties {
                next = item2
                previous = item1
            }
    )
    OtpChar(
        modifier = Modifier
            .focusRequester(item2)
            .focusProperties {
                next = item3
                previous = item1
            }
    )
    OtpChar(
        modifier = Modifier
            .focusRequester(item3)
            .focusProperties {
                next = item4
                previous = item2
            }
    )
    OtpChar(
        modifier = Modifier
            .focusRequester(item4)
            .focusProperties {
                previous = item3
                next = item4
            }
    )

    //....
}
Run Code Online (Sandbox Code Playgroud)

在此输入图像描述


小智 8

在此处查看完整示例

const val PIN_VIEW_TYPE_UNDERLINE = 0
const val PIN_VIEW_TYPE_BORDER = 1

@Composable
fun PinView(
    pinText: String,
    onPinTextChange: (String) -> Unit,
    digitColor: Color = MaterialTheme.colors.onBackground,
    digitSize: TextUnit = 16.sp,
    containerSize: Dp = digitSize.value.dp * 2,
    digitCount: Int = 4,
    type: Int = PIN_VIEW_TYPE_UNDERLINE,
) {
    BasicTextField(value = pinText,
        onValueChange = onPinTextChange,
        keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
        decorationBox = {
            Row(horizontalArrangement = Arrangement.SpaceBetween) {
                repeat(digitCount) { index ->
                    DigitView(index, pinText, digitColor, digitSize, containerSize, type = type)
                    Spacer(modifier = Modifier.width(5.dp))
                }
            }
        })
}


@Composable
private fun DigitView(
    index: Int,
    pinText: String,
    digitColor: Color,
    digitSize: TextUnit,
    containerSize: Dp,
    type: Int = PIN_VIEW_TYPE_UNDERLINE,
) {
    val modifier = if (type == PIN_VIEW_TYPE_BORDER) {
        Modifier
            .width(containerSize)
            .border(
                width = 1.dp,
                color = digitColor,
                shape = MaterialTheme.shapes.medium
            )
            .padding(bottom = 3.dp)
    } else Modifier.width(containerSize)

    Column(horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center) {
        Text(
            text = if (index >= pinText.length) "" else pinText[index].toString(),
            color = digitColor,
            modifier = modifier,
            style = MaterialTheme.typography.body1,
            fontSize = digitSize,
            textAlign = TextAlign.Center)
        if (type == PIN_VIEW_TYPE_UNDERLINE) {
            Spacer(modifier = Modifier.height(2.dp))
            Box(
                modifier = Modifier
                    .background(digitColor)
                    .height(1.dp)
                    .width(containerSize)
            )
        }
    }
}
 
Run Code Online (Sandbox Code Playgroud)


Une*_*nes 5

如果您遇到键盘问题,请尝试以下代码:

@Composable
fun OtpCell(
    modifier: Modifier,
    value: String,
    isCursorVisible: Boolean = false
) {
    val scope = rememberCoroutineScope()
    val (cursorSymbol, setCursorSymbol) = remember { mutableStateOf("") }

    LaunchedEffect(key1 = cursorSymbol, isCursorVisible) {
        if (isCursorVisible) {
            scope.launch {
                delay(350)
                setCursorSymbol(if (cursorSymbol.isEmpty()) "|" else "")
            }
        }
    }

    Box(
        modifier = modifier
    ) {
        Text(
            text = if (isCursorVisible) cursorSymbol else value,
            style = MaterialTheme.typography.body1,
            modifier = Modifier.align(Alignment.Center)
        )
    }
}

@ExperimentalComposeUiApi
@Composable
fun PinInput(
    modifier: Modifier = Modifier,
    length: Int = 5,
    value: String = "",
    onValueChanged: (String) -> Unit
) {
    val focusRequester = remember { FocusRequester() }
    val keyboard = LocalSoftwareKeyboardController.current
    TextField(
        value = value,
        onValueChange = {
            if (it.length <= length) {
                if (it.all { c -> c in '0'..'9' }) {
                    onValueChanged(it)
                }
                if (it.length >= length) {
                    keyboard?.hide()
                }
            }
        },
      // Hide the text field
        modifier = Modifier
            .size(0.dp)
            .focusRequester(focusRequester),
        keyboardOptions = KeyboardOptions(
            keyboardType = KeyboardType.Number
        )
    )

    Row(
        modifier = Modifier.fillMaxWidth(),
        horizontalArrangement = Arrangement.Center
    ) {
        repeat(length) {
            OtpCell(
                modifier = modifier
                    .size(width = 45.dp, height = 60.dp)
                    .clip(MaterialTheme.shapes.large)
                    .background(MaterialTheme.colors.surface)
                    .clickable {
                        focusRequester.requestFocus()
                        keyboard?.show()
                    },
                value = value.getOrNull(it)?.toString() ?: "",
                isCursorVisible = value.length == it
            )
            if (it != length - 1) Spacer(modifier = Modifier.size(8.dp))
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

结果: 在此输入图像描述