前言

在搞登录功能时,想弄个登录验证码,在网上溜了一圈好像还没有用 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.currentRow(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}) {// 用于响应式编程,重绘验证码flagvar dx = 0.dpvar code = ""repeat(codeNum) {// 得到单个字符, 不能直接得到 codeNum 个字符, 不然样式是一样的val oneCode = Code.getCode()code += oneCodeText(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()@Composablefun getTextUnit(minDp: Dp, maxDp: Dp) = with(LocalDensity.current) {val min = minDp.roundToPx()val max = maxDp.roundToPx()Random.nextInt(min, max + 1).toSp()}@Composablefun 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())}@Composablefun getStrokeWidth(min: Dp, max: Dp) = with(LocalDensity.current) {val min = min.roundToPx()val max = max.roundToPx()Random.nextInt(min, max + 1).toFloat()}
}

最后

文章就到这,希望对你有帮助,欢迎评论,拜拜!

Compose Text + Canvas 写个验证码相关推荐

  1. 封装构造函数,用canvas写饼状图和柱状图

    封装构造函数,用canvas写饼状图和柱状图 封装函数 // 场景 function XDLScence( options ) {this.stage = options.stage;//执行场景的初 ...

  2. python写个验证码

    用python写一个验证码功能 分析: 1.验证码背景 2.验证码包含26个字母大小写以及0-9十个数字 扩展需求:3.添加滤镜模糊等 这里使用python中专门处理图片的PIL库 from PIL ...

  3. html验证码图片,js+h5 canvas实现图片验证码

    本文实例为大家分享了js+h5 canvas实现图片验证码的具体代码,供大家参考,具体内容如下 实现效果 一.使用技术 原生js技术+html5 canvas画图 利用Math.random()函数随 ...

  4. canvas——实现图片验证码(功能实现)

    canvas实现图片验证码--效果图如下: 1.html部分代码 <div class="wrapper"><div class="inputBox&q ...

  5. 用Compose实现手写春联效果

    /   今日科技快讯   / 近日,尽管"元宇宙"这个词已经存在了近30年,但直到Facebook在2021年10月下旬更名为Meta,似乎才重新点燃了投资者对它的兴趣.其中,虚拟 ...

  6. 【情人节用Compose给女神写个爱心动画APP】

    情人节用Compose给女神写个爱心动画APP 前言 涉及知识点 实现思路 实现过程 绘制爱心 创建动画效果 Preview预览效果 完整源码 彩蛋 前言 前一阵子看电视里的学霸用代码写了个炫酷的爱心 ...

  7. 用Java写的验证码程序

    用java实现的给浏览器响应验证码程序.并且是实现了可以点击验证码图片换一张验证码. 最后边给出了完整的代码. //首先定义一个自己的类并且去继承HttpServlet这个类 public class ...

  8. html text width,HTML5 Text Canvas rotate in case text width is larger than maximum width allowed

    问题 Friends, i'm finding rotating a text canvas object a bit tricky. The thing is, I'm drawing a grap ...

  9. 由几个月前写的(验证码利用ashx一般处理程序来做),修改为不用以一般处理程序...

    这段代码是我根据几个月前 ,我写的"验证码 利用ashx一般处理程序来实现"的一片随笔所想: 见代码: Page:check.aspx protected void Page_Lo ...

最新文章

  1. matlab将二值图像与原图重叠_[转载]图像处理matlab及图像融合图像镶嵌图像拼接...
  2. 更新代码到gitlab上
  3. linux 手动配置ip地址方法
  4. Two Arrays and Sum of Functions
  5. 七阶拉丁方阵_最强大脑七阶立方之战规则解析 清华天才对其了如指掌
  6. OpenCV 图像清晰度评价算法(相机自动对焦)
  7. 学计算机要不要护发,脱发平时应该注意什么 四个妙招教你如何防止脱发
  8. [蓝桥杯2015决赛]五星填数-枚举+数论
  9. 一步步编写操作系统 07 开机启动bios
  10. java的安装和配置
  11. 【kafka】WARN Attempting to send response via channel for which there is no open connection
  12. java中html网页转化成pdf(itext)
  13. linux下dbf是什么文件,DBF文件怎么打开?DBF文件的打开方法教程
  14. android学习笔记----手机号码查询归属地
  15. java oracle spatial_安装Oracle Spatial数据组件
  16. 基于阿里云生活物联网平台的智能家居(物联网,智能家居,STM32,阿里云生活物联网平台,人脸识别,语音识别,语音交互)
  17. 华为交换机dhcp获取不到_华为S7706交换机DHCP Server 配置不成功问题
  18. 5、实现登陆功能:如果用户名输入“tom”,密码“123”,提示登陆成功,否则提示输入错误,请重新输入!您还有XX次机会。 三次输入错误后将不可以再输入,并提示对不起,你的账号将被锁定
  19. 侍魂qq最新服务器,2018腾讯发布火影、圣斗士星矢、侍魂等20款新手游
  20. 深度学习小白如何看懂神经网络模型/图?

热门文章

  1. css的div,span,img,a,map等一些常见标签使用 回顶设置
  2. 【ReID】【代码注释】HA-CNN的网络模型 deep-person-reid/models/HACNN.py
  3. php开发宝典例8.2,PHP学习宝典-第八章(二)
  4. Linux的Bash——(三)命名别名与历史命令
  5. vim全插件 linux,[转载]安装vim插件(linux下)
  6. Python开发培训哪家好
  7. excel单元格内容拆分_Excel表格技巧—如何统计有内容的单元格数量
  8. python: 测量代码运行时间
  9. 基于docker的influxdb时序数据库搭建
  10. 姿态变换(一)——旋转矩阵