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

news2025/4/8 13:43:15
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 {
  /// 用作验证图像的URL
  final 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%的用户”。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1058996.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【深蓝学院】手写VIO第4章--基于滑动窗口算法的 VIO 系统:可观性和 一致性--作业

0. 内容 T1. 参考SLAM14讲P247直接可写&#xff0c;注意 ξ 1 , ξ 2 \xi_1,\xi_2 ξ1​,ξ2​之间有约束&#xff08;关系&#xff09;。 套用舒尔补公式&#xff1a; marg掉 ξ 1 \xi_1 ξ1​之后&#xff0c;信息被传递到 L 1 和 L 2 L_1和L_2 L1​和L2​之间了。 T2. …

同学苹果ios的ipa文件应用企业代签选择签名商看看这篇文章你再去吧

同学我们要知道随着互联网的发展&#xff0c;苹果应用市场的火爆&#xff0c;越来越多的开发者加入到苹果应用开发行业中来。同时&#xff0c;苹果应用市场上的应用也在不断增多&#xff0c;用户数量也在不断增加&#xff0c;苹果应用代签是指通过第三方公司为开发者的应用进行…

谋道翻译逆向

文章目录 前文crypto模块分析完整代码结尾 前文 本文章中所有内容仅供学习交流&#xff0c;严禁用于商业用途和非法用途&#xff0c;否则由此产生的一切后果均与作者无关&#xff0c;若有侵权&#xff0c;请联系我立即删除&#xff01; crypto模块 Crypto是加密的简称&#…

nodejs+vue校园跑腿系统elementui

购物车品结算,管理个人中心&#xff0c;订单管理&#xff0c;接单处理&#xff0c;商品维护&#xff0c;用户管理&#xff0c;系统管理等功育食5&#xff09;要求系统运行可靠、性能稳定、界面友好、使用方便。 第三章 系统分析 10 3.1需求分析 10 3.2可行性分析 10 3.2.1技术…

Lagrange插值法实验:求拉格朗日插值多项式和对应x的近似值matlab实现(内附代码)

一、实验要求 已知函数表&#xff1a; 求出Lagrange 插值多项式&#xff0c;并计算x1.2处的y的近似值。 二、MATLAB代码 求解多项式&#xff1a; X input(请输入横坐标向量X:\nX); % 获取用户输入的横坐标向量 Y input(请输入纵坐标向量Y:\nY); % 获取用户输入的纵坐标…

MySQL-MVCC(Multi-Version Concurrency Control)

MySQL-MVCC&#xff08;Multi-Version Concurrency Control&#xff09; MVCC&#xff08;多版本并发控制&#xff09;&#xff1a;为了解决数据库并发读写和数据一致性的问题&#xff0c;是一种思想&#xff0c;可以有多种实现方式。 核心思想&#xff1a;写入时创建行的新版…

基于 Netty + RXTX 的无协议 COM 通讯案例实现

参考 Netty集成串口RXTX编程&#xff0c;为什么过时了&#xff1f; Java版本 java version "1.8.0_231" Java(TM) SE Runtime Environment (build 1.8.0_231-b11) Java HotSpot(TM) 64-Bit Server VM (build 25.231-b11, mixed mode) RXTX版本 # 官网 http://rx…

第 4 章 串(图书关键字索引表实现)

1. 背景说明 需要从书目文件中获取其关键字及对应的书号索引 bookInfo.txt 005 Computer Data Structures 010 Introduction to Data Structures 023 Fundamentals of Data Structures 034 The Design and Analysis of Computer Algorithms 050 Introduction to Numerical Anal…

STM32复习笔记(六):STM32远程升级BootLoader相关

目录 Preface&#xff1a; &#xff08;一&#xff09;STM32上电启动流程 &#xff08;二&#xff09;BootLoader相关 &#xff08;三&#xff09;Clion配置 Preface&#xff1a; 有关STM32的BootLoader主要还是参考了许多大佬的文章&#xff0c;这里只是简单地列举一下&am…

postgresql新特性之Merge

postgresql新特性之Merge 创建测试表测试案例 创建测试表 create table cps.public.test(id integer primary key,balance numeric,status varchar(1));测试案例 官网介绍 merge into test t using ( select 1 id,0 balance,Y status) s on(t.id s.id) -- 当匹配上了,statu…

STM32复习笔记(二):GPIO

目录 &#xff08;一&#xff09;Demo流程 &#xff08;二&#xff09;工程配置 &#xff08;三&#xff09;代码部分 &#xff08;四&#xff09;外部中断&#xff08;EXTI&#xff09; &#xff08;一&#xff09;Demo流程 首先&#xff0c;板子上有4个按键&#xff0c;…

网络技术在学校是学习网络协议吗

大家好&#xff0c;我是网络工程师成长日记实验室的郑老师&#xff0c;您现在正在查看的是网络工程师成长日记专栏&#xff0c;记录网络工程师日常生活的点点滴滴 一个同学说他现在也在学这个计算机专业&#xff0c;他以后他现在目前规划是把软考和华为的HIP1套。我自己本身也做…

AI时代给我们公司模式带来的改变

最近和几个朋友聊天&#xff0c;都说公司卷的更厉害了&#xff0c;老板趁着经济形势不好压榨的更厉害了&#xff01;但确实没办法&#xff0c;我们部分老板还是喜欢以人为本&#xff08;成本&#xff01;&#xff09;&#xff0c;更喜欢雇更多的人&#xff0c;看着他们低效而痛…

【Java】权限修饰符

目录 权限修饰符 权限修饰符-示例代码 权限修饰符 Java有四种访问权限&#xff0c;其中三种有访问权限修饰符&#xff0c;分别为 private&#xff0c;public 和 protected&#xff0c;还有一种不带任何修饰符&#xff1a; private&#xff1a;Java语言中对访问权限限制的最窄…

【C语言经典100例题-68】有n个整数,使其前面各数顺序向后移m个位置,最后m个数变成最前面的m个数

方法一 将原数组拆成两部分&#xff0c;前面n-m个数和后面m个数。首先将前面n-m个数逆序&#xff0c;然后将后面的m个数逆序。最后将整个数组逆序即可。 #include <stdio.h>void reverse(int arr[], int start, int end) {for (int i start, j end; i < (start en…

快排三种递归及其优化,非递归和三路划分

个人主页&#xff1a;Lei宝啊 愿所有美好如期而遇 目录 快排简介&#xff1a; 快排的三种递归实现&#xff1a; Hoare&#xff1a; 挖坑&#xff1a; 双指针&#xff1a; 小区间优化&#xff1a; 三数取中优化&#xff1a; 快排非递归实现&#xff1a; 快排的三路划…

嵌入式Linux应用开发-驱动大全-第一章同步与互斥④

嵌入式Linux应用开发-驱动大全-第一章同步与互斥④ 第一章 同步与互斥④1.5 自旋锁spinlock的实现1.5.1 自旋锁的内核结构体1.5.2 spinlock在UP系统中的实现1.5.3 spinlock在SMP系统中的实现 1.6 信号量semaphore的实现1.6.1 semaphore的内核结构体1.6.2 down函数的实现1.6.3 u…

Android etc1tool之png图片转换pkm 和 zipalign简介

关于作者&#xff1a;CSDN内容合伙人、技术专家&#xff0c; 从零开始做日活千万级APP。 专注于分享各领域原创系列文章 &#xff0c;擅长java后端、移动开发、商业变现、人工智能等&#xff0c;希望大家多多支持。 目录 一、导读二、etc1tool2.1、用法 三、zipalign3.1 使用 四…

day49 ARM

.text .globl _start _start:mov r1,#1mov r2,#0mov r3,#100 fun2:cmp r2,r3bcc fun1 stop:b stop fun1: ADD r2,r2,r1add r4,r4,r2b fun2 .end

华为云云耀云服务器L实例评测|部署个人在线电子书库 calibre

华为云云耀云服务器L实例评测&#xff5c;部署个人在线电子书库 calibre 一、云耀云服务器L实例介绍1.1 云服务器介绍1.2 应用场景1.3 支持镜像 二、云耀云服务器L实例配置2.1 重置密码2.2 服务器连接2.3 安全组配置 三、部署 calibre3.1 calibre 介绍3.2 Docker 环境搭建3.3 c…