preface

For example: Compose is a user user who is trying to create a verification code for the login function. For example: Compose is a user user who is trying to create a verification code for the login function. If you do not have the time or a good foundation can directly go to the complete code to see the core code, because the implementation is relatively simple, but also more repetitive. Of course, I also welcome you to read my article and I learn step by step.


1. Tool selection

For example, in Compose, textSkewX does not have any paint properties:

That’s not a good way to use paint. Finally, the verification code usually includes letters and numbers, so we can use the simplest Text plus canvas.


2. Basic Ideas

The most important thing about captchas is randomness, so how do we do randomness? That’s easy. Random. So how can captchas be different? Isn’t that easy? Random + property. So we just list the properties of Text with Random to get the basic style of the captchas:

What is the canvas used for? It is actually used to draw interference lines. The final effect looks like this.

I might have a better idea, but I don’t. Let’s take an example of an implementation.


Third, concrete implementation

0. Parameter description

Here first put the final implementation of the verification code required parameters and explain, for everyone to read:

@RequiresApi(Build.VERSION_CODES.Q)
@OptIn(ExperimentalUnitApi::class)
@Composable
fun VerifyCode(
    // Width and height need not be explained
    width: Dp,
    height: Dp.// The offset from the upper left corner, used for positioning
    topLeft: DpOffset = DpOffset.Zero,
    // The number of verification codes
    codeNum: Int = 4.// The number of interference lines
    disturbLineNum: Int = 10.// Saves the verification code for user input verification
    viewModel: MyViewModel
) {}
Copy the code

1. Verify the content

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

private val codeList = listOf(
        "0"."1"."2"."3"."4"."5"."6"."Seven"."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"
    )
Copy the code

We do this using numbers and letters, but I’m going to select codeNum at random later on.

2. Text Settings

Based on the Random + attribute thought above. We first get all the properties of Text:

Text(
    text = ,
    modifier = ,
    color = ,
    fontSize = ,
    fontStyle = ,
    fontWeight = ,
    fontFamily = ,
    textDecoration = ,
    textAlign = ,
    letterSpacing = ,
    lineHeight = ,
    maxLines =,
    onTextLayout =,
    style =,
)
Copy the code

And lists all the values that will be assigned to the attribute (here using fontFamily as an example) :

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

Here are all the values for each of the attributes that are used, and if you want to see them you can go to the full code and take a peek and come back.

Combined with the Random:

    private fun <T> List<T>.getRandom(a) = this[Random.nextInt(this.size)]
The shuffled() function returns a new List containing the set elements sorted in random order
// private fun 
      
        List
       
        .getRandom() : T = this.shuffled().take(1)[0]
       
      
Copy the code

Here we use Kotlin’s extension function (which is really cool to use), and there are two ways you can write it. The final result is this:

Text(
    text = Code.getCode(),
    modifier = Modifier
        .width(width / codeNum)
        .height(height)
        .offset(topLeft.x + dx, topLeft.y),
    color = Code.getColor(),
    // What fontSize requires is that TextUnit needs to convert dp to SP
    // use min() to ensure that characters are seen
    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(),
    // Since we have only one character in Text, some attributes are not necessary
    // letterSpacing = ,
    // lineHeight = ,
    // maxLines =,
    // onTextLayout =,
    // style =,
)
Copy the code

X and topLeft. Y, captcha can’t always be in the upper left corner. Here Code is a singleton class:

Easy to use for encapsulation methods. Finally, add:

repeat(codeNum) {}
Copy the code

We need the codeNum to run on a character, and it should run one character at a time from code.getCode (), otherwise all the characters will be the same. So that’s where we’ll implement Text.

3. Realization of interference line

Let’s start with the code:

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 this, for each line startOffset and endOffset are the same
        // Many times there is only one line, so we carry it out
        // repeat(disturbLineNum)
        drawLine(
        // I'm using brush for both
        // color = Code.getColor(),
            brush = Brush.linearGradient(
                Code.getColorList()
            ),
            start = startOffset,
            end = endOffset,
            strokeWidth = strokeWidth,
            cap = Code.getCap(),
        )
    }
}
Copy the code

So here we have the starting and ending positions, and then the drawLine is pretty easy. TopLeft. X and topLeft. Y. See you again when using miley Blinenum. Never set it too large, or you’ll embarrass the user:

This verification code is afraid of people to see it?

4, Code singleton class points to note

The opacity in getColor() can’t be set too low (I won’t set it directly), it’s not very clear, for example:

Can you see it? In getColorList(), the lower limit of random must be greater than 1, otherwise:

