当前位置: 首页 > news >正文

Flutter笔记:手写并发布一个人机滑动验证码插件

Flutter笔记
手写一个人机滑块验证码

作者李俊才 (jcLee95):https://blog.csdn.net/qq_28550263
邮箱 :291148484@163.com
本文地址:https://blog.csdn.net/qq_28550263/article/details/133529459


写 Flutter 项目时,遇到需要滑块验证码功能。滑块验证码属于人机验证码的一种,看起来像是在一个图片中“挖去”了一块,然后通过用户手动操作滑块,让被“挖去”的部分移回来。由于我不想使用各种第三方模块,因此决定自己实现一个初版以后慢慢添砖加瓦。本文是对第一个版本的一点记录。

http://thispage.tech:9680/jclee1995/flutter-jc-captcha/-/raw/master/WindowsTerminal_hiA0nG4LIy.gif


1. 概述

1.1 关于本文

1.2 什么是人机验证码

概念

Flutter 开发中,使用人机验证码(也称为 CAPTCHA,即 Completely Automated Public Turing test to tell Computers and Humans Apart)通常是为了增强应用程序的安全性和防止恶意活动。

目的

  1. 防止自动化攻击:恶意用户和自动化脚本可以尝试大规模攻击应用程序,例如注册多个虚假帐户、暴力破解密码、滥发垃圾邮件或提交虚假表单。人机验证码可以帮助阻止这些自动化攻击,因为它们要求用户证明自己是真人而不是机器;

  2. 防止垃圾数据输入:人机验证码可以确保用户提交的数据是有效和真实的。例如,在用户注册过程中,验证码可以防止恶意用户自动化注册虚假帐户,从而保护应用程序的数据质量;

  3. 防止滥用资源:如果应用程序提供某种资源,如 API 访问或文件下载,希望防止单个用户或恶意机器人滥用这些资源。通过要求用户在访问这些资源之前进行验证码验证,可以限制滥用的可能性;

  4. 增加安全性:在某些情况下,用户可能需要进行敏感操作,如更改密码、恢复帐户或进行金融交易。在这些情况下,验证码可以提供额外的安全性,确保只有授权用户可以执行这些操作;

Flutter 中实施人机验证码通常涉及使用插件或集成第三方服务,这些服务提供了生成和验证验证码的功能。

总之,人机验证码是一种重要的安全措施,可帮助保护 Flutter 应用程序免受各种恶意活动的威胁,并提高用户数据的质量和应用程序的整体安全性。

滑动验证码

1.3 项目地址

  1. flutter pub: https://pub.dev/packages/jc_captcha
  2. git lab: http://thispage.tech:9680/jclee1995/flutter-jc-captcha

2. 先看使用方法

2.1 安装

flutter pub add jc_captcha

2.2 编码

import 'package:flutter/material.dart';
import 'package:jc_captcha/jc_captcha.dart';void main() {runApp(const MyApp());
}class MyApp extends StatelessWidget {const MyApp({super.key});Widget build(BuildContext context) {return MaterialApp(home: Scaffold(appBar: AppBar(title: const Text('Captcha Plugin Example'),),body: CaptchaWidget(imageUrl:'http://thispage.tech:9680/jclee1995/flutter-jc-captcha/-/raw/master/example/test_picture.png',onSuccess: () {print('验证成功');},onFail: () {print('验证失败');},),),);}
}

说明:

验证码仅验证一次即失效:

验证码有两个状态,且状态该转换是一次性的,即:

未验证 =》 已验证
  • 从未验证到已验证仅仅转换一次,转换结果有验证成功和验证失败;
  • 如果验证成功,则执行成功回调 onSuccess
  • 如果验证失败,则执行失败回调 onFail

效果如图所示:
http://thispage.tech:9680/jclee1995/flutter-jc-captcha/-/raw/master/WindowsTerminal_hiA0nG4LIy.gif

3. 功能描述

3.1

4. 实现思路分析

4.1 分析问题比解决问题重要:抠出一块图形是抠吗

抠出小块图是不是一定要从原图像那里拷贝一块像素?你当然可以这样来实现,不过会复杂一些。不过还有一种假设是,抠出的拼图部分不是抠出的,而是原原本本的一张图。对比而言:

方案1: 从原图像中拷贝一块像素

  1. 原始图像处理: 加载原始验证码图像,该图像包括背景图像和一个需要滑动的小块图像。

  2. 拷贝小块图像: 使用Flutter的图像处理功能,将原始图像中的小块图像精确拷贝出来,并将其作为验证码的滑块。这可以通过裁剪原始图像的一部分来实现。

  3. 验证用户拖动: 当用户尝试拖动滑块时,需要检查滑块的位置是否与原始图像中的小块位置匹配。这可以涉及比较滑块的位置与小块的位置是否一致。

方案2: 将两张图叠加

  1. 原始图像处理: 同样,加载原始验证码图像,包括背景图像和一个需要滑动的小块图像。

  2. 自定义布局: 使用Flutter的布局和图层叠加功能,将两张图像堆叠在一起,确保小块图像与背景图像的对齐。这可以通过使用Stack小部件或Positioned小部件来实现。

  3. 验证用户拖动: 当用户尝试拖动滑块时,需要检查滑块的位置是否与小块图像的位置匹配。这可以通过比较滑块的位置是否在小块图像的位置上来实现。

两种方案的选择取决于项目需求和个人偏好。我个人觉得方案2更加具有可操作性,因此我后续是基于这种方法来实现的。

4.2 知识准备

这里对用到得一些 Flutter 基础知识做简单介绍,方便初学读者了解学习相关知识。

1. 堆叠布局

在Flutter中,堆叠布局(Stack布局)是一种常用的布局方式,用于将多个子部件叠加在一起。堆叠布局允许您将子部件以层叠的方式排列,每个子部件可以覆盖或部分覆盖其他子部件,从而创建复杂的布局效果。在Flutter中,堆叠布局由两个主要组件构成:

  1. Stack(堆叠): Stack是一个容器小部件,用于包含子部件并按照它们的绘制顺序将它们叠加在一起。子部件按照从底部到顶部的顺序堆叠。您可以使用children属性来指定要叠加的子部件列表。

  2. Positioned(定位): Positioned小部件用于控制子部件在Stack中的位置。它允许您指定子部件的左、上、右和下边距,从而将子部件精确定位在Stack上。

在堆叠布局中,子部件的位置和大小是通过Positioned小部件来控制的。每个Positioned小部件都必须包含一个左、上、右和下的属性,以确定子部件在Stack中的位置和大小。

  • left:指定子部件的左边距。
  • top:指定子部件的上边距。
  • right:指定子部件的右边距。
  • bottom:指定子部件的下边距。

这些属性可以设置为null,以便自动确定位置,或者设置为具体的值,以确保子部件在Stack中的精确定位。

例如,下面的代码展示了如何将两个容器叠加在一起:

Stack(alignment: Alignment.center,children: [Container(width: 200,height: 200,color: Colors.blue,),Positioned(left: 50,top: 50,child: Container(width: 100,height: 100,color: Colors.red,),),],
)

在上述示例中,Stack包含两个容器,一个蓝色的大容器和一个红色的小容器。通过Positioned小部件,我们将小容器定位到了大容器的左上角。

2. Slider组件

Flutter的Slider(滑块)组件是一个用于选择一个范围内数值的交互式控件。用户可以通过滑动滑块来选择数值,这使得它在用户界面中用于调整设置和选择数值非常有用。

Slider组件主要有以下功能:

  1. 数值范围选择: Slider允许用户在指定的数值范围内进行选择。用户可以通过滑动滑块来选择一个数值,该数值通常表示某种设置或参数。

  2. 分割刻度: Slider可以显示刻度线,并且可以根据需要进行分割。这些刻度线使用户更容易准确地选择所需的数值。

  3. 标签显示: Slider可以显示当前数值的标签,通常位于滑块上方或下方。这有助于用户了解所选择的数值。

  4. 回调函数: 您可以为Slider设置一个回调函数,当用户拖动滑块时,会触发该函数。这使得您可以在数值发生变化时执行特定的操作,如更新UI或应用程序状态。

  5. 自定义样式: Slider具有丰富的自定义样式选项,您可以更改滑块、轨道和刻度的颜色、形状和大小,以适应您的应用程序设计。

以下是Flutter Slider组件的一些常用属性:

  • value:表示当前的数值,可以通过设置此值来控制Slider的位置。
  • onChanged:一个回调函数,当用户拖动滑块时触发,用于处理数值的变化。
  • min:Slider的最小值。
  • max:Slider的最大值。
  • divisions:用于将Slider轨道分割为多少个离散步骤,通常与滑块刻度一起使用。
  • label:显示在滑块上方或下方的标签,通常用于显示当前值。
  • activeColor:激活状态下(滑块被拖动)的颜色。
  • inactiveColor:非激活状态下的颜色。
  • thumbColor:滑块的颜色。
  • thumbShape:滑块的形状,可以是圆形、方形等。
  • trackHeight:轨道的高度。

例如:

class SliderExample extends StatefulWidget {const SliderExample({super.key});State<SliderExample> createState() => _SliderExampleState();
}class _SliderExampleState extends State<SliderExample> {double _currentSliderValue = 20;Widget build(BuildContext context) {return Scaffold(appBar: AppBar(title: const Text('Slider')),body: Slider(value: _currentSliderValue,max: 100,divisions: 5,label: _currentSliderValue.round().toString(),onChanged: (double value) {setState(() {_currentSliderValue = value;});},),);}
}

这个示例是一个基本的Flutter应用程序,来自于Flutter官方。

  • 当应用程序启动时,Slider的初始值是20。用户可以通过按住并拖动滑块来选择不同的数值。
  • 滑块的最小值是0,最大值是100,滑块上有5个刻度线,表示5个离散的数值。当用户拖动滑块时,滑块的值会随着手指的拖动而实时更新。
  • 在滑块的上方显示一个标签,显示当前所选数值的整数部分,例如,当用户将滑块移动到25时,标签显示"25"。
  • 滑块的外观由activeColor(激活状态下的颜色)和inactiveColor(非激活状态下的颜色)属性定义。在示例中,激活状态下的颜色为蓝色,非激活状态下的颜色为灰色。

3. Flutter 绘图(canvas)

在Flutter中,您可以使用Canvas来进行绘图操作,Canvas是Flutter中的绘图上下文,允许您在屏幕上绘制各种形状、文本和图像。Canvas通常与CustomPaint小部件一起使用,以在Flutter的绘图流程中插入自定义绘图代码。

使用Canvas进行绘图的基本步骤包括:

  1. 创建一个CustomPaint小部件: 首先,您需要在Flutter应用程序的UI层次结构中插入一个CustomPaint小部件,以便将绘图内容放入其中。

  2. 自定义Painter: 您需要创建一个自定义的Painter类,它继承自CustomPainter,并实现paint和shouldRepaint方法。paint方法是您用来实际绘制内容的地方,shouldRepaint方法决定是否需要重新绘制。

  3. 在paint方法中绘制内容: 在paint方法中,您可以使用Canvas对象来进行各种绘图操作,如绘制图形、文本、路径等。Canvas提供了各种方法来绘制不同类型的图形。

例如,下面得代码展示了如何在Flutter中使用Canvas绘制一个简单的圆形:

import 'package:flutter/material.dart';void main() {runApp(const MyApp());
}class MyCustomPainter extends CustomPainter {void paint(Canvas canvas, Size size) {final paint = Paint()..color = Colors.blue..style = PaintingStyle.fill;final centerX = size.width / 2;final centerY = size.height / 2;final radius = size.width / 3;canvas.drawCircle(Offset(centerX, centerY), radius, paint);}bool shouldRepaint(CustomPainter oldDelegate) {return false;}
}class MyApp extends StatelessWidget {const MyApp({super.key});Widget build(BuildContext context) {return MaterialApp(home: Scaffold(appBar: AppBar(title: const Text('Canvas绘图示例'),),body: Center(child: CustomPaint(size: const Size(200, 200),painter: MyCustomPainter(),),),),);}
}

上述示例中,MyCustomPainter类继承自CustomPainter,并在其paint方法中绘制了一个蓝色的圆形。然后,CustomPaint小部件将MyCustomPainter作为其painter属性的值传递,并在UI中显示绘制的圆形。

通过Canvas和CustomPainter,您可以创建各种自定义绘图效果,包括图表、动画、自定义图形和复杂的UI元素。Canvas提供了丰富的绘图功能,可以满足各种绘图需求。

4. Flutter 裁剪(Clip)

Flutter中的裁剪(Clip)是一种用于控制Widget可见区域的技术,它可以用来创建各种不同形状和效果的UI元素。裁剪允许您定义一个区域,只有在该区域内的部分内容才会被显示,超出该区域的内容会被裁剪掉。Flutter提供了多种不同类型的裁剪小部件,以满足各种需求。

以下是一些常见的Flutter裁剪小部件和其用途:

  1. ClipRect: 这是最常见的裁剪小部件之一,它可以将其子部件裁剪为矩形形状。使用它可以创建各种矩形裁剪效果,例如将图像限制在矩形区域内。

  2. ClipOval: 这个小部件可以将其子部件裁剪为椭圆形状,用于创建椭圆形的UI元素,如头像或按钮。

  3. ClipRRect: ClipRRect用于创建带有圆角的矩形裁剪,可以用来创建圆角矩形框或卡片。

  4. ClipPath: ClipPath允许您自定义裁剪区域的形状,通过提供一个自定义路径来实现各种复杂的裁剪效果。

这里是一个示例,展示如何使用ClipRect来裁剪一个图像以显示在矩形区域内:

ClipRect(child: Image.network('https://example.com/image.jpg',width: 200,height: 200,fit: BoxFit.cover,),
)

上述示例中,ClipRect将Image小部件裁剪为矩形区域内的可见部分。您可以使用其他Clip类型来创建不同形状和效果的裁剪。

裁剪在创建各种自定义UI效果时非常有用,例如创建特定形状的按钮、卡片或背景。通过使用不同的Clip类型,您可以实现各种各样的外观和动画效果,从而增强Flutter应用程序的用户界面。

5. 基本实现

5.1 实现代码

