在 Flutter 开发中,使用人机验证码(也称为 CAPTCHA,即 Completely Automated Public Turing test to tell Computers and Humans Apart)通常是为了增强应用程序的安全性和防止恶意活动。
总之,人机验证码是一种重要的安全措施,可帮助保护 Flutter 应用程序免受各种恶意活动的威胁,并提高用户数据的质量和应用程序的整体安全性。
flutter pub add jc_captcha2.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}); // 堆代码 duidaima.com @override 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('验证失败'); }, ), ), ); } }说明:
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小部件,我们将小容器定位到了大容器的左上角。
Flutter的Slider(滑块)组件是一个用于选择一个范围内数值的交互式控件。用户可以通过滑动滑块来选择数值,这使得它在用户界面中用于调整设置和选择数值非常有用。
class SliderExample extends StatefulWidget { const SliderExample({super.key}); @override State<SliderExample> createState() => _SliderExampleState(); } class _SliderExampleState extends State<SliderExample> { double _currentSliderValue = 20; @override 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官方。
在Flutter中,您可以使用Canvas来进行绘图操作,Canvas是Flutter中的绘图上下文,允许您在屏幕上绘制各种形状、文本和图像。Canvas通常与CustomPaint小部件一起使用,以在Flutter的绘图流程中插入自定义绘图代码。
import 'package:flutter/material.dart'; // 堆代码 duidaima.com void main() { runApp(const MyApp()); } class MyCustomPainter extends CustomPainter { @override 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); } @override bool shouldRepaint(CustomPainter oldDelegate) { return false; } } class MyApp extends StatelessWidget { const MyApp({super.key}); @override 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提供了丰富的绘图功能,可以满足各种绘图需求。
ClipRect( child: Image.network( 'https://example.com/image.jpg', width: 200, height: 200, fit: BoxFit.cover, ), )
上述示例中,ClipRect将Image小部件裁剪为矩形区域内的可见部分。您可以使用其他Clip类型来创建不同形状和效果的裁剪。裁剪在创建各种自定义UI效果时非常有用,例如创建特定形状的按钮、卡片或背景。通过使用不同的Clip类型,您可以实现各种各样的外观和动画效果,从而增强Flutter应用程序的用户界面。
/// 堆代码 duidaima.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); @override 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; } @override void initState() { _offsetRate = _generateRandomNumber(); super.initState(); } @override 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); @override 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; } @override bool shouldReclip(CustomClipper<Path> oldClipper) { return false; } } class CaptchaBorderPainter extends CustomPainter { final double offsetValue; CaptchaBorderPainter(this.offsetValue); @override 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); } @override bool shouldRepaint(CustomPainter oldDelegate) { return false; } }4.2 控制逻辑
// 背景图层 Image.network( widget.imageUrl, height: 200.0, fit: BoxFit.cover, ),TODO: 这一版本都使用了固定的高度,日后可以给个调整的值。
// 背景标记层 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), ), ),4.4 水平位置确定
/// 小部件的总宽度。 late double width;4.5 图片偏移逻辑
double _generateRandomNumber() { // 创建一个Random对象 var random = Random(); // 生成一个介于0.1和0.9之间的随机小数 double randomValue = 0.1 + random.nextDouble() * 0.7; return randomValue; }这个便宜率需要在初始化状态时固定并暂存下来,放在_offsetRate中,可以使用State类的initState实现:
@override 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;背景标记层偏移
CustomPaint( size: Size(width, 200.0), painter: CaptchaBorderPainter(_offsetValue), ),在 CaptchaBorderPainter 中:
@override 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不变则水平位置再不变。
@override 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, ), ), ),这样也就是表明,一开始的位置是最左边的位置,只有用户滑动滑块才会有可能移动到验证成功的位置!
// 拼图的轮廓层 Positioned( left: _sliderValue * width - _offsetValue, child: CustomPaint( size: Size(width, 200.0), painter: CaptchaBorderPainter(_offsetValue), ), ),五. 总结、展望/后续版本
上面绘制的轮廓是简单的圆角矩形,不过如果改版为拼图的常见形状,比如: