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):
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:
Para que serve a tela mencionada acima? Na verdade, ela é usada para desenhar linhas de interferência. O efeito final é assim (deve ser bom):
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.
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 是一个单例类:
用于封装方法便于使用。 最后还要加上:
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 千万不要设置太大,不然你就是为难用户:
这验证码是怕人看见了吗?
4、Code 单例类中的注意点
在 getColor() 中的不透明度不能设置太小(我直接不设置),显示的不是很清楚,比如:
看的清吗?(好像可以哦) 在 getColorList() 里面,random的下限一定要大于1,不然:
红红的可怕吗? 这里是因为 Brush.linearGradient() 要求要有两种以上的颜色,不然和 Color 纯色有什么区别。 对 Code 单例类好奇,可以先去完整代码 看看再回头来继续学习,其实也差不多结束了。 另外,在 Code 单例类里面的 dp 、sp 、px 的转换大家可以学习一下,在此之前我还不会呢。
5、初步测试
到这里我们已经可以得到验证码的样子了,只是还没有功能,我们下一步再实现,先来测试一下传参之后能否使用:
很明显是没什么问题嘛,而且验证码还这么好看(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:
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:
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:
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!