Is red terrible? This is because brush.lineargradient () requires more than two colors. If you’re curious about the Code singleton class, you can look at the full Code and then come back to it. It’s almost over. In addition, in the Code singleton class dp, SP, PX conversion we can learn, before this I do not know.

5. Preliminary test

Here we have a verification code that looks like this, but does not have a function yet. We will implement it next.

Obviously there is nothing wrong, and the captcha looks good (WDBMNUM1). Then we implement the function, after all, no matter how beautiful the captchas are, they are not taken to see.

6. Function realization

To implement the verification function we first need to save the verification code, we can use the ViewModel to store the randomly generated verification code, the randomly generated verification code to string, do this:

. Omit code...var code = ""
	repeat(codeNum) {
		valoneCode = Code.getCode() code += oneCode ... Omit code... }Copy the code

Then save:

. Omit code...// Change the code to lower case so that some letters with similar case will not cause user input errorsviewModel.setCode(code = code.lowercase()) ... Omit code...Copy the code

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
}
Copy the code

Verify () is used to verify. 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, "Input correctly", Toast.LENGTH_SHORT).show()
            } else {
                Toast.makeText(context, "Input error", Toast.LENGTH_SHORT).show()
            }
        }) {
            Text(text = "Dot me dot me.")}}}Copy the code

ViewModel building which is passed to in the activity, in the use of TextField input can’t display question, one that I have ever seen who are interested in to the (Compose | TextField unable to display the input), best can help me to solve, ha ha. Take a look at our results:

And then finally, there’s a feature that we usually see that clicking on a captcha gives you a new captcha. How do you do that? Wouldn’t it be easy to take advantage of Compose’s responsive programming, like this:

That’s 7 lines with parentheses. Can you do that? Let’s look at the result:

Of course it will happen if you let it out. Notice that the flag at the end is there like a flag and it’s not doing anything, but we can’t get rid of it, because it’s the essence of responsive programming, and it redraws when it detects that it’s changed. Remember and mutableStateOf here if you don’t understand can I see another article (Compose | remember, use of mutableStateOf) comparative basis, if writing is bad also, please advise.

So now we have the functionality.


Four,The complete code

Here’s 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 and height need not be explained
    width: Dp,
    height: Dp.// The offset from the upper left corner, used for positioning
    topLeft: DpOffset = DpOffset.Zero,
    // The number of verification codes
    codeNum: Int = 4.// The number of interference lines
    disturbLineNum: Int = 10.// Saves the verification code for user input verification
    viewModel: MyViewModel
) {
    var flag by remember {
        mutableStateOf(-1)
    }
    Box(
        modifier = Modifier
            .width(width)
            .height(height)
            .offset(topLeft.x, topLeft.y)
            .clickable {
                flag = -flag
            }
    ) {
        // For reactive programming, redraw captchas
        flag
        var dx = 0.dp
        var code = ""
        repeat(codeNum) {
            // Get a single character. We cannot get a codeNum character directly, otherwise the style will be the same
            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(),
                // What fontSize requires is that TextUnit needs to convert dp to SP
                // use min() to ensure that characters are seen
                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(),
                // Since we have only one character in Text, some attributes are not necessary
// letterSpacing = ,
// lineHeight = ,
// maxLines =,
// onTextLayout =,
// style =,
            )
            // dx plus the width of Text to prevent stacking
            dx += width / codeNum
        }

        // Change the code to lower case so that some letters with similar case will not cause user input errors
        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 this, for each line startOffset and endOffset are the same
                // Many times there is only one line, so we carry it out
// repeat(disturbLineNum)
                drawLine(
                    // I'm using brush for both
// 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"."Seven"."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(a) = this[Random.nextInt(this.size)]
The shuffled() function returns a new List containing the set elements sorted in random order
// private fun 
      
        List
       
        .getRandom() : T = this.shuffled().take(1)[0]
       
      

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

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

    fun getFontWeight(a) = fontWeightList.getRandom()

    fun getFontFamily(a) = fontFamilyList.getRandom()

    fun getTextDecoration(a) = textDecorationList.getRandom()

    fun getTextAlign(a) = textAlignList.getRandom()

    fun getColor(a) = Color(
        red = Random.nextInt(256),
        green = Random.nextInt(256),
        blue = Random.nextInt(256),
        // The opacity is not very clear, so discard it
// alpha = Random.nextInt(256)
    )

    fun getColorList(a): ArrayList<Color> {
        val colorList = arrayListOf<Color>()
        // If the minimum value is 2, an error will be reported if the size of colorList = 1
        repeat(Random.nextInt(2.11)) {
            colorList.add(getColor())
        }
        return colorList
    }

    fun getCap(a) = 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()
    }
}
Copy the code

The last

Article to this, I hope to help you, welcome to comment, bye!