Escrever | Escreva você mesmo um código de verificação

prefácio

Quando estava trabalhando na função de login, queria obter um código de verificação para fazer login no código de verificação. Andei pela Internet e parece que não o escrevi no Compose (talvez não o tenha encontrado) , já que eu não tenho um, vou fazer um eu mesmo. Se você não tiver tempo ou uma boa base, poderá ir diretamente ao código completo para ver o código principal, pois a implementação é relativamente simples e repetitiva. Claro, também convido você a ler meu artigo e aprender passo a passo.


1. Seleção de ferramentas

A maior parte da Internet é implementada com paint, mas as propriedades de paint em Compose parecem ser reduzidas, por exemplo, textSkewX não (o seguinte é Compose):

insira a descrição da imagem aqui insira a descrição da imagem aqui

Uma vez que esta não é uma boa maneira de usar tinta. Por fim, acho que o código de verificação geralmente inclui letras e números, portanto, use o texto mais simples mais a tela.


2. Ideia básica

A coisa mais importante no código de verificação é a aleatoriedade, então como alcançamos a aleatoriedade? Não é muito simples, use Random. Como o estilo do código de verificação pode ser diferente? Não é muito simples, use Random + atributo. Portanto, precisamos apenas listar as propriedades de Text e adicionar Random para obter o estilo básico do código de verificação:

insira a descrição da imagem aqui

Para que serve a tela mencionada acima? Na verdade, ela é usada para desenhar linhas de interferência. O efeito final é assim (deve ser bom):

insira a descrição da imagem aqui

Pode haver idéias melhores, mas eu não vou. Vamos dar um exemplo para falar sobre a implementação específica.


Terceiro, a implementação específica

0. Explicação do parâmetro

Aqui primeiro coloque os parâmetros necessários para finalmente implementar o código de verificação e explicá-lo, para que todos possam ler depois:

@RequiresApi(Build.VERSION_CODES.Q)
@OptIn(ExperimentalUnitApi::class)
@Composable
fun VerifyCode(
    // 宽高不用解释
    width: Dp,
    height: Dp,
    // 距离左上角的的偏移量, 用于定位
    topLeft: DpOffset = DpOffset.Zero,
    // 验证码的数量
    codeNum: Int = 4,
    // 干扰线的数量
    disturbLineNum: Int = 10,
    // 用于保存验证码, 用于用户输入时进行验证
    viewModel: MyViewModel
) {}
复制代码

1. Verifique o conteúdo

A primeira coisa a implementar, é claro, é verificar algo, assim:

private val codeList = listOf(
        "0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
        "a", "b", "c", "d", "e", "f", "g", "h", "i", "j",
        "k", "l", "m", "n", "o", "p", "q", "r", "s", "t",
        "u", "v", "w", "x", "y", "z",
        "A", "B", "C", "D", "E", "F", "G", "H", "I", "J",
        "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T",
        "U", "V", "W", "X", "Y", "Z"
    )
复制代码

Usamos números e letras para verificação, e selecionarei aleatoriamente codeNum para ser usado como códigos de verificação posteriormente.

2. Configurações de texto

Baseado na ideia de Random + propriedades mencionadas acima. Primeiro, obtemos todas as propriedades de Text:

Text(
    text = ,
    modifier = ,
    color = ,
    fontSize = ,
    fontStyle = ,
    fontWeight = ,
    fontFamily = ,
    textDecoration = ,
    textAlign = ,
    letterSpacing = ,
    lineHeight = ,
    maxLines =,
    onTextLayout =,
    style =,
)
复制代码

E liste todos os valores que serão atribuídos à propriedade (aqui pega fontFamily como exemplo):

    private val fontFamilyList = listOf(
        FontFamily.Default,
        FontFamily.Cursive,
        FontFamily.Monospace,
        FontFamily.SansSerif,
        FontFamily.Serif
    )
复制代码

A seguir estão todos os valores dos atributos usados. Se quiser ver, pode ir até o código completo para dar uma olhada e voltar para continuar aprendendo.

