Flutter实现手势解锁

77 阅读4分钟

效果图

ezgif-5cef6f9902fbd9.gif

自定义手势代码

import 'package:flutter/material.dart';

/// 手势绘制结果
enum GesturePwdResult {
  /// 手势正确
  success,
  /// 手势错误
  failed,

  /// 点位数不满足
  lack,
}


/// 手势组件
class GesturePassword extends StatefulWidget {
  const GesturePassword({
    super.key,
    required this.onChange,
    required this.savePwd,
    this.size,
    this.autoRestore,
    this.duration,
  });

  /// 手势区域大小
  final Size? size;

  /// 保存的手势密码
  final List<int> savePwd;

  /// 回调选中的点
  final Function(GesturePwdResult result) onChange;

  /// 错误时是否需要自动消除,默认自动消除
  final bool? autoRestore;

  /// 消除延时时间,默认时间是100毫秒
  final Duration? duration;

  @override
  State<GesturePassword> createState() => _GesturePasswordState();
}

class _GesturePasswordState extends State<GesturePassword> {
  /// 触摸区域
  late Size _size;

  /// 手势点列表
  List<Offset> points = [];

  /// 点位半径
  double pointRadius = 10;

  /// 点位选中时的外环半径
  late double pointRingRadius;

  /// 点外围可触发选中的半径
  late double touchRadius;

  /// 记录当前选中的点的索引
  List<int> selectedPoints = [];

  /// 记录当前触摸点的位置
  Offset currentTouchPoint = Offset.zero;

  /// 选中时点颜色
  final Color pointColor = const Color.fromRGBO(15, 120, 205, 1);

  /// 选中时光环颜色
  final Color ringColor = const Color.fromRGBO(231, 241, 250, 1);

  /// 错误时选中点的颜色
  final Color errorPointColor = const Color.fromRGBO(250, 81, 81, 1);

  /// 错误时选中光环的颜色
  final Color errorRingColor = const Color.fromRGBO(254, 237, 237, 1);

  /// 点的颜色
  late Color pointSelectColor;

  /// 光环颜色
  late Color ringSelectColor;

  @override
  void initState() {
    super.initState();
    // 设置手势操作范围
    _size = widget.size ?? const Size.square(200);
    // 设置选中点的颜色
    _drawColor();

    // 计算九个点的位置
    initPoints();
  }

  /// 生成九宫格点位
  void initPoints() {
    //计算一个格子的半径
    double singleRadius = _size.width / 6;
    for (int i = 0; i < 3; i++) {
      for (int j = 0; j < 3; j++) {
        double x = singleRadius + j * (2 * singleRadius);
        double y = singleRadius + i * (2 * singleRadius);
        points.add(Offset(x, y));
      }
    }

    // 计算可触发选中范围
    touchRadius = singleRadius*2/3;
    // 计算选中的圆环半径
    pointRingRadius = singleRadius / 2;
    // 计算点的半径
    pointRadius = pointRingRadius / 2.5;
  }

  int? _getTouchIndex(Offset position){
    for (int i = 0; i < points.length; i++) {
      if ((position - points[i]).distance < touchRadius) {
        return i;
      }
    }
    return null;
  }

  /// 手势开始事件处理
  void _handStart(DragStartDetails details) {
    setState(() {
      _drawColor();
      selectedPoints.clear();
      // 检查触摸点是否在某个解锁点内
      int? touchIndex = _getTouchIndex(details.localPosition);
      if(touchIndex!=null){
        selectedPoints.add(touchIndex);
      }
    });
  }

  /// 手势移动事件处理
  void _handMove(DragUpdateDetails details) {
    setState(() {
      _drawColor();
      currentTouchPoint = details.localPosition;
      int? touchIndex = _getTouchIndex(currentTouchPoint);
      if(touchIndex!=null && !selectedPoints.contains(touchIndex)){
        // 判断两点间有没有别的点
        _addIntermediateDots(touchIndex);
        selectedPoints.add(touchIndex);
      }
    });
  }

  /// 手势抬起事件处理
  void _handEnd(DragEndDetails details) {
    setState(() {
      currentTouchPoint = Offset.zero;
      // 判断点位是否小于4个
      if (selectedPoints.length < 4) {
        _errorError();
        _delayRestore();
        widget.onChange(GesturePwdResult.lack);
        return;
      }
      // 验证解锁结果
      String selectPwd = selectedPoints.join();
      String correctPwd = widget.savePwd.join();
      if (selectPwd == correctPwd) {
        _drawColor();
        widget.onChange(GesturePwdResult.success);
      } else {
        _errorError();
        _delayRestore();
        widget.onChange(GesturePwdResult.failed);
      }
    });
  }

  /// 延迟消失
  void _delayRestore() {
    if ((widget.autoRestore ?? true) == false) return;
    Duration duration = widget.duration ?? const Duration(milliseconds:100);
    Future.delayed(duration, () {
      setState(() {
        _drawColor();
        selectedPoints.clear();
      });
    });
  }

  /// 判断两点之间是否有点,选中它
  void _addIntermediateDots(int newDot) {
    if (selectedPoints.isNotEmpty) {
      int lastDot = selectedPoints.last;
      int midDot = _getMidDot(lastDot, newDot);
      if (midDot != -1 && !selectedPoints.contains(midDot)) {
        selectedPoints.add(midDot);
      }
    }
  }