/// 作者:李俊才
/// 邮箱:291148484@163.com
/// 项目地址:http://thispage.tech:9680/jclee1995/flutter-jc-captcha
/// 协议:MIT
import 'dart:math';
import 'package:flutter/material.dart';/// 验证码组件
///
/// 这个组件用于显示一个验证码图像,用户需要滑动滑块以解锁验证。当验证成功或失败时,
/// 分别触发 [onSuccess] 或 [onFail] 回调函数。你可以设置允许的误差范围 [deviation]
/// 以调整验证的精确性。
class CaptchaWidget extends StatefulWidget {/// 用作验证图像的URLfinal String imageUrl;/// 当验证成功时触发的回调函数。final Function() onSuccess;/// 当验证失败时触发的回调函数。final Function() onFail;/// 允许的误差范围,用于调整验证的精确性。static double deviation = 5;/// 创建一个 [CaptchaWidget] 小部件,需要指定 [imageUrl]、[onSuccess] 和 [onFail] 回调函数。const CaptchaWidget({Key? key,required this.imageUrl,required this.onSuccess,required this.onFail,}) : super(key: key);State<CaptchaWidget> createState() => _CaptchaWidgetState();
}class _CaptchaWidgetState extends State<CaptchaWidget> {/// 滑块的当前位置。double _sliderValue = 0.0;late double _offsetRate;/// 用于定位的偏移值。late double _offsetValue;/// 小部件的总宽度。late double width;/// 用于确保验证仅仅一次有效bool _verified = false;double _generateRandomNumber() {// 创建一个Random对象var random = Random();// 生成一个介于0.1和0.9之间的随机小数double randomValue = 0.1 + random.nextDouble() * 0.7;return randomValue;}void initState() {_offsetRate = _generateRandomNumber();super.initState();}Widget build(BuildContext context) {width = MediaQuery.of(context).size.width;_offsetValue = _offsetRate * width;return Column(children: [// 堆叠三层,背景图、裁剪的拼图、拼图的轮廓绘图Stack(alignment: Alignment.center,children: [// 背景图层Image.network(widget.imageUrl,height: 200.0,fit: BoxFit.cover,),// 背景标记层CustomPaint(size: Size(width, 200.0),painter: CaptchaBorderPainter(_offsetValue),),// 拼图层Positioned(left: _sliderValue * width - _offsetValue,child: ClipPath(clipper: CaptchaClipper(_sliderValue, _offsetValue),child: Image.network(widget.imageUrl,height: 200.0,fit: BoxFit.cover,),),),// 拼图的轮廓层Positioned(left: _sliderValue * width - _offsetValue,child: CustomPaint(size: Size(width, 200.0),painter: CaptchaBorderPainter(_offsetValue),),),],),//SliderTheme(data: SliderThemeData(thumbColor: Colors.white, // 滑块颜色为白色activeTrackColor: Colors.green[900], // 激活轨道颜色为深绿色inactiveTrackColor: Colors.green[900], // 非激活轨道颜色为深绿色trackHeight: 10.0, // 轨道高度thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 10.0), // 滑块形状为圆形),child: Slider(value: _sliderValue,onChanged: (value) {setState(() {_sliderValue = value;});},onChangeEnd: (value) {if (_verified == false) {if (_sliderValue.abs() * width >_offsetValue - CaptchaWidget.deviation &&_sliderValue.abs() * width <_offsetValue + CaptchaWidget.deviation) {widget.onSuccess();_verified = true;} else {widget.onFail();_verified = true;}}},),),],);}
}/// 用于创建中滑动拼图的自定义剪切器。
class CaptchaClipper extends CustomClipper<Path> {final double sliderValue;final double offsetValue;/// 创建一个 [CaptchaClipper],需要指定 [sliderValue] 和 [offsetValue]。CaptchaClipper(this.sliderValue, this.offsetValue);Path getClip(Size size) {final path = Path();final rect = RRect.fromRectAndRadius(Rect.fromPoints(Offset(offsetValue + size.width * sliderValue, 60),Offset(offsetValue + size.width * sliderValue + 80,size.height - 40,),),const Radius.circular(10.0),);path.addRRect(rect);return path;}bool shouldReclip(CustomClipper<Path> oldClipper) {return false;}
}class CaptchaBorderPainter extends CustomPainter {final double offsetValue;CaptchaBorderPainter(this.offsetValue);void paint(Canvas canvas, Size size) {final paint = Paint()..color = Colors.black..style = PaintingStyle.stroke..strokeWidth = 2.0;final rect = Rect.fromPoints(Offset(offsetValue, 60),Offset(offsetValue + 80,size.height - 40,),);final path = Path()..addRRect(RRect.fromRectAndRadius(rect, const Radius.circular(10.0)));canvas.drawPath(path, paint);}bool shouldRepaint(CustomPainter oldDelegate) {return false;}
}

5.2 控制逻辑

这里的控制也就是通过Slider的位置控制上层图片的位置,实现同步移动效果。

5.3 堆叠分层逻辑

最底层:背景图

用作背景的图片,这张图片要求有一定的长度(比宽高),它会平铺开并安装长度覆盖。而对于超出的高度则不会显示。因此主要需要确保相对于高度而言这样图片长度不能短了;这部分:

 // 背景图层
Image.network(widget.imageUrl,height: 200.0,fit: BoxFit.cover,
),

TODO: 这一版本都使用了固定的高度,日后可以给个调整的值。

次底层:背景图中标注目标轮廓

用作强调背景图中对齐位置的轮廓绘图,表示用户操作上层图片的目标位置。这部分是有canvas绘图实现的。:

// 背景标记层
CustomPaint(size: Size(width, 200.0),painter: CaptchaBorderPainter(_offsetValue),
),

中层:被裁剪的图片,即拼图

Flutter中,可以使用ClipPath将图片裁剪为任何想要的形状,用起来就像Canvas绘图一样。这部分将一张与最底层完全重叠、完全一样的图片裁剪为想要的形状(此版本已圆角矩形为例),只不过这个图片由于是堆叠再上方,因此需要设计得小一点。然后再对这个被裁剪得区域移动到最左端——从而适配滑块一开始是再最左端得:

// 拼图层
Positioned(left: _sliderValue * width - _offsetValue,child: ClipPath(clipper: CaptchaClipper(_sliderValue, _offsetValue),child: Image.network(widget.imageUrl,height: 200.0,fit: BoxFit.cover,),),
),

上层:拼图轮廓

// 拼图的轮廓层
Positioned(left: _sliderValue * width - _offsetValue,child: CustomPaint(size: Size(width, 200.0),painter: CaptchaBorderPainter(_offsetValue),),
),

5.4 水平位置确定

总水平长度

总水平长度是通过媒体查询来确定的,这对于移动设备来说,不会存在动态改变设备宽度的问题,因此也没有实时媒体查询的必要。总体长度将保存在以下字段中:

/// 小部件的总宽度。
late double width;

5.5 图片偏移逻辑

首先背景图是不需要便宜的,需要便宜的是上面的各个堆叠层。以下的所有偏移按照相对于左侧位置计算。

初始化随机偏移量

我考虑了一个内部的 _generateRandomNumber 方法,用于随机生成一个总位置 0.1~0.9 之间的偏移率,用于滑动验证成功的位置。代码为:

double _generateRandomNumber() {// 创建一个Random对象var random = Random();// 生成一个介于0.1和0.9之间的随机小数double randomValue = 0.1 + random.nextDouble() * 0.7;return randomValue;
}

这个便宜率需要在初始化状态时固定并暂存下来,放在_offsetRate中,可以使用State类的initState实现:

  void initState() {_offsetRate = _generateRandomNumber();super.initState();}

_offsetRate 的固定对于基于Clip的CaptchaClipper类的getClip方法中没有什么影响,应为在Flutter中Clip是不需要总是去重新绘制的,但是在基于Canvas的CaptchaBorderPainter就不一样了——毕竟CustomPainter类的paint方法会被不断调用,以至于如果不固定随机生成的_offsetRate ,则不断调用_generateRandomNumber方法导致描边位置错乱。实际的偏移量,无非是媒体查询出来的宽度去乘以这个便宜率:

width = MediaQuery.of(context).size.width;
_offsetValue = _offsetRate * width;

背景标记层偏移

背景标记层的偏移是一个固定的偏移量,这个偏移量由初始化的_offsetValue确定就不需要改:

CustomPaint(size: Size(width, 200.0),painter: CaptchaBorderPainter(_offsetValue),
),

在 CaptchaBorderPainter 中:

void paint(Canvas canvas, Size size) {final paint = Paint()..color = Colors.black..style = PaintingStyle.stroke..strokeWidth = 2.0;final rect = Rect.fromPoints(Offset(offsetValue, 60),Offset(offsetValue + 80,size.height - 40,),);final path = Path()..addRRect(RRect.fromRectAndRadius(rect, const Radius.circular(10.0)));canvas.drawPath(path, paint);}

可见,offsetValue不变则水平位置再不变。

拼图层偏移

拼图层是对于一个和背景图大小完全一样的图片的一个水平随机位置的一小块裁剪的,裁剪的初始有一个随机的偏移量,和背景标记层偏移是一样的,就是 offsetValue,着只不过是一个初始的裁剪距离左侧的偏离距离,即CaptchaClipper中:

Path getClip(Size size) {final path = Path();final rect = RRect.fromRectAndRadius(Rect.fromPoints(Offset(offsetValue + size.width * sliderValue, 60),Offset(offsetValue + size.width * sliderValue + 80,size.height - 40,),),const Radius.circular(10.0),);path.addRRect(rect);return path;}

前面有一个“offsetValue + …”。就是初始在相对于原图片左边的偏移量。正因为有了这个量,裁剪的不是图片左边的一部分,但是下面滑块却是初始时位于最左边的,我们需要在堆叠时将这个偏移量减去,就使得偏移的裁剪与底下的滑块初始时是“对齐”的:

// 拼图层
Positioned(// 减去偏移量与滑块对齐left: _sliderValue * width - _offsetValue, child: ClipPath(clipper: CaptchaClipper(_sliderValue, _offsetValue),child: Image.network(widget.imageUrl,height: 200.0,fit: BoxFit.cover,),),
),

这样也就是表明,一开始的位置是最左边的位置,只有用户滑动滑块才会有可能移动到验证成功的位置!

拼图的轮廓层偏移

拼图的轮廓层偏移 和 拼图层偏移的值始终保持相等的,但是需要注意的是,由于它们使用的技术分别是CustomClipper和Canvas,Clip的getClip 和 CustomPainter 的 paint执行时机不同,为了使得在paint中采用和相同的offsetValue,我们将第一次随机生成的偏移量做过缓存。

// 拼图的轮廓层
Positioned(left: _sliderValue * width - _offsetValue,child: CustomPaint(size: Size(width, 200.0),painter: CaptchaBorderPainter(_offsetValue),),
),

6. 总结、展望/后续版本

滑动控制UI