insira a descrição da imagem aqui

Adicionar Aleatório:

    private fun <T> List<T>.getRandom() = this[Random.nextInt(this.size)]
//    shuffled() 函数返回⼀个包含了以随机顺序排序的集合元素的新的 List
//    private fun <T>  List<T>.getRandom() : T = this.shuffled().take(1)[0]
复制代码

这里用了 kotlin 的扩展函数(用起来真的爽),有两种写法大家自选。 最后的得到这样的结果:

Text(
    text = Code.getCode(),
    modifier = Modifier
        .width(width / codeNum)
        .height(height)
        .offset(topLeft.x + dx, topLeft.y),
    color = Code.getColor(),
    // fontSize 需要的是 TextUnit 需要将 dp 转为 sp
    // 用 min() 保证字符都能被看见
    fontSize = Code.getTextUnit(
        minDp = min(width / codeNum / 2, height),
        maxDp = min(width / codeNum, height)
    ),
    fontStyle = Code.getFontStyle(),
    fontWeight = Code.getFontWeight(),
    fontFamily = Code.getFontFamily(),
    textDecoration = Code.getTextDecoration(),
    textAlign = Code.getTextAlign(),
    // 由于我们 Text 里只有一个字符, 有的属性就没必要了
    // letterSpacing = ,
    // lineHeight = ,
    // maxLines =,
    // onTextLayout =,
    // style =,
)
复制代码

大家一定要注意加上 topLeft.x 和 topLeft.y,验证码不能老待在左上角吧。这里的 Code 是一个单例类:

insira a descrição da imagem aqui

用于封装方法便于使用。 最后还要加上:

repeat(codeNum) {}
复制代码

我们需要 codeNum 个字符,而且每次应该从 Code.getCode() 的到一个字符,不然的话所有字符的样式都是相同的。 到这我们 Text 就实现好了。

3、干扰线的实现

先放代码:

repeat(disturbLineNum) {
    val startOffset = Code.getLineOffset(
        minDpX = topLeft.x,
        maxDpX = topLeft.x + width,
        minDpY = topLeft.y,
        maxDpY = topLeft.y + height
    )
    
    val endOffset = Code.getLineOffset(
        minDpX = topLeft.x,
        maxDpX = topLeft.x + width,
        minDpY = topLeft.y,
        maxDpY = topLeft.y + height
    )
    
    val strokeWidth = Code.getStrokeWidth(height / 100, height / 40)
    Canvas(
        modifier = Modifier
            .width(width)
            .height(height)
    ) {
        // repeat 放在这, 对于每一条线 startOffset 和 endOffset 是一样的
        // repeat 多少次都只有一条线, 所以我们往外提
        // repeat(disturbLineNum)
        drawLine(
        // 这里两种都行, 我采用 brush
        // color = Code.getColor(),
            brush = Brush.linearGradient(
                Code.getColorList()
            ),
            start = startOffset,
            end = endOffset,
            strokeWidth = strokeWidth,
            cap = Code.getCap(),
        )
    }
}
复制代码

这里我们首先得到起点和终点的位置,之后 drawLine 就轻而易举了。这里面的注释大家还是要注意的,和 Text 一样 topLeft.x 和 topLeft.y 不能忘,不然要怎么干扰 Text 呢。还有一点使用时 disturbLineNum 千万不要设置太大,不然你就是为难用户:

insira a descrição da imagem aqui

这验证码是怕人看见了吗?

4、Code 单例类中的注意点

在 getColor() 中的不透明度不能设置太小(我直接不设置),显示的不是很清楚,比如:

insira a descrição da imagem aqui

看的清吗?(好像可以哦) 在 getColorList() 里面,random的下限一定要大于1,不然:

insira a descrição da imagem aqui

