Написать | Напишите код подтверждения самостоятельно

предисловие

Когда я работал над функцией входа в систему, я хотел получить проверочный код для входа в проверочный код.Я бродил по Интернету и, кажется, я не написал его в Compose (может быть, я не нашел его) , так как у меня его нет, я сделаю его сам. Если у вас нет времени или нет хорошей основы, вы можете перейти непосредственно к полному коду , чтобы увидеть основной код, потому что реализация относительно проста и повторяема. Конечно, я также приветствую вас, чтобы прочитать мою статью и изучить ее шаг за шагом.


1. Выбор инструмента

Большая часть интернета реализована с помощью пэйнта, но свойства пэйнта в Compose как бы урезаны, например у textSkewX нет (следующее Compose):

вставьте сюда описание изображения вставьте сюда описание изображения

Так как это не лучший способ использовать краску. Наконец, я думаю, что код подтверждения обычно включает буквы и цифры, поэтому просто используйте самый простой текст плюс холст.


2. Основная идея

Самое главное в проверочном коде — это случайность, так как же нам добиться случайности? Разве это не очень просто, используйте Random. Чем может отличаться стиль проверочного кода? Разве это не очень просто, используйте атрибут Random +. Итак, нам нужно только перечислить свойства Text и добавить Random, чтобы получить базовый стиль кода подтверждения:

вставьте сюда описание изображения

Для чего используется холст, упомянутый выше? На самом деле он используется для рисования интерференционных линий. Конечный эффект выглядит так (должно быть в порядке):

вставьте сюда описание изображения

Возможно, есть идеи получше, но я не буду. Давайте возьмем пример, чтобы поговорить о конкретной реализации.


В-третьих, конкретная реализация

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. Настройки текста

На основе упомянутой выше идеи Random + properties. Сначала мы получаем все свойства 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
    )
复制代码

Ниже приведены все значения используемых атрибутов.Если вы хотите увидеть, вы можете перейти к полному коду , чтобы взглянуть и вернуться, чтобы продолжить обучение.

вставьте сюда описание изображения

Добавить случайный:

    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 передается после создания в действии.При использовании TextField я столкнулся с проблемой, что ввод не может быть отображен.Если вам интересно, вы можете переместить ( Compose | TextField не может отображать входное содержимое ) и посмотреть , Лучше помочь мне ответить на него, ха-ха. Проверьте наши результаты:

вставьте сюда описание изображения

Наконец, есть еще одна функция, то есть мы обычно видим, что нажатие на код подтверждения дает новый код подтверждения. Как этого добиться? Не проще ли использовать реактивное программирование Compose, например:

вставьте сюда описание изображения

Всего строк со скобками, связанных с кликами, 7. Можно ли этого добиться за такое короткое время? Посмотрим на результаты:

вставьте сюда описание изображения

Не бойтесь выпустить его, конечно, это может быть достигнуто. Здесь следует отметить, что хотя последний флаг и вставляется как флаг, он ничего не делает, но удалить его мы не можем, в этом суть реактивного программирования, когда программа обнаружит, что он изменился, она будет перерисована. Если вы не понимаете, что такое запоминание и mutableStateOf здесь, вы можете прочитать мою другую статью ( Compose | запомнить, использование mutableStateOf ) для более простого сравнения.Пожалуйста, сообщите, если они не очень хорошо написаны.

На данный момент наша функция также была реализована.


4. Полный код

Вот основной код, а не на 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()
    }
}
复制代码

наконец

Статья здесь, надеюсь, она будет вам полезна, добро пожаловать в комментарии, пока!

рекомендация

отjuejin.im/post/7083063918402207758