使用Slider实现的滑动控制器一些UI效果实现还不好看,用起来也不方便触摸,如果改成矩形的可能会更好。下一步计划使用绘图来替代Slider绘制矩形风格的滑块和滑槽,滑块可以使用拟物风格的图标。

轮廓形状

上面绘制的轮廓是简单的圆角矩形,不过如果改版为拼图的常见形状,比如:
在这里插入图片描述
会更加好看一些。
可以将再下一个版本中可以考虑重新使用Canvas绘制。

自带成功效果

如果验证成功后,可以在整个图片叠加区域上面添加一个半透明白色覆盖物(overlay组件)实现一个成功覆盖层,并且在中间加一个圆形背景的勾(√)来增强效果。

重置验证码

有时候可能用户一次验证没有成功,但也不意味着是机器人。目前可以重新构建组件,并传入新的图片来。不过可以考虑一个用于直接刷洗验证码的接口。

滑动时间

为了让用户体验更加丝滑,可以考虑手势的时间计算,一旦验证成功,则告诉用于“本次认证使用了xx秒,超过了99%的用户”。

相关文章:

Flutter笔记:手写并发布一个人机滑动验证码插件

Flutter笔记 手写一个人机滑块验证码 作者&#xff1a;李俊才 &#xff08;jcLee95&#xff09;&#xff1a;https://blog.csdn.net/qq_28550263 邮箱 &#xff1a;291148484163.com 本文地址&#xff1a;https://blog.csdn.net/qq_28550263/article/details/133529459 写 Flut…...

RabbitMQ安装与简单使用

安装 下载资源 可以访问官网查看下载信息rabbitmq官网 选择合适的版本&#xff0c;注意&#xff1a;rabbitmq需要下载一个Erlang才能使用 我自己是在一下两个连接中下载的 rabbitmq 3.8.8 erlang 21.3.8.15 需要下载其他版本的同学注意erlang版本是否匹配&#xff0c;可以访…...

不做静态化,当部署到服务器上的项目刷新出现404【已解决】

当线上项目刷新出现404页面解决方法&#xff1a; 在nginx配置里加入这样一段代码 try_files $uri $uri/ /index.html; 它的作用是尝试按照给定的顺序访问文件 变量解释 try_files 固定语法 $uri 指代home文件(ip地址后面的路径&#xff0c;假如是127.0.0.1/index/a.png&…...

SpringBoot结合Redisson实现分布式锁

&#x1f9d1;‍&#x1f4bb;作者名称&#xff1a;DaenCode &#x1f3a4;作者简介&#xff1a;啥技术都喜欢捣鼓捣鼓&#xff0c;喜欢分享技术、经验、生活。 &#x1f60e;人生感悟&#xff1a;尝尽人生百味&#xff0c;方知世间冷暖。 &#x1f4d6;所属专栏&#xff1a;Sp…...

css字体属性

一、CSS字体属性用于设置文本的字体样式。以下是常用的CSS字体属性&#xff1a; font-family&#xff1a;设置文本的字体系列&#xff0c;可以使用多个字体&#xff0c;用逗号分隔。font-size&#xff1a;设置文本的字体大小&#xff0c;可用像素、百分比、em等单位。font-wei…...

云原生微服务治理 第四章 Spring Cloud Netflix 服务注册/发现组件Eureka

系列文章目录 第一章 Java线程池技术应用 第二章 CountDownLatch和Semaphone的应用 第三章 Spring Cloud 简介 第四章 Spring Cloud Netflix 之 Eureka 文章目录 系列文章目录[TOC](文章目录) 前言1、Eureka 两大组件2、Eureka 服务注册与发现3、案例3.1、创建主工程3.1.1、主…...

【白细胞介素6(IL-6)】

## IL-6&#xff0c;至关重要的多功能细胞因子 ## 聊一聊白细胞介素6&#xff08;IL-6&#xff09; ## 简述&#xff1a;国内外IL-6 _ IL-6R在研药物一览_药智新闻.2017 ## 研究项目&#xff5c;靶向IL-6药物在研现状 2021...

设计模式之抽象工厂模式--创建一系列相关对象的艺术(简单工厂、工厂方法、到抽象工厂的进化过程,类图NS图)

目录 概述概念适用场景结构类图 衍化过程业务需求基本的数据访问程序工厂方法实现数据访问程序抽象工厂实现数据访问程序简单工厂改进抽象工厂使用反射抽象工厂反射配置文件衍化过程总结 常见问题总结 概述 概念 抽象工厂模式是一种创建型设计模式&#xff0c;它提供了一种将相…...

大数据-玩转数据-Flink SQL编程实战 (热门商品TOP N)

一、需求描述 每隔30min 统计最近 1hour的热门商品 top3, 并把统计的结果写入到mysql中。 二、需求分析 1.统计每个商品的点击量, 开窗2.分组窗口分组3.over窗口 三、需求实现 3.1、创建数据源示例 input/UserBehavior.csv 543462,1715,1464116,pv,1511658000 662867,22…...

python中实现定时任务的几种方案

目录 while True: sleep()Timeloop库threading.Timersched模块schedule模块APScheduler框架Celery框架数据流工具Apache Airflow概述Airflow 核心概念Airflow 的架构 总结以下几种方案实现定时任务&#xff0c;可根据不同需求去使用不同方案。 while True: sleep() 利用whil…...

AcWing算法提高课-5.6.1同余方程

宣传一下 算法提高课整理 CSDN个人主页&#xff1a;更好的阅读体验 原题链接 题目描述 求关于 x x x 的同余方程 a x ≡ 1 ( m o d b ) ax ≡ 1 \pmod b ax≡1(modb) 的最小正整数解。 输入格式 输入只有一行&#xff0c;包含两个正整数 a , b a,b a,b&#xff0c;用一…...

Docker Tutorial

什么是Docker 为每个应用提供完全隔离的运行环境 Dockerfile&#xff0c; Image&#xff0c;Container Image&#xff1a; 相当于虚拟机的快照&#xff08;snapshot&#xff09;里面包含了我们需要部署的应用程序以及替它所关联的所有库。通过image&#xff0c;我们可以创建很…...

平面图—简单应用

平面图&#xff1a;若一个图&#x1d43a;能画在平面&#x1d446;上&#xff0c;且使&#x1d43a;的边仅在端点处相交&#xff0c;则称图&#x1d43a;为可嵌入平面&#x1d446;&#xff0c;&#x1d43a;称为可平面图&#xff0c;简称为平面图。 欧拉公式&#xff1a;设有…...

安装JDK(Java SE Development Kit)超详细教程

文章时间 &#xff1a; 2023-10-04 1. 下载地址 直接去下载地址&#xff1a;https://www.oracle.com/java/technologies/downloads/ &#xff08;需要翻墙&#xff0c;不想翻墙或者不想注册oracel账号的&#xff0c;直接去我的阿里云盘&#xff09; 阿里云盘&#xff1a;http…...

KUKA机器人通过3点法设置工作台基坐标系的具体方法

KUKA机器人通过3点法设置工作台基坐标系的具体方法 具体方法和步骤可参考以下内容: 进入主菜单界面,依次选择“投入运行”—“测量”—基坐标,选择“3点法”, 在系统弹出的基坐标编辑界面,给基座标编号为3,命名为table1,然后单击“继续”按钮,进行下一步操作, 在弹出的…...

以太网的MAC层

以太网的MAC层 一、硬件地址 ​ 局域网中&#xff0c;硬件地址又称物理地址或MAC地址&#xff08;因为用在MAC帧&#xff09;&#xff0c;它是局域网上每一台计算机中固化在适配器的ROM中的地址。 ​ 关于地址问题&#xff0c;有这样的定义&#xff1a;“名字指出我们所要寻…...

Hadoop启动后jps发现没有DateNode解决办法

多次使用 Hadoop namenode -format 格式化节点后DateNode丢失 找到hadoop配置文件core-site.xml查找tmp路径 进入该路径&#xff0c;使用rm -rf data删除data文件 再次使用Hadoop namenode -format 格式化后jps后出现DateNode节点...

VUE3照本宣科——应用实例API与setup

VUE3照本宣科——应用实例API与setup 前言一、应用实例API1.createApp()2.app.use()3.app.mount() 二、setup 前言 &#x1f468;‍&#x1f4bb;&#x1f468;‍&#x1f33e;&#x1f4dd;记录学习成果&#xff0c;以便温故而知新 “VUE3照本宣科”是指照着中文官网和菜鸟教…...

json/js对象的key有什么区别?

1.对于JS对象来说 一个js对象如果是这样的 obj {"0": "小明","0name": "小明明", "": 18,"&#xffe5;": "哈哈"," ": "爱好广泛" }对于js对象来说&#xff0c;有时候key是不…...

极大似然估计概念的理解——统计学习方法

目录 1.最大似然估计的概念的理解1 2.最大似然估计的概念的理解2 3.最大似然估计的概念的理解3 4.例子 1.最大似然估计的概念的理解1 最大似然估计是一种概率论在统计学上的概念&#xff0c;是参数估计的一种方法。给定观测数据来评估模型参数。也就是模型已知&#xff0c;参…...

python模拟表格任意输入位置

在表格里输入数值&#xff0c;要任意位置&#xff0c;我找到了好方法&#xff1a; input输入 1. 行 2. 列输入&#xff1a;1 excel每行输入文字input输入位置 3.2 表示输入位置在&#xff1a;3行个列是要实现一个类似于 Excel 表格的输入功能&#xff0c;并且希望能够指定输入…...

如何限制文件只能通过USB打印机打印,限制打印次数和时限并且无法在打印前查看或编辑内容

在今天这个高度信息化的时代&#xff0c;文档打印已经成为日常工作中不可或缺的一部分。然而&#xff0c;这也带来了诸多安全风险&#xff0c;如文档被篡改、知识产权被侵犯以及信息泄露等。为了解决这些问题&#xff0c;只印应运而生。作为一款独特的软件工具&#xff0c;只印…...

车牌文本检测与识别:License Plate Recognition Based On Multi-Angle View Model

