Flutter手势密码插件从开发到发布至pub仓库
Posted yubo_725
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Flutter手势密码插件从开发到发布至pub仓库相关的知识,希望对你有一定的参考价值。
本文同步发布在掘金社区:https://juejin.cn/post/6996860982488219661
前言
本篇记录的是使用Flutter完成手势密码的功能,大致效果如下图所示:
该手势密码的功能比较简单,下面会详细记录实现的过程,另外还会简单说明如何将该手势密码作为插件发布到pub仓库。
开始
实现上面的手势密码并不难,大致可以拆分成如下几部分来完成:
-
绘制9个圆点
-
绘制手指滑动的线路
-
合并以上两个部分
绘制圆点
我们使用面向对象的方式来处理9个圆点的绘制,每个圆点作为一个GesturePoint
类,这个类要提供一个圆心坐标和半径才能画出圆形来,这里先放上这个类的源码:
// point.dart
import 'package:flutter/material.dart';
import 'dart:math';
// 手势密码盘上的圆点
class GesturePoint
// 中心实心圆点的画笔
static final pointPainter = Paint()
..style = PaintingStyle.fill
..color = Colors.blue;
// 外层圆环的画笔
static final linePainter = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 1.2
..color = Colors.blue;
// 圆点索引,0-9
final int index;
// 圆心坐标
final double centerX;
final double centerY;
// 中心实心圆点的半径
final double radius = 4;
// 外层空心的圆环半径
final double padding = 26;
GesturePoint(this.index, this.centerX, this.centerY);
// 绘制小圆点
void drawCircle(Canvas canvas)
// 绘制中心实心的圆点
canvas.drawOval(
Rect.fromCircle(center: Offset(centerX, centerY), radius: radius),
pointPainter);
// 绘制外层的圆环
canvas.drawOval(
Rect.fromCircle(center: Offset(centerX, centerY), radius: padding),
linePainter);
// 判断坐标是否在小圆内(padding为半径)
// 该方法用于在手指滑动时做判断,一旦坐标处于圆点内部,则认为选中该圆点
bool checkInside(double x, double y)
var distance = sqrt(pow((x - centerX), 2) + pow((y - centerY), 2));
return distance <= padding;
// 提供比较方法,用于判断List中是否存在某个点
// 这个方法会在后面用到,当手势滑动到某个点时,如果之前滑动到过这个点,则这个点不能再被选中
@override
bool operator ==(Object other)
if (other is GesturePoint)
return this.index == other.index &&
this.centerX == other.centerX &&
this.centerY == other.centerY;
return false;
// 复写==方法时必须同时复写hashCode方法
@override
int get hashCode => super.hashCode;
上面需要注意的是,GesturePoint
类提供了一个drawCircle
方法用于绘制自身,将会在后面的代码中用到。
有了圆点这个对象,我们还需要将9个圆点依次画在屏幕上,由于这9个圆点后续是不再更新的,所以使用一个StatelessWidget
即可。(如果你需要做成手指滑动到某个圆点,该圆点变色的效果,则需要用StatefulWidget组件去更新状态。)
下面使用一个自定义的无状态组件去画这9个圆点,代码如下:
// panel.dart
import 'package:flutter/material.dart';
import 'package:flutter_gesture_password/point.dart';
// 9个圆点视图
class GestureDotsPanel extends StatelessWidget
// 表示圆点盘的宽高
final double width, height;
// 装载9个圆点的集合,从外部传入
final List<GesturePoint> points;
// 构造方法
GestureDotsPanel(this.width, this.height, this.points);
@override
Widget build(BuildContext context)
return Container(
width: width,
height: height,
child: CustomPaint(
painter: _PanelPainter(points),
),
);
// 自定义的Painter,用于从圆点集合中遍历所有圆点并依次画出
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)
// 画出所有的圆点
p.drawCircle(canvas);
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false; // 不让更新
以上代码比较简单,就不做详细说明了,如果对Flutter绘图基础还不了解的同学,可以看看这里的介绍:《Flutter实战——自绘组件 (CustomPaint与Canvas)》
绘制手势路径
之所以手势路径要单独拿出来绘制,没有跟上面的9个小圆点盘放一起,是因为我们的圆点盘是不更新的,而手势路径需要在手指的每一次滑动中更新,所以单独将手势路径作为一个组件。显然这个组件是一个有状态的组件,需要继承StatefulWidget
来实现。
在开始编码前,我们需要分析手势滑动的流程:
-
必须监听手指按下,手指滑动,手指抬起三种不同的事件
-
手指按下时,如果不在9个圆点中的任意一个上面,则手指滑动是无效的
-
手指按下时若在某个点上,则后面手指移动时,需要绘制从那个点到手指当前的一条直线,若手指移动过程中进入其他圆点,则需要先绘制之前手指经过的所有圆点间的直线,再绘制最后一个圆点到手指当前滑动的坐标间的直线
-
每个圆点只允许被记录一次,若之前手指滑动经过某个点,后面手指再经过该点时,该点不应该被记录
-
手指抬起后,需要计算手指移动过程中经过了哪些点,以数组的形式返回所有点的索引。且手指抬起后,不需要绘制最后一个点到手指抬起时的坐标间的直线
梳理了上面的手势密码绘制流程后,我们还需要了解Flutter处理手势的一些API,本例子中主要使用的GestureDetector
,这是Flutter官方对移动端手势封装的一个Widget,使用起来非常方便,如果有不太了解的同学,可以参考这里——《Flutter实战——手势识别》
下面放上绘制手势密码路径的所有代码:
// 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
// 手势密码路径视图的宽高,需要跟圆点视图保持一致,由构造方法传入
final double width;
final double height;
// 手势密码中的9个点,由构造方法传入
final List<GesturePoint> points;
// 手势密码监听器,用于在手指抬起时触发,其定义为:typedef OnGestureCompleteListener = void Function(List<int>);
final OnGestureCompleteListener listener;
// 构造方法
GesturePathView(this.width, this.height, this.points, this.listener);
@override
State<StatefulWidget> createState() => _GesturePathViewState();
class _GesturePathViewState extends State<GesturePathView>
// 记录手指按下或者滑动过程中,经过的最后一个点
GesturePoint? lastPoint;
// 记录手指滑动时的坐标
Offset? movePos;
// 记录手指滑动过程中所有经过的点
List<GesturePoint> pathPoints = [];
@override
Widget build(BuildContext context)
return GestureDetector(
child: CustomPaint(
size: Size(widget.width, widget.height), // 指定组件大小
painter: _PathPainter(movePos, pathPoints), // 指定组件的绘制者,当movePos或者pathPoints更新时,整个组件也需要更新
),
onPanDown: _onPanDown, // 手指按下
onPanUpdate: _onPanUpdate, // 手指滑动
onPanEnd: _onPanEnd, // 手指抬起
);
// 手指按下
_onPanDown(DragDownDetails e)
// 判断按下的坐标是否在某个点上
// 注意:e.localPosition表示的坐标为相对整个组件的坐标
// e.globalPosition表示的坐标为相对整个屏幕的坐标
final x = e.localPosition.dx;
final y = e.localPosition.dy;
// 判断是否按在某个点上
for (var p in widget.points)
if (p.checkInside(x, y))
lastPoint = p;
// 重置pathPoints
pathPoints.clear();
// 手指滑动
_onPanUpdate(DragUpdateDetails e)
// 如果手指按下时不在某个圆点上,则不处理滑动事件
if (lastPoint == null)
return;
// 滑动时如果在某个圆点上,则将该圆点加入路径中
final x = e.localPosition.dx;
final y = e.localPosition.dy;
// passPoint代表手指滑动时是否经过某个点,可为空
GesturePoint? passPoint;
for (var p in widget.points)
// 如果手指滑动经过某个点,且这个点之前没有经过,则记录下这个点
if (p.checkInside(x, y) && !pathPoints.contains(p))
passPoint = p;
break;
setState(()
// 如果经过点部为空,则需要刷新lastPoint和pathPoints,触发整个组件的更新
if (passPoint != null)
lastPoint = passPoint;
pathPoints.add(passPoint);
// 更新movePos的值
movePos = Offset(x, y);
);
// 手指抬起
_onPanEnd(DragEndDetails e)
setState(()
// 将movePos设置为空,防止画出最后一个点到手指抬起时的坐标间的直线
movePos = null;
);
// 调用Listener,返回手势经过的所有点
List<int> arr = [];
if (pathPoints.isNotEmpty)
for (var value in pathPoints)
arr.add(value.index);
widget.listener(arr);
// 绘制手势路径
class _PathPainter extends CustomPainter
// 手指当前的坐标
final Offset? movePos;
// 手指经过点集合
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);
// 绘制手指一动过程中,经过的所有点之间的直线
_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);
// 绘制实时的,最后一个经过点和当前手指坐标间的直线
_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个圆点盘和手势路径
组合这两个组件需要用到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';
// 定义手势密码回调监听器
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();
// 计算9个圆点的位置坐标
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)
],
);
手势密码组件的使用
到这里,手势密码就开发完成了,使用起来也非常简单,本文开篇的预览图使用的如下代码:
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仓库的流程不算很复杂,这里先放上官方文档:https://dart.cn/tools/pub/publishing
下面整理发布插件到pub仓库的主要步骤:
- (这一步非必须但是建议)在github上新建一个项目,并将我们写的代码push到该仓库。(后面配置homepage时可以直接使用GitHub仓库地址)
- 在项目根目录下创建README.md文件,在其中编写对于项目的一些介绍,以及你编写的插件的用法
- 在项目根目录下创建CHANGELOG.md文件,记录每个不同版本更新了什么
- 在项目根目录下新建一个LICENSE文件,表明该插件使用什么开源协议
- 修改项目中的
pubspec.yaml
文件,主要修改点有:homepage: 「填写项目主页地址,这里可以直接用github仓库地址」 publish_to: 'https://pub.dev' # 这个配置表示要把插件发布到哪里 version: 0.0.2 # 插件版本,每次更新记得修改这个version
- 在项目根目录下执行
dart pub publish
,首次执行会出现如下提示:
点击上面的链接会打开浏览器,授权即可。授权通过后,控制台会提示上传完成等信息。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...
后记
本篇记录的Flutter手势密码已经上传到pub仓库,地址为:https://pub.dev/packages/flutter_gesture_password
该项目的源码已托管至GitHub:https://github.com/yubo725/flutter-gesture-password
如果大家觉得有帮助,请不吝给个Star支持一下。
手势密码的最基本的实现方式就是上面的过程了,在本例中我并未做过多的封装,也没有提供更多的配置项比如手势密码圆点颜色,路径线条颜色、粗细等等,这些大家可以根据自己的项目,自行拷贝代码并做相应修改。另外,手势密码的保存与校验不在本篇记录范围内,大家可以根据最终的整型数组来做一些加密之类并保存到本地,在校验密码时,做字符串匹配即可。
以上是关于Flutter手势密码插件从开发到发布至pub仓库的主要内容,如果未能解决你的问题,请参考以下文章