  int _getMidDot(int dot1, int dot2) {
    // 确保 dot1 小于 dot2,方便统一处理
    if (dot1 > dot2) {
      int temp = dot1;
      dot1 = dot2;
      dot2 = temp;
    }

    // 横向判断
    if ((dot1 % 3 == 0 && dot2 % 3 == 2) && (dot1 ~/ 3 == dot2 ~/ 3)) {
      return dot1 + 1;
    }
    // 纵向判断
    if ((dot1 < 3 && dot2 >= 6) && (dot1 % 3 == dot2 % 3)) {
      return dot1 + 3;
    }
    // 对角线判断(左上到右下)
    if (dot1 == 0 && dot2 == 8) {
      return 4;
    }
    // 对角线判断(右上到左下)
    if (dot1 == 2 && dot2 == 6) {
      return 4;
    }
    return -1;
  }

  /// 蓝色选中
  void _drawColor() {
    pointSelectColor = pointColor;
    ringSelectColor = ringColor;
  }

  /// 红色选中
  void _errorError() {
    pointSelectColor = errorPointColor;
    ringSelectColor = errorRingColor;
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onPanStart: _handStart,
      onPanUpdate: _handMove,
      onPanEnd: _handEnd,
      child: CustomPaint(
        painter: _GesturePainter(
          points: points,
          selectPoints: selectedPoints,
          curTouchPoint: currentTouchPoint,
          pointRadius: pointRadius,
          pointRingRadius: pointRingRadius,
          selectPointColor: pointSelectColor,
          selectRingColor: ringSelectColor,
        ),
        size: _size,
      ),
    );
  }
}

/// 自定义手势绘制
class _GesturePainter extends CustomPainter {
  final List<Offset> points;
  late List<int> selectPoints;
  final Offset curTouchPoint;
  final double pointRadius;
  final double pointRingRadius;

  /// 选中时点的颜色
  final Color selectPointColor;

  /// 选中时外环的颜色
  final Color selectRingColor;

  /// 未选中时的颜色
  final Color normalColor = const Color.fromRGBO(87, 107, 149, 0.2);

  /// 连线宽度
  final double lineWidth = 1.5;

  _GesturePainter({
    required this.points,
    required this.selectPoints,
    required this.curTouchPoint,
    required this.pointRadius,
    required this.pointRingRadius,
    required this.selectPointColor,
    required this.selectRingColor,
  });

  @override
  void paint(Canvas canvas, Size size) {
    // 绘制九宫格点位
    Paint pointPaint = Paint()
      ..color = normalColor
      ..style = PaintingStyle.fill
      ..strokeWidth = 2;
    for (Offset point in points) {
      canvas.drawCircle(point, pointRadius, pointPaint);
    }

    // 绘制选中点
    Paint selectedPointPaint = Paint()..style = PaintingStyle.fill;
    for (int index in selectPoints) {
      //绘制外环
      selectedPointPaint.color = selectRingColor;
      canvas.drawCircle(points[index], pointRingRadius, selectedPointPaint);
      //绘制点
      selectedPointPaint.color = selectPointColor;
      canvas.drawCircle(points[index], pointRadius, selectedPointPaint);
    }

    // 绘制选中点之间的连线
    if (selectPoints.isNotEmpty) {
      Paint linePaint = Paint()
        ..color = selectPointColor
        ..style = PaintingStyle.stroke
        ..strokeWidth = lineWidth
        ..strokeJoin = StrokeJoin.round
        ..strokeCap = StrokeCap.round;
      for (int i = 0; i < selectPoints.length - 1; i++) {
        canvas.drawLine(
            points[selectPoints[i]], points[selectPoints[i + 1]], linePaint);
      }
      if (curTouchPoint != Offset.zero) {
        canvas.drawLine(points[selectPoints.last], curTouchPoint, linePaint);
      }
    }
  }

  @override
  bool shouldRepaint(_GesturePainter oldDelegate) {
    // return oldDelegate.points != points ||
    //     oldDelegate.selectPoints != selectPoints ||
    //     oldDelegate.curTouchPoint != curTouchPoint ||
    //     oldDelegate.selectPointColor != selectPointColor ||
    //     oldDelegate.selectRingColor != selectRingColor;
    return true;
  }
}

使用

import 'package:flutter/material.dart';
import 'package:gesture_demo/gesture_password.dart';

class TestGesture extends StatefulWidget {
  const TestGesture({super.key});

  @override
  State<TestGesture> createState() => _TestGestureState();
}

class _TestGestureState extends State<TestGesture> {
  /// 原来保存的密码
  List<int> savePwd = [0, 1, 2, 4, 6];

  final ValueNotifier<String> _tips = ValueNotifier('请绘制手势密码,至少连接4个点');
  Color _textColor = Colors.black;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(title: const Text('手势密码')),
        body: Column(
          children: [
            Container(
              height: 80,
            ),
            ValueListenableBuilder(
              valueListenable: _tips,
              builder: (context,value,child) {
                return Text(value,
                    style:  TextStyle(fontSize: 14, color: _textColor));
              }
            ),
            Expanded(
                child: Container(
              alignment: Alignment.topCenter,
              margin: const EdgeInsets.only(top: 60),
              child: Container(
                width: 300,
                height: 300,
                child: GesturePassword(
                  size: const Size.square(300),
                  savePwd: savePwd,
                  onChange: (result) {
                    switch(result){
                      case GesturePwdResult.lack:
                        _tips.value = '至少连接4个点,请重新绘制';
                        _textColor = Colors.red;
                        return;
                      case GesturePwdResult.failed:
                        _tips.value = '手势不一致,请重新绘制';
                        _textColor = Colors.red;
                        return;
                      default:
                        _tips.value = '验证成功';
                        _textColor = Colors.blue;
                    }
                  },
                ),
              )

            )),
          ],
        ));
  }
}