论文作者&#xff1a;Dat Tran-Anh,Khanh Linh Tran,Hoai-Nam Vu 作者单位&#xff1a;Thuyloi University;Posts and Telecommunications Institute of Technology 论文链接&#xff1a;http://arxiv.org/abs/2309.12972v1 内容简介&#xff1a; 1&#xff09;方向&#x…...

Blender中的4种视图着色模式

Blender中有四种主要的视图着色模式&#xff1a;线框、实体、Look Dev和渲染。它们的主要区别如下&#xff1a; - 线框模式只显示物体的边缘&#xff08;线框&#xff09;&#xff0c;可以让您看到场景中的所有物体&#xff0c;也可以调整线框的颜色和背景的颜色。 - 实…...

Flutter项目安装到Android手机一直显示在assembledebug

问题 Flutter项目安装到Android手机一直显示在assembledebug 原因 网络不好&#xff0c;gradle依赖下载不下来 解决方案 修改如下的文件 gradle-wrapper.properties 使用腾讯提供的gradle镜像下载 distributionUrlhttps://mirrors.cloud.tencent.com/gradle/gradle-7.5…...

数据挖掘实验(二)数据预处理【等深分箱与等宽分箱】

一、分箱平滑的原理 &#xff08;1&#xff09;分箱方法 在分箱前&#xff0c;一定要先排序数据&#xff0c;再将它们分到等深&#xff08;等宽&#xff09;的箱中。 常见的有两种分箱方法&#xff1a;等深分箱和等宽分箱。 等深分箱&#xff1a;按记录数进行分箱&#xff0…...

Vue2 第一次学习

本章为超级浓缩版,文章过于短,方便复习使用哦~ 文章目录 1. 简单引入 vue.js2. 指令2.1 事件绑定指令 v-on (简写 )2.2 内容渲染指令2.3 双向绑定指令 v-model2.4 属性绑定指令 v-bind (简写 : )2.5 条件渲染指令2.6 循环指令 v-for 3. vue 其他知识3.1 侦听器 watch3.2 计算属…...

tiny模式基本原理整合

【Tiny模式】的基本构成 M【首头在首位】 U【/】 V【HTTP/】 Host H【真实ip】 XH \r回车 \n换行 \t制表 \ 空格 一个基本的模式构成 [method] [uri] [version]\r\nHost: [host]\r\n[method] [uri] [version]\r\nHost: [host]\r\n 检测顺序 http M H XH 有些地区 XH H M 我这边…...

使用聚氨酯密封件的好处?

聚氨酯密封件因其优异的耐用性、灵活性和广泛的应用范围而在各个行业中广受欢迎。在本文中&#xff0c;我们将探讨使用聚氨酯密封件的优点&#xff0c;阐明其在许多不同领域广泛使用背后的原因。 1、高性能&#xff1a; 聚氨酯密封件具有出色的性能特征&#xff0c;使其成为各…...

DevEco Studio如何安装中文插件

首先 官网下载中文插件 由于DevEco是基于IntelliJ IDEA Community的&#xff0c;所有Compatibility选择“IntelliJ IDEA Community”&#xff0c;然后下载一个对应最新的就ok了。 最后打开Plugins页面&#xff0c;点击右上角齿轮 -> Install Plugin from Disk…。选择下载的…...

做vue用哪个网站/橙子建站

介绍 Redis发布与订阅功能可以让用户将消息同时发送给多个客户端 角色说明 发布者&#xff08;publisher&#xff09;&#xff1a; 发布消息的客户端。频道&#xff08;channel&#xff09;&#xff1a; 构建在服务器内部&#xff0c;负责接收发布者发送的消息&#xff0c;并…...

网站维护专业/网店推广费用多少钱

import Vue from vue import App from ./App import router from ./router import axios from axios Vue.config.productionTip false Vue.prototype.$http axios;...

大学城网站开发公司/廊坊seo

学习于&#xff1a; https://www.bilibili.com/video/av29268873/?p23 《鸟哥的linux私房菜》先让我们来看看进程的虚拟地址中的0~3g用户空间的存储&#xff1a;正文即代码段&#xff0c;初始化的数据段&#xff0c;未初始化的数据即bss段。bss段和初始化的数据段加在一起就是…...

中关村网站建设公司/网络优化培训

这里的电压环带宽&#xff0c;反应的是电压环响应指令能力。即输出电压跟踪指令电压的能力。 指令响应能力需要有一个指标来衡量&#xff0c;例如用输入阶跃指令来看输出的上升、超调等 或者用输入正弦指令看输出正弦分量与指令的幅度差和相位差&#xff0c;这就是我们说的带…...

宁波做网站烟台厂商/上热门最火标题

情景描述&#xff1a;之前使用的gitblit地址是&#xff1a;http://admin127.0.0.1:8080/gitblit/r/aaa.git&#xff0c;现在想修改为&#xff1a;http://admin127.0.0.1:8080/gitblit/r/bbb.git。解决方案&#xff1a;1、查看现用的remote地址&#xff1a;$ git remote -v orig…...

山乙建设公司网站/百度舆情

设置vscode为中文ctrshiftp 输入 configure language 进 en更改为zh-cn , 重启vscode即可 , 如果还不行,就安装插件 转载于:https://www.cnblogs.com/enych/p/10550095.html...