Flutter手勢密碼的實(shí)現(xiàn)示例(附demo)
前言
本篇記錄的是使用Flutter完成手勢密碼的功能,大致效果如下圖所示:
該手勢密碼的功能比較簡單,下面會(huì)詳細(xì)記錄實(shí)現(xiàn)的過程,另外還會(huì)簡單說明如何將該手勢密碼作為插件發(fā)布到pub倉庫。
開始
實(shí)現(xiàn)上面的手勢密碼并不難,大致可以拆分成如下幾部分來完成:
- 繪制9個(gè)圓點(diǎn)
- 繪制手指滑動(dòng)的線路
- 合并以上兩個(gè)部分
繪制圓點(diǎn)
我們使用面向?qū)ο蟮姆绞絹硖幚?個(gè)圓點(diǎn)的繪制,每個(gè)圓點(diǎn)作為一個(gè)GesturePoint類,這個(gè)類要提供一個(gè)圓心坐標(biāo)和半徑才能畫出圓形來,這里先放上這個(gè)類的源碼:
// point.dart import 'package:flutter/material.dart'; import 'dart:math'; // 手勢密碼盤上的圓點(diǎn) class GesturePoint { // 中心實(shí)心圓點(diǎn)的畫筆 static final pointPainter = Paint() ..style = PaintingStyle.fill ..color = Colors.blue; // 外層圓環(huán)的畫筆 static final linePainter = Paint() ..style = PaintingStyle.stroke ..strokeWidth = 1.2 ..color = Colors.blue; // 圓點(diǎn)索引,0-9 final int index; // 圓心坐標(biāo) final double centerX; final double centerY; // 中心實(shí)心圓點(diǎn)的半徑 final double radius = 4; // 外層空心的圓環(huán)半徑 final double padding = 26; GesturePoint(this.index, this.centerX, this.centerY); // 繪制小圓點(diǎn) void drawCircle(Canvas canvas) { // 繪制中心實(shí)心的圓點(diǎn) canvas.drawOval( Rect.fromCircle(center: Offset(centerX, centerY), radius: radius), pointPainter); // 繪制外層的圓環(huán) canvas.drawOval( Rect.fromCircle(center: Offset(centerX, centerY), radius: padding), linePainter); } // 判斷坐標(biāo)是否在小圓內(nèi)(padding為半徑) // 該方法用于在手指滑動(dòng)時(shí)做判斷,一旦坐標(biāo)處于圓點(diǎn)內(nèi)部,則認(rèn)為選中該圓點(diǎn) bool checkInside(double x, double y) { var distance = sqrt(pow((x - centerX), 2) + pow((y - centerY), 2)); return distance <= padding; } // 提供比較方法,用于判斷List中是否存在某個(gè)點(diǎn) // 這個(gè)方法會(huì)在后面用到,當(dāng)手勢滑動(dòng)到某個(gè)點(diǎn)時(shí),如果之前滑動(dòng)到過這個(gè)點(diǎn),則這個(gè)點(diǎn)不能再被選中 @override bool operator ==(Object other) { if (other is GesturePoint) { return this.index == other.index && this.centerX == other.centerX && this.centerY == other.centerY; } return false; } // 復(fù)寫==方法時(shí)必須同時(shí)復(fù)寫hashCode方法 @override int get hashCode => super.hashCode; }
上面需要注意的是,GesturePoint類提供了一個(gè)drawCircle方法用于繪制自身,將會(huì)在后面的代碼中用到。
有了圓點(diǎn)這個(gè)對(duì)象,我們還需要將9個(gè)圓點(diǎn)依次畫在屏幕上,由于這9個(gè)圓點(diǎn)后續(xù)是不再更新的,所以使用一個(gè)StatelessWidget即可。(如果你需要做成手指滑動(dòng)到某個(gè)圓點(diǎn),該圓點(diǎn)變色的效果,則需要用StatefulWidget組件去更新狀態(tài)。)
下面使用一個(gè)自定義的無狀態(tài)組件去畫這9個(gè)圓點(diǎn),代碼如下:
// panel.dart import 'package:flutter/material.dart'; import 'package:flutter_gesture_password/point.dart'; // 9個(gè)圓點(diǎn)視圖 class GestureDotsPanel extends StatelessWidget { // 表示圓點(diǎn)盤的寬高 final double width, height; // 裝載9個(gè)圓點(diǎn)的集合,從外部傳入 final List<GesturePoint> points; // 構(gòu)造方法 GestureDotsPanel(this.width, this.height, this.points); @override Widget build(BuildContext context) { return Container( width: width, height: height, child: CustomPaint( painter: _PanelPainter(points), ), ); } } // 自定義的Painter,用于從圓點(diǎn)集合中遍歷所有圓點(diǎn)并依次畫出 class _PanelPainter extends CustomPainter { final List<GesturePoint> points; _PanelPainter(this.points); @override void paint(Canvas canvas, Size size) { if (points.isNotEmpty) { for (var p in points) { // 畫出所有的圓點(diǎn) p.drawCircle(canvas); } } } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => false; // 不讓更新 }
以上代碼比較簡單,就不做詳細(xì)說明了,如果對(duì)Flutter繪圖基礎(chǔ)還不了解的同學(xué),可以看看這里的介紹:《Flutter實(shí)戰(zhàn)——自繪組件 (CustomPaint與Canvas) 》
繪制手勢路徑
之所以手勢路徑要單獨(dú)拿出來繪制,沒有跟上面的9個(gè)小圓點(diǎn)盤放一起,是因?yàn)槲覀兊膱A點(diǎn)盤是不更新的,而手勢路徑需要在手指的每一次滑動(dòng)中更新,所以單獨(dú)將手勢路徑作為一個(gè)組件。顯然這個(gè)組件是一個(gè)有狀態(tài)的組件,需要繼承StatefulWidget來實(shí)現(xiàn)。
在開始編碼前,我們需要分析手勢滑動(dòng)的流程:
- 必須監(jiān)聽手指按下,手指滑動(dòng),手指抬起三種不同的事件
- 手指按下時(shí),如果不在9個(gè)圓點(diǎn)中的任意一個(gè)上面,則手指滑動(dòng)是無效的
- 手指按下時(shí)若在某個(gè)點(diǎn)上,則后面手指移動(dòng)時(shí),需要繪制從那個(gè)點(diǎn)到手指當(dāng)前的一條直線,若手指移動(dòng)過程中進(jìn)入其他圓點(diǎn),則需要先繪制之前手指經(jīng)過的所有圓點(diǎn)間的直線,再繪制最后一個(gè)圓點(diǎn)到手指當(dāng)前滑動(dòng)的坐標(biāo)間的直線
- 每個(gè)圓點(diǎn)只允許被記錄一次,若之前手指滑動(dòng)經(jīng)過某個(gè)點(diǎn),后面手指再經(jīng)過該點(diǎn)時(shí),該點(diǎn)不應(yīng)該被記錄
- 手指抬起后,需要計(jì)算手指移動(dòng)過程中經(jīng)過了哪些點(diǎn),以數(shù)組的形式返回所有點(diǎn)的索引。且手指抬起后,不需要繪制最后一個(gè)點(diǎn)到手指抬起時(shí)的坐標(biāo)間的直線
梳理了上面的手勢密碼繪制流程后,我們還需要了解Flutter處理手勢的一些API,本例子中主要使用的GestureDetector,這是Flutter官方對(duì)移動(dòng)端手勢封裝的一個(gè)Widget,使用起來非常方便,如果有不太了解的同學(xué),可以參考這里——《Flutter實(shí)戰(zhàn)——手勢識(shí)別》
下面放上繪制手勢密碼路徑的所有代碼:
// path.dart import 'package:flutter/material.dart'; import 'package:flutter_gesture_password/gesture_view.dart'; import 'package:flutter_gesture_password/point.dart'; // 手勢密碼路徑視圖 class GesturePathView extends StatefulWidget { // 手勢密碼路徑視圖的寬高,需要跟圓點(diǎn)視圖保持一致,由構(gòu)造方法傳入 final double width; final double height; // 手勢密碼中的9個(gè)點(diǎn),由構(gòu)造方法傳入 final List<GesturePoint> points; // 手勢密碼監(jiān)聽器,用于在手指抬起時(shí)觸發(fā),其定義為:typedef OnGestureCompleteListener = void Function(List<int>); final OnGestureCompleteListener listener; // 構(gòu)造方法 GesturePathView(this.width, this.height, this.points, this.listener); @override State<StatefulWidget> createState() => _GesturePathViewState(); } class _GesturePathViewState extends State<GesturePathView> { // 記錄手指按下或者滑動(dòng)過程中,經(jīng)過的最后一個(gè)點(diǎn) GesturePoint? lastPoint; // 記錄手指滑動(dòng)時(shí)的坐標(biāo) Offset? movePos; // 記錄手指滑動(dòng)過程中所有經(jīng)過的點(diǎn) List<GesturePoint> pathPoints = []; @override Widget build(BuildContext context) { return GestureDetector( child: CustomPaint( size: Size(widget.width, widget.height), // 指定組件大小 painter: _PathPainter(movePos, pathPoints), // 指定組件的繪制者,當(dāng)movePos或者pathPoints更新時(shí),整個(gè)組件也需要更新 ), onPanDown: _onPanDown, // 手指按下 onPanUpdate: _onPanUpdate, // 手指滑動(dòng) onPanEnd: _onPanEnd, // 手指抬起 ); } // 手指按下 _onPanDown(DragDownDetails e) { // 判斷按下的坐標(biāo)是否在某個(gè)點(diǎn)上 // 注意:e.localPosition表示的坐標(biāo)為相對(duì)整個(gè)組件的坐標(biāo) // e.globalPosition表示的坐標(biāo)為相對(duì)整個(gè)屏幕的坐標(biāo) final x = e.localPosition.dx; final y = e.localPosition.dy; // 判斷是否按在某個(gè)點(diǎn)上 for (var p in widget.points) { if (p.checkInside(x, y)) { lastPoint = p; } } // 重置pathPoints pathPoints.clear(); } // 手指滑動(dòng) _onPanUpdate(DragUpdateDetails e) { // 如果手指按下時(shí)不在某個(gè)圓點(diǎn)上,則不處理滑動(dòng)事件 if (lastPoint == null) { return; } // 滑動(dòng)時(shí)如果在某個(gè)圓點(diǎn)上,則將該圓點(diǎn)加入路徑中 final x = e.localPosition.dx; final y = e.localPosition.dy; // passPoint代表手指滑動(dòng)時(shí)是否經(jīng)過某個(gè)點(diǎn),可為空 GesturePoint? passPoint; for (var p in widget.points) { // 如果手指滑動(dòng)經(jīng)過某個(gè)點(diǎn),且這個(gè)點(diǎn)之前沒有經(jīng)過,則記錄下這個(gè)點(diǎn) if (p.checkInside(x, y) && !pathPoints.contains(p)) { passPoint = p; break; } } setState(() { // 如果經(jīng)過點(diǎn)部為空,則需要刷新lastPoint和pathPoints,觸發(fā)整個(gè)組件的更新 if (passPoint != null) { lastPoint = passPoint; pathPoints.add(passPoint); } // 更新movePos的值 movePos = Offset(x, y); }); } // 手指抬起 _onPanEnd(DragEndDetails e) { setState(() { // 將movePos設(shè)置為空,防止畫出最后一個(gè)點(diǎn)到手指抬起時(shí)的坐標(biāo)間的直線 movePos = null; }); // 調(diào)用Listener,返回手勢經(jīng)過的所有點(diǎn) List<int> arr = []; if (pathPoints.isNotEmpty) { for (var value in pathPoints) { arr.add(value.index); } } widget.listener(arr); } } // 繪制手勢路徑 class _PathPainter extends CustomPainter { // 手指當(dāng)前的坐標(biāo) final Offset? movePos; // 手指經(jīng)過點(diǎn)集合 final List<GesturePoint> pathPoints; // 路徑畫筆 final pathPainter = Paint() ..style = PaintingStyle.stroke ..strokeWidth = 6 ..strokeCap = StrokeCap.round ..color = Colors.blue; _PathPainter(this.movePos, this.pathPoints); @override void paint(Canvas canvas, Size size) { _drawPassPath(canvas); _drawRTPath(canvas); } // 繪制手指一動(dòng)過程中,經(jīng)過的所有點(diǎn)之間的直線 _drawPassPath(Canvas canvas) { if (pathPoints.length <= 1) { return; } for (int i = 0; i < pathPoints.length - 1; i++) { var start = pathPoints[i]; var end = pathPoints[i + 1]; canvas.drawLine(Offset(start.centerX, start.centerY), Offset(end.centerX, end.centerY), pathPainter); } } // 繪制實(shí)時(shí)的,最后一個(gè)經(jīng)過點(diǎn)和當(dāng)前手指坐標(biāo)間的直線 _drawRTPath(Canvas canvas) { if (pathPoints.isNotEmpty && movePos != null) { var lastPoint = pathPoints.last; canvas.drawLine(Offset(lastPoint.centerX, lastPoint.centerY), movePos!, pathPainter); } } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => true; }
組合9個(gè)圓點(diǎn)盤和手勢路徑
組合這兩個(gè)組件需要用到Stack組件,代碼比較簡單,直接上代碼了:
import 'package:flutter/material.dart'; import 'package:flutter_gesture_password/path.dart'; import 'package:flutter_gesture_password/point.dart'; import 'package:flutter_gesture_password/panel.dart'; // 定義手勢密碼回調(diào)監(jiān)聽器 typedef OnGestureCompleteListener = void Function(List<int>); class GestureView extends StatefulWidget { final double width, height; final OnGestureCompleteListener listener; GestureView({required this.width, required this.height, required this.listener}); @override State<StatefulWidget> createState() => _GestureViewState(); } class _GestureViewState extends State<GestureView> { List<GesturePoint> _points = []; @override void initState() { super.initState(); // 計(jì)算9個(gè)圓點(diǎn)的位置坐標(biāo) double deltaW = widget.width / 4; double deltaH = widget.height / 4; for (int row = 0; row < 3; row++) { for (int col = 0; col < 3; col++) { int index = row * 3 + col; var p = GesturePoint(index, (col + 1) * deltaW, (row + 1) * deltaH); _points.add(p); } } } @override Widget build(BuildContext context) { return Stack( children: [ GestureDotsPanel(widget.width, widget.height, _points), GesturePathView(widget.width, widget.height, _points, widget.listener) ], ); } }
手勢密碼組件的使用
到這里,手勢密碼就開發(fā)完成了,使用起來也非常簡單,本文開篇的預(yù)覽圖使用的如下代碼:
import 'package:flutter/material.dart'; import 'package:flutter_gesture_password/gesture_view.dart'; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Gesture password', home: _Home(), ); } } class _Home extends StatefulWidget { @override State<StatefulWidget> createState() => _HomeState(); } class _HomeState extends State<_Home> { List<int>? pathArr; @override Widget build(BuildContext context) { final screenWidth = MediaQuery.of(context).size.width; return Scaffold( appBar: AppBar( title: Text('Gesture password'), ), body: Column( children: [ GestureView( width: screenWidth, height: screenWidth, listener: (arr) { setState(() { pathArr = arr; }); }, ), Text("${pathArr == null ? '' : pathArr}") ], ), ); } }
上傳自定義組件到pub倉庫
上傳自定義組件到Pub倉庫的流程不算很復(fù)雜,這里先放上官方文檔:https://dart.cn/tools/pub/publishing
下面整理發(fā)布插件到pub倉庫的主要步驟:
(這一步非必須但是建議)在github上新建一個(gè)項(xiàng)目,并將我們寫的代碼push到該倉庫。(后面配置homepage時(shí)可以直接使用GitHub倉庫地址)
在項(xiàng)目根目錄下創(chuàng)建README.md文件,在其中編寫對(duì)于項(xiàng)目的一些介紹,以及你編寫的插件的用法
在項(xiàng)目根目錄下創(chuàng)建CHANGELOG.md文件,記錄每個(gè)不同版本更新了什么
在項(xiàng)目根目錄下新建一個(gè)LICENSE文件,表明該插件使用什么開源協(xié)議
修改項(xiàng)目中的pubspec.yaml文件,主要修改點(diǎn)有:
homepage: 「填寫項(xiàng)目主頁地址,這里可以直接用github倉庫地址」 publish_to: 'https://pub.dev' # 這個(gè)配置表示要把插件發(fā)布到哪里 version: 0.0.2 # 插件版本,每次更新記得修改這個(gè)version
在項(xiàng)目根目錄下執(zhí)行dart pub publish,首次執(zhí)行會(huì)出現(xiàn)如下提示:
Package has 2 warnings.. Do you want to publish xxx 0.0.1 (y/N)? y
Pub needs your authorization to upload packages on your behalf.
In a web browser, go to https://accounts.google.com/o/oauth2/auth?access_type=offline&approval_prompt=force&response_type=code&client_id=8183068855108-8grd2eg9tjq9f38os6f1urbcvsq39u8n.apps.googleusercontent.com&redirect_uri=http%3A%2F%2Flocalhost%3A55486&code_challenge=V1-sGcrLkXljXXpOyJdqf8BJfRzBcUQaH9G1m329_M&code_challenge_method=S2536&scope=openid+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email
Then click "Allow access".Waiting for your authorization...
點(diǎn)擊上面的鏈接會(huì)打開瀏覽器,授權(quán)即可。授權(quán)通過后,控制臺(tái)會(huì)提示上傳完成等信息。
后記
本篇記錄的Flutter手勢密碼已經(jīng)上傳到pub倉庫,地址為:https://pub.dev/packages/flutter_gesture_password
該項(xiàng)目的源碼已托管至GitHub:https://github.com/yubo725/flutter-gesture-password
手勢密碼的最基本的實(shí)現(xiàn)方式就是上面的過程了,在本例中我并未做過多的封裝,也沒有提供更多的配置項(xiàng)比如手勢密碼圓點(diǎn)顏色,路徑線條顏色、粗細(xì)等等,這些大家可以根據(jù)自己的項(xiàng)目,自行拷貝代碼并做相應(yīng)修改。另外,手勢密碼的保存與校驗(yàn)不在本篇記錄范圍內(nèi),大家可以根據(jù)最終的整型數(shù)組來做一些加密之類并保存到本地,在校驗(yàn)密碼時(shí),做字符串匹配即可。
到此這篇關(guān)于Flutter手勢密碼的實(shí)現(xiàn)示例(附demo)的文章就介紹到這了,更多相關(guān)Flutter手勢密碼內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
flutter實(shí)現(xiàn)底部不規(guī)則導(dǎo)航欄
這篇文章主要為大家詳細(xì)介紹了flutter實(shí)現(xiàn)底部不規(guī)則導(dǎo)航欄,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-07-07Android網(wǎng)格布局GridView學(xué)習(xí)使用
這篇文章主要為大家詳細(xì)介紹了Android網(wǎng)格布局GirdView的學(xué)習(xí)使用,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-12-12android 6.0下webview的定位權(quán)限設(shè)置方法
今天小編就為大家分享一篇android 6.0下webview的定位權(quán)限設(shè)置方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2018-07-07詳解Android 8.1.0 Service 中 彈出 Dialog的方法
這篇文章主要介紹了Android 8.1.0 Service 中怎么彈出 Dialog問題,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-10-10Flutter實(shí)現(xiàn)單選,復(fù)選和開關(guān)組件的示例代碼
在App開發(fā)過程中,選擇交互是非常常見的,今天主要介紹下關(guān)于選擇的三個(gè)組件的使用:開關(guān)、單選和復(fù)選,感興趣的小伙伴可以了解一下2022-04-04Android編程設(shè)計(jì)模式之訪問者模式詳解
這篇文章主要介紹了Android編程設(shè)計(jì)模式之訪問者模式,詳細(xì)分析了訪問者模式的概念、功能、原理、使用場景并結(jié)合實(shí)例形式給出了Android訪問者模式的具體實(shí)現(xiàn)技巧與相關(guān)操作注意事項(xiàng),需要的朋友可以參考下2017-12-12Flutter 透明狀態(tài)欄及字體顏色的設(shè)置方法
這篇文章主要介紹了Flutter 透明狀態(tài)欄及字體顏色的設(shè)置方法,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-04-04Android字符串資源文件format方法使用實(shí)例
本文介紹了Android的資源文件values/strings.xml中如何實(shí)現(xiàn)格式化字符串,這里舉個(gè)簡單的例子供大家參考2013-11-11Android 開發(fā)程序鎖應(yīng)用簡單實(shí)例
這篇文章主要介紹了Android 開發(fā)程序鎖應(yīng)用簡單實(shí)例的相關(guān)資料,需要的朋友可以參考下2016-10-10