红红的可怕吗? 这里是因为 Brush.linearGradient() 要求要有两种以上的颜色,不然和 Color 纯色有什么区别。 对 Code 单例类好奇,可以先去完整代码 看看再回头来继续学习,其实也差不多结束了。 另外,在 Code 单例类里面的 dp 、sp 、px 的转换大家可以学习一下,在此之前我还不会呢。

5、初步测试

到这里我们已经可以得到验证码的样子了,只是还没有功能,我们下一步再实现,先来测试一下传参之后能否使用:

insira a descrição da imagem aqui

很明显是没什么问题嘛,而且验证码还这么好看(WDBMNUM1)。接着我们实现功能,毕竟验证码再好看也不是拿来看的嘛。

6、功能实现

Para realizar a função de verificação, precisamos primeiro salvar o código de verificação. Podemos usar o ViewModel para armazenar o código de verificação gerado aleatoriamente. O código de verificação gerado aleatoriamente deve ser conectado a uma string. Faça isso:

		...省略代码...
	var code = ""
	repeat(codeNum) {
		val oneCode = Code.getCode()
		code += oneCode
		...省略代码...
	}
复制代码

Então salve:

	...省略代码...
	// 将 code 转为小写, 以免一些大小写相似的字母导致用户输入错误
	viewModel.setCode(code = code.lowercase())
	...省略代码...
复制代码

Código ViewModel, relativamente simples:

class MyViewModel : ViewModel() {
    private var verifyCode by mutableStateOf("")
    fun setCode(code: String) {
        verifyCode = code
    }
    fun verify(input: String) = input.lowercase() == verifyCode
}
复制代码

verifique() é usado para verificação. Verifique usando:

@RequiresApi(Build.VERSION_CODES.Q)
@Composable
fun Main(viewModel: MyViewModel) {
    Column {
        var text by remember {
            mutableStateOf("")
        }
        val context = LocalContext.current
        Row(
            Modifier
                .fillMaxWidth()
                .height(50.dp)
        ) {
            TextField(
                value = text,
                onValueChange = {
                    text = it
                },
                Modifier.weight(1f)
            )
            VerifyCode(
                width = 150.dp,
                height = 50.dp,
                topLeft = DpOffset(0.dp, 0.dp),
                codeNum = 4,
                disturbLineNum = 20,
                viewModel = viewModel
            )
        }
        Button(onClick = {
            if (viewModel.verify(text)) {
                Toast.makeText(context, "输入正确", Toast.LENGTH_SHORT).show()
            } else {
                Toast.makeText(context, "输入错误", Toast.LENGTH_SHORT).show()
            }
        }) {
            Text(text = "点我点我")
        }
    }
}
复制代码

O viewModel é passado após ser construído na atividade. Ao usar TextField, encontrei o problema de que a entrada não pode ser exibida. Se estiver interessado, você pode mover ( Compose | TextField não pode exibir o conteúdo de entrada ) e dar uma olhada É melhor me ajudar a responder, haha. Confira nossos resultados:

insira a descrição da imagem aqui

Por fim, há outra função, ou seja, geralmente podemos ver que clicar no código de verificação fornecerá um novo código de verificação. Como isso pode ser alcançado? Não é fácil, use a programação reativa do Compose, assim:

insira a descrição da imagem aqui

São 7 linhas no total com parênteses relacionados aos cliques. Isso pode ser alcançado em tão pouco tempo? Vamos ver os resultados:

insira a descrição da imagem aqui

Atreva-se a deixá-lo sair, é claro que pode ser alcançado. Deve-se notar aqui que embora o último sinalizador seja inserido como um sinalizador, ele não faz nada, mas não podemos excluí-lo. É a essência da programação reativa. Quando o programa detecta que ele muda, ele será redesenhado. Se você não entender remember e mutableStateOf aqui, você pode ler meu outro artigo ( Compose | Remember, the use of mutableStateOf ) para uma comparação mais básica. Por favor, avise se eles não estiverem bem escritos.

Neste ponto, nossa função também foi implementada.


4. Código completo

Aqui está o código principal, não no Github:

import android.os.Build
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle.Companion.Italic
import androidx.compose.ui.text.font.FontStyle.Companion.Normal
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.*
import com.glintcatcher.mytest.MyViewModel
import kotlin.random.Random

@RequiresApi(Build.VERSION_CODES.Q)
@OptIn(ExperimentalUnitApi::class)
@Composable
fun VerifyCode(
    // 宽高不用解释
    width: Dp,
    height: Dp,
    // 距离左上角的的偏移量, 用于定位
    topLeft: DpOffset = DpOffset.Zero,
    // 验证码的数量
    codeNum: Int = 4,
    // 干扰线的数量
    disturbLineNum: Int = 10,
    // 用于保存验证码, 用于用户输入时进行验证
    viewModel: MyViewModel
) {
    var flag by remember {
        mutableStateOf(-1)
    }
    Box(
        modifier = Modifier
            .width(width)
            .height(height)
            .offset(topLeft.x, topLeft.y)
            .clickable {
                flag = -flag
            }
    ) {
        // 用于响应式编程,重绘验证码
        flag
        var dx = 0.dp
        var code = ""
        repeat(codeNum) {
            // 得到单个字符, 不能直接得到 codeNum 个字符, 不然样式是一样的
            val oneCode = Code.getCode()
            code += oneCode
            Text(
                text = oneCode,
                modifier = Modifier
                    .width(width / codeNum)
                    .height(height)
                    .offset(topLeft.x + dx, topLeft.y),
                color = Code.getColor(),
                // fontSize 需要的是 TextUnit 需要将 dp 转为 sp
                // 用 min() 保证字符都能被看见
                fontSize = Code.getTextUnit(
                    minDp = min(width / codeNum / 2, height),
                    maxDp = min(width / codeNum, height)
                ),
                fontStyle = Code.getFontStyle(),
                fontWeight = Code.getFontWeight(),
                fontFamily = Code.getFontFamily(),
                textDecoration = Code.getTextDecoration(),
                textAlign = Code.getTextAlign(),
                // 由于我们 Text 里只有一个字符, 有的属性就没必要了
//                letterSpacing = ,
//                lineHeight = ,
//                maxLines =,
//                onTextLayout =,
//                style =,
            )
            // dx 加上 Text 的宽度防止堆叠
            dx += width / codeNum
        }

        // 将 code 转为小写, 以免一些大小写相似的字母导致用户输入错误
        viewModel.setCode(code = code.lowercase())

        repeat(disturbLineNum) {
            val startOffset = Code.getLineOffset(
                minDpX = topLeft.x,
                maxDpX = topLeft.x + width,
                minDpY = topLeft.y,
                maxDpY = topLeft.y + height
            )

            val endOffset = Code.getLineOffset(
                minDpX = topLeft.x,
                maxDpX = topLeft.x + width,
                minDpY = topLeft.y,
                maxDpY = topLeft.y + height
            )

            val strokeWidth = Code.getStrokeWidth(height / 100, height / 40)
            Canvas(
                modifier = Modifier
                    .width(width)
                    .height(height)
            ) {
                // repeat 放在这, 对于每一条线 startOffset 和 endOffset 是一样的
                // repeat 多少次都只有一条线, 所以我们往外提
//            repeat(disturbLineNum)
                drawLine(
                    // 这里两种都行, 我采用 brush
//                color = Code.getColor(),
                    brush = Brush.linearGradient(
                        Code.getColorList()
                    ),
                    start = startOffset,
                    end = endOffset,
                    strokeWidth = strokeWidth,
                    cap = Code.getCap(),
                )
            }
        }
    }
}

object Code {
    private val codeList = listOf(
        "0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
        "a", "b", "c", "d", "e", "f", "g", "h", "i", "j",
        "k", "l", "m", "n", "o", "p", "q", "r", "s", "t",
        "u", "v", "w", "x", "y", "z",
        "A", "B", "C", "D", "E", "F", "G", "H", "I", "J",
        "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T",
        "U", "V", "W", "X", "Y", "Z"
    )

    @RequiresApi(Build.VERSION_CODES.Q)
    private val fontStyleList = listOf(
        Normal,
        Italic
    )

    private val fontWeightList = listOf(
        FontWeight.Black,
        FontWeight.Bold,
        FontWeight.ExtraBold,
        FontWeight.ExtraLight,
        FontWeight.Light,
        FontWeight.Medium,
        FontWeight.Normal,
        FontWeight.SemiBold,
        FontWeight.Thin,
        FontWeight.W100,
        FontWeight.W200,
        FontWeight.W300,
        FontWeight.W400,
        FontWeight.W500,
        FontWeight.W600,
        FontWeight.W700,
        FontWeight.W800,
        FontWeight.W900
    )

    private val fontFamilyList = listOf(
        FontFamily.Default,
        FontFamily.Cursive,
        FontFamily.Monospace,
        FontFamily.SansSerif,
        FontFamily.Serif
    )

    private val textDecorationList = listOf(
        TextDecoration.None,
        TextDecoration.LineThrough,
        TextDecoration.Underline
    )

    private val textAlignList = listOf(
        TextAlign.Center,
        TextAlign.Start,
        TextAlign.End,
        TextAlign.Justify,
        TextAlign.Left,
        TextAlign.Right
    )

    private val capList = listOf(
        StrokeCap.Butt,
        StrokeCap.Round,
        StrokeCap.Square
    )

    private fun <T> List<T>.getRandom() = this[Random.nextInt(this.size)]
//    shuffled() 函数返回⼀个包含了以随机顺序排序的集合元素的新的 List
//    private fun <T>  List<T>.getRandom() : T = this.shuffled().take(1)[0]

    fun getCode(): String = codeList.getRandom()

    @RequiresApi(Build.VERSION_CODES.Q)
    fun getFontStyle() = fontStyleList.getRandom()

    fun getFontWeight() = fontWeightList.getRandom()

    fun getFontFamily() = fontFamilyList.getRandom()

    fun getTextDecoration() = textDecorationList.getRandom()

    fun getTextAlign() = textAlignList.getRandom()

    fun getColor() = Color(
        red = Random.nextInt(256),
        green = Random.nextInt(256),
        blue = Random.nextInt(256),
        // 不透明度小的时候显示的不是很清楚, 所以就舍弃掉吧
//        alpha = Random.nextInt(256)
    )

    fun getColorList(): ArrayList<Color> {
        val colorList = arrayListOf<Color>()
        // 最小值要是 2, 如果 colorList 的 size = 1 会报错
        repeat(Random.nextInt(2, 11)) {
            colorList.add(getColor())
        }
        return colorList
    }

    fun getCap() = capList.getRandom()

    @Composable
    fun getTextUnit(minDp: Dp, maxDp: Dp) = with(LocalDensity.current) {
        val min = minDp.roundToPx()
        val max = maxDp.roundToPx()
        Random.nextInt(min, max + 1).toSp()
    }

    @Composable
    fun getLineOffset(minDpX: Dp, maxDpX: Dp, minDpY: Dp, maxDpY: Dp) =
        with(LocalDensity.current) {
            val minX = minDpX.roundToPx()
            val maxX = maxDpX.roundToPx()
            val minY = minDpY.roundToPx()
            val maxY = maxDpY.roundToPx()
            Offset(
                Random.nextInt(minX, maxX + 1).toFloat(),
                Random.nextInt(minY, maxY + 1).toFloat()
            )
        }

    @Composable
    fun getStrokeWidth(min: Dp, max: Dp) = with(LocalDensity.current) {
        val min = min.roundToPx()
        val max = max.roundToPx()
        Random.nextInt(min, max + 1).toFloat()
    }
}
复制代码

finalmente

O artigo está aqui, espero que seja útil para você, bem-vindo a comentar, tchau!

Acho que você gosta

Origin juejin.im/post/7083063918402207758
Recomendado
Clasificación