作者:李俊才 (jcLee95):https://blog.csdn.net/qq_28550263
郵箱 :291148484@163.com
本文地址:https://blog.csdn.net/qq_28550263/article/details/133529459
寫(xiě) Flutter 項(xiàng)目時(shí),遇到需要滑塊驗(yàn)證碼功能?;瑝K驗(yàn)證碼屬于人機(jī)驗(yàn)證碼的一種,看起來(lái)像是在一個(gè)圖片中“挖去”了一塊,然后通過(guò)用戶手動(dòng)操作滑塊,讓被“挖去”的部分移回來(lái)。由于我不想使用各種第三方模塊,因此決定自己實(shí)現(xiàn)一個(gè)初版以后慢慢添磚加瓦。本文是對(duì)第一個(gè)版本的一點(diǎn)記錄。
1. 概述
1.1 關(guān)于本文
1.2 什么是人機(jī)驗(yàn)證碼
概念
在 Flutter 開(kāi)發(fā)中,使用人機(jī)驗(yàn)證碼(也稱為 CAPTCHA,即 Completely Automated Public Turing test to tell Computers and Humans Apart)通常是為了增強(qiáng)應(yīng)用程序的安全性和防止惡意活動(dòng)。
目的
-
防止自動(dòng)化攻擊:惡意用戶和自動(dòng)化腳本可以嘗試大規(guī)模攻擊應(yīng)用程序,例如注冊(cè)多個(gè)虛假帳戶、暴力破解密碼、濫發(fā)垃圾郵件或提交虛假表單。人機(jī)驗(yàn)證碼可以幫助阻止這些自動(dòng)化攻擊,因?yàn)樗鼈円笥脩糇C明自己是真人而不是機(jī)器;
-
防止垃圾數(shù)據(jù)輸入:人機(jī)驗(yàn)證碼可以確保用戶提交的數(shù)據(jù)是有效和真實(shí)的。例如,在用戶注冊(cè)過(guò)程中,驗(yàn)證碼可以防止惡意用戶自動(dòng)化注冊(cè)虛假帳戶,從而保護(hù)應(yīng)用程序的數(shù)據(jù)質(zhì)量;
-
防止濫用資源:如果應(yīng)用程序提供某種資源,如 API 訪問(wèn)或文件下載,希望防止單個(gè)用戶或惡意機(jī)器人濫用這些資源。通過(guò)要求用戶在訪問(wèn)這些資源之前進(jìn)行驗(yàn)證碼驗(yàn)證,可以限制濫用的可能性;
-
增加安全性:在某些情況下,用戶可能需要進(jìn)行敏感操作,如更改密碼、恢復(fù)帳戶或進(jìn)行金融交易。在這些情況下,驗(yàn)證碼可以提供額外的安全性,確保只有授權(quán)用戶可以執(zhí)行這些操作;
在 Flutter 中實(shí)施人機(jī)驗(yàn)證碼通常涉及使用插件或集成第三方服務(wù),這些服務(wù)提供了生成和驗(yàn)證驗(yàn)證碼的功能。
總之,人機(jī)驗(yàn)證碼是一種重要的安全措施,可幫助保護(hù) Flutter 應(yīng)用程序免受各種惡意活動(dòng)的威脅,并提高用戶數(shù)據(jù)的質(zhì)量和應(yīng)用程序的整體安全性。
滑動(dòng)驗(yàn)證碼
1.3 項(xiàng)目地址
- flutter pub: https://pub.dev/packages/jc_captcha
- 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('驗(yàn)證成功');
},
onFail: () {
print('驗(yàn)證失敗');
},
),
),
);
}
}
說(shuō)明:
驗(yàn)證碼僅驗(yàn)證一次即失效:
驗(yàn)證碼有兩個(gè)狀態(tài),且狀態(tài)該轉(zhuǎn)換是一次性的,即:
未驗(yàn)證 =》 已驗(yàn)證
- 從未驗(yàn)證到已驗(yàn)證僅僅轉(zhuǎn)換一次,轉(zhuǎn)換結(jié)果有驗(yàn)證成功和驗(yàn)證失?。?/li>
- 如果驗(yàn)證成功,則執(zhí)行成功回調(diào) onSuccess;
- 如果驗(yàn)證失敗,則執(zhí)行失敗回調(diào) onFail;
效果如圖所示:
3. 功能描述
3.1
4. 實(shí)現(xiàn)思路分析
4.1 分析問(wèn)題比解決問(wèn)題重要:摳出一塊圖形是摳嗎
摳出小塊圖是不是一定要從原圖像那里拷貝一塊像素?你當(dāng)然可以這樣來(lái)實(shí)現(xiàn),不過(guò)會(huì)復(fù)雜一些。不過(guò)還有一種假設(shè)是,摳出的拼圖部分不是摳出的,而是原原本本的一張圖。對(duì)比而言:
方案1: 從原圖像中拷貝一塊像素
-
原始圖像處理: 加載原始驗(yàn)證碼圖像,該圖像包括背景圖像和一個(gè)需要滑動(dòng)的小塊圖像。
-
拷貝小塊圖像: 使用Flutter的圖像處理功能,將原始圖像中的小塊圖像精確拷貝出來(lái),并將其作為驗(yàn)證碼的滑塊。這可以通過(guò)裁剪原始圖像的一部分來(lái)實(shí)現(xiàn)。
-
驗(yàn)證用戶拖動(dòng): 當(dāng)用戶嘗試拖動(dòng)滑塊時(shí),需要檢查滑塊的位置是否與原始圖像中的小塊位置匹配。這可以涉及比較滑塊的位置與小塊的位置是否一致。
方案2: 將兩張圖疊加
-
原始圖像處理: 同樣,加載原始驗(yàn)證碼圖像,包括背景圖像和一個(gè)需要滑動(dòng)的小塊圖像。
-
自定義布局: 使用Flutter的布局和圖層疊加功能,將兩張圖像堆疊在一起,確保小塊圖像與背景圖像的對(duì)齊。這可以通過(guò)使用
Stack
小部件或Positioned
小部件來(lái)實(shí)現(xiàn)。 -
驗(yàn)證用戶拖動(dòng): 當(dāng)用戶嘗試拖動(dòng)滑塊時(shí),需要檢查滑塊的位置是否與小塊圖像的位置匹配。這可以通過(guò)比較滑塊的位置是否在小塊圖像的位置上來(lái)實(shí)現(xiàn)。
兩種方案的選擇取決于項(xiàng)目需求和個(gè)人偏好。我個(gè)人覺(jué)得方案2更加具有可操作性,因此我后續(xù)是基于這種方法來(lái)實(shí)現(xiàn)的。
4.2 知識(shí)準(zhǔn)備
這里對(duì)用到得一些 Flutter 基礎(chǔ)知識(shí)做簡(jiǎn)單介紹,方便初學(xué)讀者了解學(xué)習(xí)相關(guān)知識(shí)。
1. 堆疊布局
在Flutter中,堆疊布局(Stack布局)是一種常用的布局方式,用于將多個(gè)子部件疊加在一起。堆疊布局允許您將子部件以層疊的方式排列,每個(gè)子部件可以覆蓋或部分覆蓋其他子部件,從而創(chuàng)建復(fù)雜的布局效果。在Flutter中,堆疊布局由兩個(gè)主要組件構(gòu)成:
-
Stack(堆疊): Stack是一個(gè)容器小部件,用于包含子部件并按照它們的繪制順序?qū)⑺鼈儻B加在一起。子部件按照從底部到頂部的順序堆疊。您可以使用
children
屬性來(lái)指定要疊加的子部件列表。 -
Positioned(定位): Positioned小部件用于控制子部件在Stack中的位置。它允許您指定子部件的左、上、右和下邊距,從而將子部件精確定位在Stack上。
在堆疊布局中,子部件的位置和大小是通過(guò)Positioned小部件來(lái)控制的。每個(gè)Positioned小部件都必須包含一個(gè)左、上、右和下的屬性,以確定子部件在Stack中的位置和大小。
-
left
:指定子部件的左邊距。 -
top
:指定子部件的上邊距。 -
right
:指定子部件的右邊距。 -
bottom
:指定子部件的下邊距。
這些屬性可以設(shè)置為null
,以便自動(dòng)確定位置,或者設(shè)置為具體的值,以確保子部件在Stack中的精確定位。
例如,下面的代碼展示了如何將兩個(gè)容器疊加在一起:
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包含兩個(gè)容器,一個(gè)藍(lán)色的大容器和一個(gè)紅色的小容器。通過(guò)Positioned小部件,我們將小容器定位到了大容器的左上角。
2. Slider組件
Flutter的Slider(滑塊)組件是一個(gè)用于選擇一個(gè)范圍內(nèi)數(shù)值的交互式控件。用戶可以通過(guò)滑動(dòng)滑塊來(lái)選擇數(shù)值,這使得它在用戶界面中用于調(diào)整設(shè)置和選擇數(shù)值非常有用。
Slider組件主要有以下功能:
-
數(shù)值范圍選擇: Slider允許用戶在指定的數(shù)值范圍內(nèi)進(jìn)行選擇。用戶可以通過(guò)滑動(dòng)滑塊來(lái)選擇一個(gè)數(shù)值,該數(shù)值通常表示某種設(shè)置或參數(shù)。
-
分割刻度: Slider可以顯示刻度線,并且可以根據(jù)需要進(jìn)行分割。這些刻度線使用戶更容易準(zhǔn)確地選擇所需的數(shù)值。
-
標(biāo)簽顯示: Slider可以顯示當(dāng)前數(shù)值的標(biāo)簽,通常位于滑塊上方或下方。這有助于用戶了解所選擇的數(shù)值。
-
回調(diào)函數(shù): 您可以為Slider設(shè)置一個(gè)回調(diào)函數(shù),當(dāng)用戶拖動(dòng)滑塊時(shí),會(huì)觸發(fā)該函數(shù)。這使得您可以在數(shù)值發(fā)生變化時(shí)執(zhí)行特定的操作,如更新UI或應(yīng)用程序狀態(tài)。
-
自定義樣式: Slider具有豐富的自定義樣式選項(xiàng),您可以更改滑塊、軌道和刻度的顏色、形狀和大小,以適應(yīng)您的應(yīng)用程序設(shè)計(jì)。
以下是Flutter Slider組件的一些常用屬性:
-
value
:表示當(dāng)前的數(shù)值,可以通過(guò)設(shè)置此值來(lái)控制Slider的位置。 -
onChanged
:一個(gè)回調(diào)函數(shù),當(dāng)用戶拖動(dòng)滑塊時(shí)觸發(fā),用于處理數(shù)值的變化。 -
min
:Slider的最小值。 -
max
:Slider的最大值。 -
divisions
:用于將Slider軌道分割為多少個(gè)離散步驟,通常與滑塊刻度一起使用。 -
label
:顯示在滑塊上方或下方的標(biāo)簽,通常用于顯示當(dāng)前值。 -
activeColor
:激活狀態(tài)下(滑塊被拖動(dòng))的顏色。 -
inactiveColor
:非激活狀態(tài)下的顏色。 -
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;
});
},
),
);
}
}
這個(gè)示例是一個(gè)基本的Flutter應(yīng)用程序,來(lái)自于Flutter官方。
- 當(dāng)應(yīng)用程序啟動(dòng)時(shí),Slider的初始值是20。用戶可以通過(guò)按住并拖動(dòng)滑塊來(lái)選擇不同的數(shù)值。
- 滑塊的最小值是0,最大值是100,滑塊上有5個(gè)刻度線,表示5個(gè)離散的數(shù)值。當(dāng)用戶拖動(dòng)滑塊時(shí),滑塊的值會(huì)隨著手指的拖動(dòng)而實(shí)時(shí)更新。
- 在滑塊的上方顯示一個(gè)標(biāo)簽,顯示當(dāng)前所選數(shù)值的整數(shù)部分,例如,當(dāng)用戶將滑塊移動(dòng)到25時(shí),標(biāo)簽顯示"25"。
- 滑塊的外觀由activeColor(激活狀態(tài)下的顏色)和inactiveColor(非激活狀態(tài)下的顏色)屬性定義。在示例中,激活狀態(tài)下的顏色為藍(lán)色,非激活狀態(tài)下的顏色為灰色。
3. Flutter 繪圖(canvas)
在Flutter中,您可以使用Canvas來(lái)進(jìn)行繪圖操作,Canvas是Flutter中的繪圖上下文,允許您在屏幕上繪制各種形狀、文本和圖像。Canvas通常與CustomPaint小部件一起使用,以在Flutter的繪圖流程中插入自定義繪圖代碼。
使用Canvas進(jìn)行繪圖的基本步驟包括:
-
創(chuàng)建一個(gè)CustomPaint小部件: 首先,您需要在Flutter應(yīng)用程序的UI層次結(jié)構(gòu)中插入一個(gè)CustomPaint小部件,以便將繪圖內(nèi)容放入其中。
-
自定義Painter: 您需要?jiǎng)?chuàng)建一個(gè)自定義的Painter類,它繼承自CustomPainter,并實(shí)現(xiàn)paint和shouldRepaint方法。paint方法是您用來(lái)實(shí)際繪制內(nèi)容的地方,shouldRepaint方法決定是否需要重新繪制。
-
在paint方法中繪制內(nèi)容: 在paint方法中,您可以使用Canvas對(duì)象來(lái)進(jìn)行各種繪圖操作,如繪制圖形、文本、路徑等。Canvas提供了各種方法來(lái)繪制不同類型的圖形。
例如,下面得代碼展示了如何在Flutter中使用Canvas繪制一個(gè)簡(jiǎn)單的圓形:
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方法中繪制了一個(gè)藍(lán)色的圓形。然后,CustomPaint小部件將MyCustomPainter作為其painter屬性的值傳遞,并在UI中顯示繪制的圓形。
通過(guò)Canvas和CustomPainter,您可以創(chuàng)建各種自定義繪圖效果,包括圖表、動(dòng)畫(huà)、自定義圖形和復(fù)雜的UI元素。Canvas提供了豐富的繪圖功能,可以滿足各種繪圖需求。
4. Flutter 裁剪(Clip)
Flutter中的裁剪(Clip)是一種用于控制Widget可見(jiàn)區(qū)域的技術(shù),它可以用來(lái)創(chuàng)建各種不同形狀和效果的UI元素。裁剪允許您定義一個(gè)區(qū)域,只有在該區(qū)域內(nèi)的部分內(nèi)容才會(huì)被顯示,超出該區(qū)域的內(nèi)容會(huì)被裁剪掉。Flutter提供了多種不同類型的裁剪小部件,以滿足各種需求。
以下是一些常見(jiàn)的Flutter裁剪小部件和其用途:
-
ClipRect: 這是最常見(jiàn)的裁剪小部件之一,它可以將其子部件裁剪為矩形形狀。使用它可以創(chuàng)建各種矩形裁剪效果,例如將圖像限制在矩形區(qū)域內(nèi)。
-
ClipOval: 這個(gè)小部件可以將其子部件裁剪為橢圓形狀,用于創(chuàng)建橢圓形的UI元素,如頭像或按鈕。
-
ClipRRect: ClipRRect用于創(chuàng)建帶有圓角的矩形裁剪,可以用來(lái)創(chuàng)建圓角矩形框或卡片。
-
ClipPath: ClipPath允許您自定義裁剪區(qū)域的形狀,通過(guò)提供一個(gè)自定義路徑來(lái)實(shí)現(xiàn)各種復(fù)雜的裁剪效果。
這里是一個(gè)示例,展示如何使用ClipRect來(lái)裁剪一個(gè)圖像以顯示在矩形區(qū)域內(nèi):
ClipRect(
child: Image.network(
'https://example.com/image.jpg',
width: 200,
height: 200,
fit: BoxFit.cover,
),
)
上述示例中,ClipRect將Image小部件裁剪為矩形區(qū)域內(nèi)的可見(jiàn)部分。您可以使用其他Clip類型來(lái)創(chuàng)建不同形狀和效果的裁剪。
裁剪在創(chuàng)建各種自定義UI效果時(shí)非常有用,例如創(chuàng)建特定形狀的按鈕、卡片或背景。通過(guò)使用不同的Clip類型,您可以實(shí)現(xiàn)各種各樣的外觀和動(dòng)畫(huà)效果,從而增強(qiáng)Flutter應(yīng)用程序的用戶界面。
5. 基本實(shí)現(xiàn)
5.1 實(shí)現(xiàn)代碼
/// 作者:李俊才
/// 郵箱:291148484@163.com
/// 項(xiàng)目地址:http://thispage.tech:9680/jclee1995/flutter-jc-captcha
/// 協(xié)議:MIT
import 'dart:math';
import 'package:flutter/material.dart';
/// 驗(yàn)證碼組件
///
/// 這個(gè)組件用于顯示一個(gè)驗(yàn)證碼圖像,用戶需要滑動(dòng)滑塊以解鎖驗(yàn)證。當(dāng)驗(yàn)證成功或失敗時(shí),
/// 分別觸發(fā) [onSuccess] 或 [onFail] 回調(diào)函數(shù)。你可以設(shè)置允許的誤差范圍 [deviation]
/// 以調(diào)整驗(yàn)證的精確性。
class CaptchaWidget extends StatefulWidget {
/// 用作驗(yàn)證圖像的URL
final String imageUrl;
/// 當(dāng)驗(yàn)證成功時(shí)觸發(fā)的回調(diào)函數(shù)。
final Function() onSuccess;
/// 當(dāng)驗(yàn)證失敗時(shí)觸發(fā)的回調(diào)函數(shù)。
final Function() onFail;
/// 允許的誤差范圍,用于調(diào)整驗(yàn)證的精確性。
static double deviation = 5;
/// 創(chuàng)建一個(gè) [CaptchaWidget] 小部件,需要指定 [imageUrl]、[onSuccess] 和 [onFail] 回調(diào)函數(shù)。
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> {
/// 滑塊的當(dāng)前位置。
double _sliderValue = 0.0;
late double _offsetRate;
/// 用于定位的偏移值。
late double _offsetValue;
/// 小部件的總寬度。
late double width;
/// 用于確保驗(yàn)證僅僅一次有效
bool _verified = false;
double _generateRandomNumber() {
// 創(chuàng)建一個(gè)Random對(duì)象
var random = Random();
// 生成一個(gè)介于0.1和0.9之間的隨機(jī)小數(shù)
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,
),
// 背景標(biāo)記層
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;
}
}
},
),
),
],
);
}
}
/// 用于創(chuàng)建中滑動(dòng)拼圖的自定義剪切器。
class CaptchaClipper extends CustomClipper<Path> {
final double sliderValue;
final double offsetValue;
/// 創(chuàng)建一個(gè) [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 控制邏輯
這里的控制也就是通過(guò)Slider的位置控制上層圖片的位置,實(shí)現(xiàn)同步移動(dòng)效果。
5.3 堆疊分層邏輯
最底層:背景圖
用作背景的圖片,這張圖片要求有一定的長(zhǎng)度(比寬高),它會(huì)平鋪開(kāi)并安裝長(zhǎng)度覆蓋。而對(duì)于超出的高度則不會(huì)顯示。因此主要需要確保相對(duì)于高度而言這樣圖片長(zhǎng)度不能短了;這部分:
// 背景圖層
Image.network(
widget.imageUrl,
height: 200.0,
fit: BoxFit.cover,
),
TODO: 這一版本都使用了固定的高度,日后可以給個(gè)調(diào)整的值。
次底層:背景圖中標(biāo)注目標(biāo)輪廓
用作強(qiáng)調(diào)背景圖中對(duì)齊位置的輪廓繪圖,表示用戶操作上層圖片的目標(biāo)位置。這部分是有canvas繪圖實(shí)現(xiàn)的。:
// 背景標(biāo)記層
CustomPaint(
size: Size(width, 200.0),
painter: CaptchaBorderPainter(_offsetValue),
),
中層:被裁剪的圖片,即拼圖
Flutter中,可以使用ClipPath將圖片裁剪為任何想要的形狀,用起來(lái)就像Canvas繪圖一樣。這部分將一張與最底層完全重疊、完全一樣的圖片裁剪為想要的形狀(此版本已圓角矩形為例),只不過(guò)這個(gè)圖片由于是堆疊再上方,因此需要設(shè)計(jì)得小一點(diǎn)。然后再對(duì)這個(gè)被裁剪得區(qū)域移動(dòng)到最左端——從而適配滑塊一開(kāi)始是再最左端得:
// 拼圖層
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 水平位置確定
總水平長(zhǎng)度
總水平長(zhǎng)度是通過(guò)媒體查詢來(lái)確定的,這對(duì)于移動(dòng)設(shè)備來(lái)說(shuō),不會(huì)存在動(dòng)態(tài)改變?cè)O(shè)備寬度的問(wèn)題,因此也沒(méi)有實(shí)時(shí)媒體查詢的必要。總體長(zhǎng)度將保存在以下字段中:
/// 小部件的總寬度。
late double width;
5.5 圖片偏移邏輯
首先背景圖是不需要便宜的,需要便宜的是上面的各個(gè)堆疊層。以下的所有偏移按照相對(duì)于左側(cè)位置計(jì)算。
初始化隨機(jī)偏移量
我考慮了一個(gè)內(nèi)部的 _generateRandomNumber 方法,用于隨機(jī)生成一個(gè)總位置 0.1~0.9 之間的偏移率,用于滑動(dòng)驗(yàn)證成功的位置。代碼為:
double _generateRandomNumber() {
// 創(chuàng)建一個(gè)Random對(duì)象
var random = Random();
// 生成一個(gè)介于0.1和0.9之間的隨機(jī)小數(shù)
double randomValue = 0.1 + random.nextDouble() * 0.7;
return randomValue;
}
這個(gè)便宜率需要在初始化狀態(tài)時(shí)固定并暫存下來(lái),放在_offsetRate中,可以使用State類的initState實(shí)現(xiàn):
void initState() {
_offsetRate = _generateRandomNumber();
super.initState();
}
_offsetRate 的固定對(duì)于基于Clip的CaptchaClipper類的getClip方法中沒(méi)有什么影響,應(yīng)為在Flutter中Clip是不需要總是去重新繪制的,但是在基于Canvas的CaptchaBorderPainter就不一樣了——畢竟CustomPainter類的paint方法會(huì)被不斷調(diào)用,以至于如果不固定隨機(jī)生成的_offsetRate ,則不斷調(diào)用_generateRandomNumber方法導(dǎo)致描邊位置錯(cuò)亂。實(shí)際的偏移量,無(wú)非是媒體查詢出來(lái)的寬度去乘以這個(gè)便宜率:
width = MediaQuery.of(context).size.width;
_offsetValue = _offsetRate * width;
背景標(biāo)記層偏移
背景標(biāo)記層的偏移是一個(gè)固定的偏移量,這個(gè)偏移量由初始化的_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);
}
可見(jiàn),offsetValue不變則水平位置再不變。
拼圖層偏移
拼圖層是對(duì)于一個(gè)和背景圖大小完全一樣的圖片的一個(gè)水平隨機(jī)位置的一小塊裁剪的,裁剪的初始有一個(gè)隨機(jī)的偏移量,和背景標(biāo)記層偏移是一樣的,就是 offsetValue,著只不過(guò)是一個(gè)初始的裁剪距離左側(cè)的偏離距離,即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;
}
前面有一個(gè)“offsetValue + …”。就是初始在相對(duì)于原圖片左邊的偏移量。正因?yàn)橛辛诉@個(gè)量,裁剪的不是圖片左邊的一部分,但是下面滑塊卻是初始時(shí)位于最左邊的,我們需要在堆疊時(shí)將這個(gè)偏移量減去,就使得偏移的裁剪與底下的滑塊初始時(shí)是“對(duì)齊”的:
// 拼圖層
Positioned(
// 減去偏移量與滑塊對(duì)齊
left: _sliderValue * width - _offsetValue,
child: ClipPath(
clipper: CaptchaClipper(_sliderValue, _offsetValue),
child: Image.network(
widget.imageUrl,
height: 200.0,
fit: BoxFit.cover,
),
),
),
這樣也就是表明,一開(kāi)始的位置是最左邊的位置,只有用戶滑動(dòng)滑塊才會(huì)有可能移動(dòng)到驗(yàn)證成功的位置!
拼圖的輪廓層偏移
拼圖的輪廓層偏移 和 拼圖層偏移的值始終保持相等的,但是需要注意的是,由于它們使用的技術(shù)分別是CustomClipper和Canvas,Clip的getClip 和 CustomPainter 的 paint執(zhí)行時(shí)機(jī)不同,為了使得在paint中采用和相同的offsetValue,我們將第一次隨機(jī)生成的偏移量做過(guò)緩存。
// 拼圖的輪廓層
Positioned(
left: _sliderValue * width - _offsetValue,
child: CustomPaint(
size: Size(width, 200.0),
painter: CaptchaBorderPainter(_offsetValue),
),
),
6. 總結(jié)、展望/后續(xù)版本
滑動(dòng)控制UI
使用Slider實(shí)現(xiàn)的滑動(dòng)控制器一些UI效果實(shí)現(xiàn)還不好看,用起來(lái)也不方便觸摸,如果改成矩形的可能會(huì)更好。下一步計(jì)劃使用繪圖來(lái)替代Slider繪制矩形風(fēng)格的滑塊和滑槽,滑塊可以使用擬物風(fēng)格的圖標(biāo)。
輪廓形狀
上面繪制的輪廓是簡(jiǎn)單的圓角矩形,不過(guò)如果改版為拼圖的常見(jiàn)形狀,比如:
會(huì)更加好看一些。
可以將再下一個(gè)版本中可以考慮重新使用Canvas繪制。
自帶成功效果
如果驗(yàn)證成功后,可以在整個(gè)圖片疊加區(qū)域上面添加一個(gè)半透明白色堆疊物實(shí)現(xiàn)一個(gè)成功覆蓋層,并且在中間加一個(gè)圓形背景的勾(√)來(lái)增強(qiáng)效果。
重置驗(yàn)證碼
有時(shí)候可能用戶一次驗(yàn)證沒(méi)有成功,但也不意味著是機(jī)器人。目前可以重新構(gòu)建組件,并傳入新的圖片來(lái)。不過(guò)可以考慮一個(gè)用于直接刷洗驗(yàn)證碼的接口。文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-734096.html
滑動(dòng)時(shí)間
為了讓用戶體驗(yàn)更加絲滑,可以考慮手勢(shì)的時(shí)間計(jì)算,一旦驗(yàn)證成功,則告訴用于“本次認(rèn)證使用了xx秒,超過(guò)了99%的用戶”。文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-734096.html
到了這里,關(guān)于Flutter筆記:手寫(xiě)并發(fā)布一個(gè)人機(jī)滑動(dòng)驗(yàn)證碼插件的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!