Compose | Write a verification code yourself

foreword

When I was working on the login function, I wanted to get a verification code to log in to the verification code. I wandered around the Internet and it seems that I haven't written it in Compose (maybe I haven't found it), since I don't have one, I'll make one myself. If you don't have time or have a good foundation, you can go directly to the complete code to see the core code, because the implementation is relatively simple and repetitive. Of course, I also welcome you to read my article and learn it step by step.


1. Tool selection

Most of the Internet is implemented with paint, but the properties of paint in Compose seem to be reduced, for example, textSkewX does not (the following is Compose):

insert image description here insert image description here

Since this is not a good way to use paint. Finally, I think that the verification code generally includes letters and numbers, so just use the simplest Text plus canvas.


2. Basic idea

The most important thing in the verification code is randomness, so how do we achieve randomness? Isn't this very simple, use Random. How can the style of the verification code be different? Isn't this very simple, use Random + attribute. So we only need to list the properties of Text and add Random to get the basic style of the verification code:

insert image description here

What is the canvas mentioned above used for? It is actually used to draw interference lines. The final effect is like this (should be ok):

insert image description here

There may be better ideas, but I won't. Let's take an example to talk about the specific implementation.


Third, the specific implementation

0. Parameter explanation

Here first put the parameters required to finally implement the verification code and explain it, so that everyone can read it later:

@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. Verify the content

The first thing to implement, of course, is to verify something, like this:

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"
    )
复制代码

We use numbers and letters for verification, and I will randomly select codeNum to be used as verification codes later.

2. Text settings

Based on the idea of ​​Random + properties mentioned above. We first get all the properties of Text:

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

And list all the values ​​that will be assigned to the property (here takes fontFamily as an example):

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

The following are all the values ​​of the attributes used. If you want to see, you can go to the complete code to take a peek and go back to continue learning.

insert image description here

Add 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 是一个单例类:

insert image description here

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

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 千万不要设置太大,不然你就是为难用户:

insert image description here

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

4、Code 单例类中的注意点

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

insert image description here

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

insert image description here

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

5、初步测试

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

insert image description here

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

6、功能实现

To realize the verification function, we need to save the verification code first. We can use the ViewModel to store the randomly generated verification code. The randomly generated verification code should be connected into a string. Do this:

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

Then save:

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

ViewModel code, relatively simple:

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

verify() is used for verification. Verify using:

@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 = "点我点我")
        }
    }
}
复制代码

The viewModel is passed in after being constructed in the activity. When using TextField, I have encountered the problem that the input cannot be displayed. If you are interested, you can move ( Compose | TextField cannot display the input content ) and take a look. It is best to help me answer it, haha. Check out our results:

insert image description here

Finally, there is another function, that is, we can usually see that clicking on the verification code will give a new verification code. How can this be achieved? Isn't it easy, use Compose's reactive programming, like this:

insert image description here

There are 7 lines in total with parentheses related to clicks. Can it be achieved in such a short time? Let's see the results:

insert image description here

Dare to let it out, of course it can be achieved. It should be noted here that although the last flag is inserted like a flag, it does nothing, but we cannot delete it. It is the essence of reactive programming. When the program detects that it changes, it will be redrawn. If you don't understand remember and mutableStateOf here, you can read my other article ( Compose | remember, the use of mutableStateOf ) for a more basic comparison. Please advise if they are not well written.

At this point, our function has also been implemented.


4. Complete code

Here is the core code, not on 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()
    }
}
复制代码

finally

The article is here, I hope it will be helpful to you, welcome to comment, bye!

Guess you like

Origin juejin.im/post/7083063918402207758