前言
做登录功能的时候,想弄个登录验证码,在网上溜一圈好像没用 Compose 写过(可能是没找到),既然没有,自己做一个。 如果没有时间或者基础好,可以直接完成代码 查看核心代码,因为它相对简单和重复。当然,我也欢迎你一步一步地阅读我的文章。
一、工具选择
大部分都是网上用的 paint 但是在 Compose 里 paint 例如,属性似乎减少了, textSkewX 没有(下为 Compose ): 既然这样用 paint 不太好。最后,我认为验证码通常包括字母和数字,所以直接使用最简单的 Text 加 canvas 来呗。
二、基本思想
验证码最重要的是随机性,那么如何做到随机性呢?这不是很简单吗? Random 啊。验证码的样式如何不同?这不是很简单吗? Random 属性啊。所以我们只需要列出它。 Text 配合属性 Random 验证码的基本样式: 上面提到的 canvas 为什么?它实际上是用来画干扰线的。最终效果如下(应该还可以): 可能有更好的想法,但我不会。让我们举一个具体实现的例子。
三、具体实现
0、参数解释
在这里,首先放置最终实现验证码所需的参数并解释,以便您以后阅读:
@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、验证内容
当然,首先要实现的是要验证的东西,就像这样:
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"
)
我们用数字和字母进行验证,在后面我会随机挑选 codeNum 个用来作为验证码。
2、Text设置
根据上面所说的 Random + 属性的想法。我们先得到 Text 的所有属性:
Text(
text = ,
modifier = ,
color = ,
fontSize = ,
fontStyle = ,
fontWeight = ,
fontFamily = ,
textDecoration = ,
textAlign = ,
letterSpacing = ,
lineHeight = ,
maxLines =,
onTextLayout =,
style =,
)
并且罗列所有将要赋予属性的值(这里以 fontFamily 为例):
private val fontFamilyList = listOf(
FontFamily.Default,
FontFamily.Cursive,
FontFamily.Monospace,
FontFamily.SansSerif,
FontFamily.Serif
)
下面是各个用到的属性的所有值,大家如果想看可以前往完整代码先偷窥一下再回头来继续学习。 加上 Random :
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、功能实现
要实现验证功能我们先要保存验证码,我们可以用 ViewModel 进行储存随机生成的验证码,随机生成的验证码要连成字符串,这样做:
...省略代码...
var code = ""
repeat(codeNum) {
val oneCode = Code.getCode()
code += oneCode
...省略代码...
}
然后进行保存:
...省略代码...
// 将 code 转为小写, 以免一些大小写相似的字母导致用户输入错误
viewModel.setCode(code = code.lowercase())
...省略代码...
ViewModel 代码,比较简单:
class MyViewModel : ViewModel() {
private var verifyCode by mutableStateOf("")
fun setCode(code: String) {
verifyCode = code
}
fun verify(input: String) = input.lowercase() == verifyCode
}
verify() 用于验证。 验证使用:
@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 = "点我点我")
}
}
}
viewModel 在 activity 中构建后传入,在使用 TextField 我遇到过输入不能显示的问题,有兴趣可以移步Compose | TextField 无法显示输入内容看看,最好可以帮我解答一下,哈哈。看看我们的结果吧: 最后还有一个功能,就是我们平常都能看到点击验证码会给一个新的验证码。这要怎么实现呢?这不是很简单吗,利用 Compose 的响应式编程啊,像这样: 和点击有关的加上括号一共也就 7 行,这么短能实现吗,我们看看结果: 敢放出来当然能实现啦。这里要注意最后面的 flag 虽然像旗一样插在那什么都没干,但是我们不能删去它,它就是响应式编程的精髓,当程序检测到它变化时就会进行重绘。这里的 remember 和 mutableStateOf 要是不懂可以看看我另一篇文章Compose | remember、mutableStateOf的使用比较基础,要是写的不好也请指教。
到这我们功能也实现啦。
四、完整代码
这里就放一下核心的代码,就不往 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", 标签:
高压电阻器5w500m