用 canvas 实现 Web 手势解锁

2017/04/04 · HTML5 ·
Canvas

原文出处: songjz   

最近参加 360 暑假的前端星计划,有一个在线作业,截止日期是 3 月 30
号,让手动实现一个 H5 手势解锁,具体的效果就像原生手机的九宫格解锁那样。

金沙国际官网 1

实现的最终效果就像下面这张图这样:

金沙国际官网 2

基本要求是这样的:将密码保存到 localStorage
里,开始的时候会从本地读取密码,如果没有就让用户设置密码,密码最少为五位数,少于五位要提示错误。需要对第一次输入的密码进行验证,两次一样才能保持,然后是验证密码,能够对用户输入的密码进行验证。

支付宝登陆界面(手势解锁的实现),手势解锁

 //1.下面是实现的步骤,基本上下面的注释应该都写明白了,多谢大牛们指点,如果需要素材和源工程文件,可以索要,谢谢支持

 //2.在最下面附有效果图

#import “ViewController.h”

#import “FFFGestureView.h”

@interface ViewController ()

@property (weak, nonatomic) IBOutlet UIImageView *smallView;

@property (weak, nonatomic) IBOutlet FFFGestureView *gestureView;

@end

@implementation ViewController

– (void)viewDidLoad {

    [super viewDidLoad];

    self.view.backgroundColor = [UIColor
colorWithPatternImage:[UIImage imageNamed:@”Home_refresh_bg”]];

    self.gestureView.myblock = ^(UIImage *image,NSString *pass){

        NSString *turePass = @”012″;

        if([pass isEqualToString:turePass]){

            self.smallView.image = nil;

            return YES;

        }else{

            self.smallView.image = image;

            return NO;

        }

    };

}

***************************************************************************

#import <UIKit/UIKit.h>

@interface FFFGestureView : UIView

@property (nonatomic,copy) BOOL(^myblock)(UIImage *,NSString *);

@end

***************************************************************************

#import “FFFGestureView.h”

#import “SVProgressHUD.h”

#define SUMCOUNT 9

@interface FFFGestureView ()

//定义可变数组加载需要的button

@property (nonatomic,strong) NSArray *buttons;

//设置数组接收画的线

@property (nonatomic,strong) NSMutableArray *lineButton;

//定义一个点,保存手指当前的位置

@property(nonatomic,assign) CGPoint currentPoint;

@end

 

@implementation FFFGestureView

 

-(NSMutableArray *)lineButton{

    if(_lineButton==nil){

        _lineButton = [NSMutableArray array];

    }

    return _lineButton;

}

 

//懒加载button

-(NSArray *)buttons{

    if(_buttons==nil){

        NSMutableArray *arrayM = [NSMutableArray array];

        for(int i=0;i<SUMCOUNT;i++){

            

            UIButton *button = [[UIButton alloc] init];

            button.tag = i;

//            button.backgroundColor = [UIColor redColor];

            [button setUserInteractionEnabled:NO];

            

            [button setBackgroundImage:[UIImage
imageNamed:@”gesture_node_normal”] forState:UIControlStateNormal];

            [button setBackgroundImage:[UIImage
imageNamed:@”gesture_node_highlighted”]
forState:UIControlStateHighlighted];

            [button setBackgroundImage:[UIImage
imageNamed:@”gesture_node_error”] forState:UIControlStateSelected];

            

            [self addSubview:button];

            [arrayM addObject:button];

        }

        _buttons = [arrayM copy];

    }

    return _buttons;

}

 

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{

 

//    获取touch对象

    UITouch *touch = [touches anyObject];

//    获取点击的点

    CGPoint point = [touch locationInView:touch.view];

    

//    遍历所有的按钮

    for(int i=0;i<self.buttons.count;i++){

    

        UIButton *button = self.buttons[i];

//        按钮的frame是否包含了点击的点

        if(CGRectContainsPoint(button.frame, point)){

//        开始高亮状态

            button.highlighted = YES;

            

//            判断这个按钮是不是已经添加到了数组当中,如果没有在添加

            if(![self.lineButton containsObject:button]){

            

                [self.lineButton addObject:button];

            }

        }

    }

}

 

-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event{

    //    获取touch对象

    UITouch *touch = [touches anyObject];

    //    获取点击的点

    CGPoint point = [touch locationInView:touch.view];

    

    //    获取移动的时候手指位置

    self.currentPoint = point;

 

    //    遍历所有的按钮

    for(int i=0;i<self.buttons.count;i++){

        

        UIButton *button = self.buttons[i];

        //        按钮的frame是否包含了点击的点

        if(CGRectContainsPoint(button.frame, point)){

            //        开始高亮状态

            button.highlighted = YES;

//            判断这个按钮是不是已经添加到了数组当中,如果没有在添加

            if(![self.lineButton containsObject:button]){

                

                [self.lineButton addObject:button];

            }

        }

    }

    [self setNeedsDisplay];

}

 

-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event{

 

//    解决错误的时候,最后手指的位置不连接

    self.currentPoint = [[self.lineButton lastObject] center];

    [self setNeedsDisplay];

    

    for (int i=0; i<self.lineButton.count; i++) {

        UIButton *button = self.lineButton[i];

        button.selected = YES;

        button.highlighted = NO;

    }

//    在恢复之前不能进行连线

    [self setUserInteractionEnabled:NO];

    

    NSString *passWord = @””;

    for (int i=0; i<self.lineButton.count; i++) {

        //        拼接按钮的tag

        passWord = [passWord stringByAppendingString:[NSString
stringWithFormat:@”%ld”,[self.lineButton[i] tag]]];

    }

    

//    输出当前VIew作为image

    UIGraphicsBeginImageContextWithOptions(self.bounds.size, NO, 0);

//    获取上下文

    CGContextRef ctx = UIGraphicsGetCurrentContext();

//    渲染

    [self.layer renderInContext:ctx];

//    通过上下文获取图片

    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();

//    关闭上下文

    UIGraphicsEndImageContext();

    

    if(self.myblock){

        if(self.myblock(image,passWord)){

            [SVProgressHUD showSuccessWithStatus:@”密码正确”];

        }else{

            [SVProgressHUD showErrorWithStatus:@”密码错误”];

        }

    }

//    显示错误的样式 1秒钟

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 *
NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

//        恢复之后再把用户交互打开

        [self setUserInteractionEnabled:YES];

        [self clearScreen];

    });

}

 

-(void)clearScreen{

    [self.lineButton removeAllObjects];

    for (int i=0; i<self.buttons.count ; i++) {

        UIButton *button = self.buttons[i];

        button.highlighted = NO;

        button.selected = NO;

    }

//    恢复原始状态

    [self setNeedsDisplay];

}

-(void)drawRect:(CGRect)rect{

 

//    创建路径对象

    UIBezierPath *path = [UIBezierPath bezierPath];

    

    for(int i=0;i<self.lineButton.count;i++){

        if(i==0){

            [path moveToPoint:[self.lineButton[i] center]];

        }else{

            [path addLineToPoint:[self.lineButton[i] center]];

        }

    }

    if(self.lineButton.count){

//     连接到手指的位置

        [path addLineToPoint:self.currentPoint];

    }

//    设置颜色

    [[UIColor redColor] set];

    

//    设置线宽

    path.lineWidth = 10;

    

//    设置连接处的样式

    [path setLineJoinStyle:kCGLineJoinRound];

    

//    设置头尾的样式

    [path setLineCapStyle:kCGLineCapRound];

    

//    渲染

    [path stroke];

}

-(void)layoutSubviews{

    

    [super layoutSubviews];

    

    CGFloat w = 74;

    CGFloat h = w;

    CGFloat margin = (self.frame.size.width-3*w)/4;

    

    for(int i=0;i<self.buttons.count;i++){

    

        UIButton *button = self.buttons[i];

        CGFloat row = i % 3;

        CGFloat col = i / 3;

        CGFloat x = row * (margin + w) + margin;

        CGFloat y = col * (margin + h) + margin;

        button.frame = CGRectMake(x, y, w, h);

    }

}

@end

 

金沙国际官网 3

 

金沙国际官网 4

//1.下面是实现的步骤,基本上下面的注释应该都写明白了,多谢大牛们指点,如果需要素材…

发散

理解手势生命周期可以实现类似 水果忍者,还有笔画的效果

  //
  //  LockGestureView.swift
  //  手势解锁swift
  //
  //  Created by lotawei on 16/12/8.
  //  Copyright © 2016年 lotawei. All rights reserved.
  //

  import UIKit
  import CoreGraphics



  class LockGestureView: UIView {
     private let   imgsize = 50
      private  var   pawd:String!
     private var   rowcount = 3
       var  back:UIImage {
          return  #imageLiteral(resourceName: "back_bg.png")
      }
      var btns: [UIButton]! = [UIButton]()

 override init(frame: CGRect) {
    super.init(frame: frame)
    backgroundColor = #colorLiteral(red: 0.2392156869, green: 0.6745098233, blue: 0.9686274529, alpha: 1)
    initialview()
}
convenience init(frame: CGRect, _ row:Int,_ pwd:String!) {
    self.init(frame:frame)
    self.rowcount = row
    self.pawd = pwd

}
func initialview(){
    //布局

    let    xpadding = (Int( self.bounds.size.width) - 3 * imgsize) / 4 //  计算一个横间距
    let    ypadding =  (Int( self.bounds.size.height) - (rowcount ) * imgsize) / (rowcount + 1)
    var   x = 0
    var   y = 0
    for  i in 0  ..< rowcount * 3  {
        let   btn = UIButton()
        btn.isSelected  = false
        btn.setImage(#imageLiteral(resourceName: "gesture_node_highlighted"), for: .selected)
        btn.setImage(#imageLiteral(resourceName: "gesture_node_highlighted"), for: .highlighted)
        btn.setImage(#imageLiteral(resourceName: "gesture_node_normal"), for: .normal)
        // 并设置一个tag
         btn.tag = i
        addSubview(btn)
        btn.isUserInteractionEnabled = false
        x=xpadding*((i%3)+1)+imgsize*(i%3)
        y=ypadding*((i/rowcount)+1)+imgsize*(i/rowcount)
        btn.frame = CGRect(x: x, y: y, width: imgsize, height: imgsize)

    }

}
required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
}

override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
    let  touch = touches.first  //
    let  point  = touch?.location(in: self)

    for    abtn in self.subviews{
        let  btn = abtn  as!  UIButton
        //表示滑动的时候这个矩形框是否包含这个点
        if btn.frame.contains(point!) {
            if btn.isSelected == false  {
                if !btns.contains(btn) {
                      btns.append(btn)
                }

                setNeedsDisplay()

            }
            btn.isSelected = true
        }
    }
}



override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {


    //自定义视图如果设置交互为false默认会向俯视图传递响应莲  因此  解决方式  hittest

    var   result = ""
    for  btn  in btns{

        if btn.isSelected {
            result  +=  "\(btn.tag)"

              print(result)
        }
        btn.isSelected = false



    }


    if (result == self.pawd) {


        print("解锁成功!")


    }
    self.btns.removeAll()

    setNeedsDisplay()


}

// 重写draw方法
override func draw(_ rect: CGRect) {
    let  cout = self.btns.count
    let   path = UIBezierPath()
    for  i in 0..<cout {
        let  point  = self.btns[i].center
        if i == 0 {
            path.move(to: point)
        }
        else{
            path.addLine(to: point)
        }
    }
    UIColor.blue.setStroke()//画笔颜色
    path.lineWidth   =  3
    path.stroke()
}
  }

个人原创文章,请尊重原创,转载请注明出处:吴磊的简书:http://www.jianshu.com/p/a4c29ec5712f

修改版:http://he8090.cn/2017/06/05/swift实现手势解锁绘制/
近期增加了object-c的具体应用,包括手势绘制,验证,更新,以及处理了一些手势绘制过程中Bug。详情见object-c实现。

H5 手势解锁

扫码在线查看:

金沙国际官网 5

或者点击查看手机版。

项目 GitHub
地址,H5HandLock。

首先,我要说明一下,对于这个项目,我是参考别人的,H5lock。

我觉得一个比较合理的解法应该是利用 canvas 来实现,不知道有没有大神用 css
来实现。如果纯用 css 的话,可以将连线先设置
display: none,当手指划过的时候,显示出来。光设置这些应该就非常麻烦吧。

之前了解过 canvas,但没有真正的写过,下面就来介绍我这几天学习 canvas
并实现 H5 手势解锁的过程。

前言

  • 最近自己写的小项目中需要用到手势解锁,但是使用了多个开源的轮子效果都不理想,
    想要京东金融的那种样式,于是,谁让我是程序员呢?没有什么就创造什么。
  • 手势解锁其实是一个比较轻量级的东西,并不需要很多附加的、酷炫的功能,所以自定义一个
    适合自己项目的是最好不过了。
  • 本文出自安卓新手,旨在引导初入安卓坑的同学们做出一个简单的自定义View,大佬可以直接跳过,
    另,有不足之处望读者指出。
  • 一、swift实现

准备及布局设置

我这里用了一个比较常规的做法:

(function(w){ var handLock = function(option){} handLock.prototype = {
init : function(){}, … } w.handLock = handLock; })(window) // 使用 new
handLock({ el: document.getElementById(‘id’), … }).init();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(function(w){
  var handLock = function(option){}
 
  handLock.prototype = {
    init : function(){},
    …
  }
 
  w.handLock = handLock;
})(window)
 
// 使用
new handLock({
  el: document.getElementById(‘id’),
  …
}).init();

常规方法,比较易懂和操作,弊端就是,可以被随意的修改。

传入的参数中要包含一个 dom 对象,会在这个 dom 对象內创建一个
canvas。当然还有一些其他的 dom 参数,比如 message,info 等。

关于 css 的话,懒得去新建文件了,就直接內联了。

思路

其实实现这个自定义的控件有很多思路,首先想到的是,要在View中创建9个圆,那么继承GridLayout再合适不过了,但是经过尝试,放弃了,逻辑变的更加复杂了,所以我选择直接继承View,那么下面就是需要处理的逻辑:
1 . 根据控件的大小,绘制9个圆圈;
2 . 在手指按到圆圈时,开始绘制路线,并且将按下的圆圈置为选中状态;
3 . 在手指滑动时,绘制一根跟随手指移动的、起点为按下的圆圈的线;
4
.当手指滑动到另外一个圆圈时,将第一个按下的圆圈与当前圆圈用线连起来,并且绘制一根以当前圆圈为起点的跟随手指移动的线;
5
.手指按下到圆圈时,以及每次划过圆圈时,将此圆圈对应的数字添加到数组;
6
.当手指抬起时,根据添加的数字判断密码是否正确,若错误,则将所有的线、选中的圆,都置为错误的状态、颜色;

使用swift实现iOS手势锁屏,虽然在iOS客户端很少使用到滑动手势,但是有时候为了和安卓应用保持用户交互的一致性,所以有的时候还是很有必要的。
iOS客户端解锁建议使用touch ID。

canvas

实现

  • 首先看一下效果图:
![](https://upload-images.jianshu.io/upload_images/6942228-94c2eee393fd5127.gif)

GestureUnlock.gif
  • Demo中实现的功能是:第一次为设置手势密码,第二次验证手势密码是否与第一次相同,运用的场景为设置手势密码和解锁;
  • 废话不说,下面上代码,由于没有添加xml中自定义属性的功能,所以所有代码就一个UnlockView类,各类自定义的属性通过set方法来设置,另外需要一个Circle的内部类,作为圆圈的对象,包含圆圈的XY坐标以及状态(选中、未选中、出错);
  • 首先是OnMeasure方法,由于OnMeasure会在最开始被调用,所以在这里获取控件的宽高再合适不过:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        width = getMeasuredWidth();
        height = getMeasuredHeight();
    }
  • 然后是OnLayout方法,在此方法中,绘制9个圆圈,以及初始化一些画笔、颜色等参数,注意,此方法可能会多次调用,所以要考虑到多次调用的情况,如初始化9个圆圈。:

@Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        //init all path/paint
        mPath = new Path();
        tempPath = new Path();
        pathPaint = new Paint();
        pathPaint.setColor(selectColor);
        pathPaint.setDither(true);
        pathPaint.setAntiAlias(true);
        pathPaint.setStyle(Paint.Style.STROKE);
        pathPaint.setStrokeCap(Paint.Cap.ROUND);
        pathPaint.setStrokeJoin(Paint.Join.ROUND);
        pathPaint.setStrokeWidth(pathWidth);
        //普通状态小圆画笔
        circletBmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        mCanvas = new Canvas(circletBmp);
        cirNorPaint = new Paint();
        cirNorPaint.setAntiAlias(true);
        cirNorPaint.setDither(true);
        cirNorPaint.setColor(normalColor);
        //选中状态大圆画笔
        cirSelPaint = new Paint();
        cirSelPaint.setAntiAlias(true);
        cirSelPaint.setDither(true);
        cirSelPaint.setStyle(Paint.Style.STROKE);
        cirSelPaint.setStrokeWidth(strokeWidth);
        cirSelPaint.setColor(selectColor);
        //选中状态小圆画笔
        smallCirSelPaint = new Paint();
        smallCirSelPaint.setAntiAlias(true);
        smallCirSelPaint.setDither(true);
        smallCirSelPaint.setColor(selectColor);
        //出错状态大圆画笔
        cirErrPaint = new Paint();
        cirErrPaint.setAntiAlias(true);
        cirErrPaint.setDither(true);
        cirErrPaint.setStyle(Paint.Style.STROKE);
        cirErrPaint.setStrokeWidth(strokeWidth);
        cirErrPaint.setColor(errorColor);
        //出错状态小圆画笔
        smallcirErrPaint = new Paint();
        smallcirErrPaint.setAntiAlias(true);
        smallcirErrPaint.setDither(true);
        smallcirErrPaint.setColor(errorColor);

        //init all circles
        int hor = width / 6;
        int ver = height / 6;
        if(!hasNewCircles){
            for (int i = 0; i < 9; i++) {
                int tempX = (i % 3 + 1) * 2 * hor - hor;
                int tempY = (i / 3 + 1) * 2 * ver - ver;
                Circle circle = new Circle(i, tempX, tempY, CIRCLE_NORMAL);
                circleList.add(circle);
            }
        }
        hasNewCircles=true;
    }
  • 再然后是重中之中,OnTouchEvent方法,在此方法中,需要监听手指的操作,当手指按到屏幕中的某个圆圈时,开始整个流程,所以我们需要通过以下代码判断手指的落点是都在某个圆内:

    @Nullable
    private Circle getOuterCircle(int x, int y) {
        for (int i = 0; i < circleList.size(); i++) {
            Circle circle = circleList.get(i);
            if ((x - circle.getX()) * (x - circle.getX()) + (y - circle.getY()) * (y - circle.getY()) <= normalR * normalR) {
                if (circle.getState() != CIRCLE_SELECTED) {
                    return circle;
                }
            }
        }
        return null;
    }

在ACTION_DOWN中,若落点在圆内,则将此圆的状态置为选中状态,并且将可移动的连线的起点置为此圆的坐标;在ACTION_MOVE中,若手指滑动,但未滑动到任何其他圆内,则只绘制一根移动的线,若手指在移动时移动到其他任何圆内,则将这个圆与一开始的圆用线连接,并且将可移动的线的起点置为此圆的坐标,并将此圆对应的序号添加到List中,最后将所有选中的圆的状态置为选中并且重新绘制画布;在ACTION_UP中,需要判断所有经过的圆的序号跟预设密码是否一致,并且重置所有状态。每次操作后,都需要通过invalidate()通知重新onDraw下面上代码:

@Override
    public boolean onTouchEvent(MotionEvent event) {
        if (isShowError)
            return true;
        int curX = (int) event.getX();
        int curY = (int) event.getY();
        Circle circle = getOuterCircle(curX, curY);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                this.resetAll();
                if (circle != null) {
                    rootX = circle.getX();
                    rootY = circle.getY();
                    circle.setState(CIRCLE_SELECTED);
                    pathCircleList.add(circle);
                    tempPath.moveTo(rootX, rootY);
                    addItem(circle.getPosition() + 1);
                    isUnlocking = true;
                }
                break;
            case MotionEvent.ACTION_MOVE:
                if (isUnlocking) {
                    mPath.reset();
                    mPath.addPath(tempPath);
                    mPath.moveTo(rootX, rootY);
                    mPath.lineTo(curX, curY);
                    handleMove(circle);
                }
                break;
            case MotionEvent.ACTION_UP:
                isUnlocking = false;
                if (pathCircleList.size() > 0) {
                    mPath.reset();
                    mPath.addPath(tempPath);
                    StringBuilder sb = new StringBuilder();
                    for (Integer num : passList) {
                        sb.append(num);
                    }

                    if (this.mode == CREATE_MODE) {
                        if(createListener!=null){
                            createListener.onGestureCreated(sb.toString());
                        }else{
                            Log.e("UnLockView","Please set CreateGestureListener first!");
                        }
                    } else if(this.mode == CHECK_MODE){
                        if(listener!=null){
                            if (listener.isUnlockSuccess(sb.toString())) {
                                listener.onSuccess();
                            } else {
                                listener.onFailure();
                                for (Circle circle1 : pathCircleList) {
                                    circle1.setState(CIRCLE_ERROR);
                                }
                                pathPaint.setColor(errorColor);
                            }
                        }else{
                            Log.e("UnLockView","Please set OnUnlockListener first!");
                        }

                    }else{
                        try {
                            throw new Exception("Please set mode first!");
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                    isShowError = true;
                    handler.postDelayed(new Runnable() {
                        @Override
                        public void run() {
                            handler.sendEmptyMessage(0);
                        }
                    }, 1000);

                }
                break;
        }
        invalidate();
        return true;
    }
  • 最后是所有操作的提现:onDraw(),在此方法中,需要根据当前9个圆的状态绘制不同状态的圆,并且要根据记录的Path来绘制连线,代码如下:

    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawBitmap(circletBmp, 0, 0, null);
        for (int i = 0; i < circleList.size(); i++) {
            drawCircles(circleList.get(i));
        }
        canvas.drawPath(mPath, pathPaint);
    }

    /**
     * called in onDraw for drawing all circles
     *
     * @param circle
     */
    private void drawCircles(Circle circle) {
        switch (circle.getState()) {
            case CIRCLE_NORMAL:
                mCanvas.drawCircle(circle.getX(), circle.getY(), normalR, cirNorPaint);
                break;
            case CIRCLE_SELECTED:
                mCanvas.drawCircle(circle.getX(), circle.getY(), selectR, cirSelPaint);
                mCanvas.drawCircle(circle.getX(), circle.getY(), normalR, smallCirSelPaint);
                break;
            case CIRCLE_ERROR:
                mCanvas.drawCircle(circle.getX(), circle.getY(), selectR, cirErrPaint);
                mCanvas.drawCircle(circle.getX(), circle.getY(), normalR, smallcirErrPaint);
                break;
        }
    }
  • 至此,所有的绘制、记录的逻辑基本完成,最后就是添加监听。由于此控件需要同时用于创建手势、验证手势,所以需要根据模式,添加不同的监听,首先添加的是创建手势的监听,在ACTION_UP的时候,会将记录的密码回调给接口:

    interface CreateGestureListener {
        void onGestureCreated(String result);
    }

    public void setGestureListener(CreateGestureListener listener) {
        this.createListener = listener;
    }

然后是添加验证手势的接口:

    interface OnUnlockListener {
        boolean isUnlockSuccess(String result);

        void onSuccess();

        void onFailure();
    }

    public void setOnUnlockListener(OnUnlockListener listener) {
        this.listener = listener;
    }

好啦,到这里自定义手势控制的View的逻辑基本完成,下面是将此控件用到布局后,在Activity中操作的一个示例:

@Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_main);
       mUnlockView= (UnlockView) findViewById(R.id.unlock);
       mUnlockView.setMode(UnlockView.CREATE_MODE);
       mUnlockView.setGestureListener(new UnlockView.CreateGestureListener() {
           @Override
           public void onGestureCreated(String result) {
               pwd=result;
               Toast.makeText(MainActivity.this,"Set Gesture Succeeded!",Toast.LENGTH_SHORT).show();
               mUnlockView.setMode(UnlockView.CHECK_MODE);
           }
       });
       mUnlockView.setOnUnlockListener(new UnlockView.OnUnlockListener() {
           @Override
           public boolean isUnlockSuccess(String result) {
               if(result.equals(pwd)){
                   return true;
               }else{
                   return false;
               }
           }

           @Override
           public void onSuccess() {
               Toast.makeText(MainActivity.this,"Check Succeeded!",Toast.LENGTH_SHORT).show();
           }

           @Override
           public void onFailure() {
               Toast.makeText(MainActivity.this,"Check Failed!",Toast.LENGTH_SHORT).show();
           }
       });

   }

到这里就完成了整个自定义View的编写、使用。完整代码及Demo可以在我的Github中查看。如发现有改进之处,望指正,万分感谢!

个人原创文章,请尊重原创,转载请注明出处:吴磊的简书:http://www.jianshu.com/p/a4c29ec5712f

欢迎联系我、提供工作机会:
Github:https://github.com/MondeoWu/GestureUnlock
E-mail:331948214@qq.com
QQ:331948214

swift与object-c的CGContextRef不一样,在swift中统一使用CGContext进行管理和使用。

1. 学习 canvas 并搞定画圆

MDN
上面有个简易的教程,大致浏览了一下,感觉还行。Canvas教程。

先创建一个 canvas,然后设置其大小,并通过 getContext
方法获得绘画的上下文:

var canvas = document.createElement(‘canvas’); canvas.width =
canvas.height = width; this.el.appendChild(canvas); this.ctx =
canvas.getContext(‘2d’);

1
2
3
4
5
var canvas = document.createElement(‘canvas’);
canvas.width = canvas.height = width;
this.el.appendChild(canvas);
 
this.ctx = canvas.getContext(‘2d’);

然后呢,先画 n*n 个圆出来:

JavaScript

createCircles: function(){ var ctx = this.ctx, drawCircle =
this.drawCircle, n = this.n; this.r = ctx.canvas.width / (2 + 4 * n) //
这里是参考的,感觉这种画圆的方式挺合理的,方方圆圆 r = this.r;
this.circles = []; // 用来存储圆心的位置 for(var i = 0; i < n;
i++){ for(var j = 0; j < n; j++){ var p = { x: j * 4 * r + 3 * r,
y: i * 4 * r + 3 * r, id: i * 3 + j } this.circles.push(p); } }
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); //
为了防止重复画 this.circles.forEach(function(v){ drawCircle(ctx, v.x,
v.y); // 画每个圆 }) }, drawCircle: function(ctx, x, y){ // 画圆函数
ctx.strokeStyle = ‘#FFFFFF’; ctx.lineWidth = 2; ctx.beginPath();
ctx.arc(x, y, this.r, 0, Math.PI * 2, true); ctx.closePath();
ctx.stroke(); }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
createCircles: function(){
  var ctx = this.ctx,
    drawCircle = this.drawCircle,
    n = this.n;
  this.r = ctx.canvas.width / (2 + 4 * n) // 这里是参考的,感觉这种画圆的方式挺合理的,方方圆圆
  r = this.r;
  this.circles = []; // 用来存储圆心的位置
  for(var i = 0; i < n; i++){
    for(var j = 0; j < n; j++){
      var p = {
        x: j * 4 * r + 3 * r,
        y: i * 4 * r + 3 * r,
        id: i * 3 + j
      }
      this.circles.push(p);
    }
  }
  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); // 为了防止重复画
  this.circles.forEach(function(v){
    drawCircle(ctx, v.x, v.y); // 画每个圆
  })
},
 
drawCircle: function(ctx, x, y){ // 画圆函数
  ctx.strokeStyle = ‘#FFFFFF’;
  ctx.lineWidth = 2;
  ctx.beginPath();
  ctx.arc(x, y, this.r, 0, Math.PI * 2, true);
  ctx.closePath();
  ctx.stroke();
}

画圆函数,需要注意:如何确定圆的半径和每个圆的圆心坐标(这个我是参考的),如果以圆心为中点,每个圆上下左右各扩展一个半径的距离,同时为了防止四边太挤,四周在填充一个半径的距离。那么得到的半径就是
width / ( 4 * n + 2),对应也可以算出每个圆所在的圆心坐标,也有一套公式,GET

本示例采用9*button进行绘制,关闭button交互事件,通过touchesBegan系列方法对滑动路径进行跟踪和绘制(imageView)。

2. 画线

画线需要借助 touch event 来完成,也就是,当我们 touchstart
的时候,传入开始时的相对坐标,作为线的一端,当我们 touchmove
的时候,获得坐标,作为线的另一端,当我们 touchend 的时候,开始画线。

这只是一个测试画线功能,具体的后面再进行修改。

有两个函数,获得当前 touch 的相对坐标:

getTouchPos: function(e){ // 获得触摸点的相对位置 var rect =
e.target.getBoundingClientRect(); var p = { // 相对坐标 x:
e.touches[0].clientX – rect.left, y: e.touches[0].clientY – rect.top
}; return p; }

1
2
3
4
5
6
7
8
getTouchPos: function(e){ // 获得触摸点的相对位置
  var rect = e.target.getBoundingClientRect();
  var p = { // 相对坐标
    x: e.touches[0].clientX – rect.left,
    y: e.touches[0].clientY – rect.top
  };
  return p;
}

画线:

drawLine: function(p1, p2){ // 画线 this.ctx.beginPath();
this.ctx.lineWidth = 3; this.ctx.moveTo(p1.x, p2.y);
this.ctx.lineTo(p.x, p.y); this.ctx.stroke(); this.ctx.closePath(); },

1
2
3
4
5
6
7
8
drawLine: function(p1, p2){ // 画线
  this.ctx.beginPath();
  this.ctx.lineWidth = 3;
  this.ctx.moveTo(p1.x, p2.y);
  this.ctx.lineTo(p.x, p.y);
  this.ctx.stroke();
  this.ctx.closePath();
},

然后就是监听 canvas 的 touchstarttouchmove、和 touchend 事件了。

以下为全部代码:

3. 画折线

所谓的画折线,就是,将已经触摸到的点连起来,可以把它看作是画折线。

首先,要用两个数组,一个数组用于已经 touch 过的点,另一个数组用于存储未
touch 的点,然后在 move 监听时候,对 touch
的相对位置进行判断,如果触到点,就把该点从未 touch 移到 touch
中,然后,画折线,思路也很简单。

JavaScript

drawLine: function(p){ // 画折线 this.ctx.beginPath();
this.ctx.lineWidth = 3; this.ctx.moveTo(this.touchCircles[0].x,
this.touchCircles[0].y); for (var i = 1 ; i <
this.touchCircles.length ; i++) {
this.ctx.lineTo(this.touchCircles[i].x, this.touchCircles[i].y); }
this.ctx.lineTo(p.x, p.y); this.ctx.stroke(); this.ctx.closePath(); },

1
2
3
4
5
6
7
8
9
10
11
drawLine: function(p){ // 画折线
  this.ctx.beginPath();
  this.ctx.lineWidth = 3;
  this.ctx.moveTo(this.touchCircles[0].x, this.touchCircles[0].y);
  for (var i = 1 ; i < this.touchCircles.length ; i++) {
    this.ctx.lineTo(this.touchCircles[i].x, this.touchCircles[i].y);
  }
  this.ctx.lineTo(p.x, p.y);
  this.ctx.stroke();
  this.ctx.closePath();
},

JavaScript

judgePos: function(p){ // 判断 触点 是否在 circle 內 for(var i = 0; i
< this.restCircles.length; i++){ temp = this.restCircles[i];
if(Math.abs(p.x – temp.x) < r && Math.abs(p.y – temp.y) < r){
this.touchCircles.push(temp); this.restCircles.splice(i, 1);
this.touchFlag = true; break; } } }

1
2
3
4
5
6
7
8
9
10
11
judgePos: function(p){ // 判断 触点 是否在 circle 內
  for(var i = 0; i < this.restCircles.length; i++){
    temp = this.restCircles[i];
    if(Math.abs(p.x – temp.x) < r && Math.abs(p.y – temp.y) < r){
      this.touchCircles.push(temp);
      this.restCircles.splice(i, 1);
      this.touchFlag = true;
      break;
    }
  }
}
import UIKit

  class TouchPasswordViewController: UIViewController {

    let screenWidth : CGFloat = UIScreen.main.bounds.size.width;
    let screenHeight : CGFloat = UIScreen.main.bounds.size.height;

    var btnArray : NSMutableArray = NSMutableArray.init(); //btn数组
    var selectToArray : NSMutableArray = NSMutableArray.init();//选中的btn数组
    var startPoint : CGPoint = CGPoint.init();//起点
    var endPoint : CGPoint = CGPoint.init();//终点
    var imageView : UIImageView = UIImageView.init();//绘制的背景



    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.backgroundColor = UIColor.white;

        imageView = UIImageView.init(frame: CGRect.init(x: 0, y: 0, width: screenWidth, height: screenHeight));
        self.view.addSubview(imageView);

        for i in 0...2 {
            for j in 0...2 {
                let btn : UIButton = UIButton.init(type: UIButtonType.custom);
                btn.frame = CGRect.init(x: screenWidth / 12 + screenWidth / 3 * CGFloat(j), y: screenHeight / 3 + screenWidth / 3 * CGFloat(i), width: screenWidth / 6, height: screenWidth / 6);

                btn.setImage(UIImage.init(named: "尼罗河-4"), for: UIControlState.normal);
                btn.setImage(UIImage.init(named: "尼罗河"), for: UIControlState.selected);
                btn.isUserInteractionEnabled = false;//取消button的交互事件,否则touch会被截断。
                btnArray.add(btn);
                imageView.addSubview(btn);


            }
        }

    }


    func drawLine() -> (UIImage){
        var img : UIImage = UIImage.init();
        let color : UIColor = UIColor.init(red: 1, green: 0, blue: 0, alpha: 1);

        //绘制线条路径
        UIGraphicsBeginImageContext(imageView.frame.size);
        let context = UIGraphicsGetCurrentContext();
        context?.setLineWidth(5);
        context?.setStrokeColor(color.cgColor);
        context?.move(to: startPoint);

        for b in selectToArray {
            let btn : UIButton = b as! UIButton;
            let btnPit = btn.center;

            context?.addLine(to: btnPit);
            context?.move(to: btnPit);
        }

        context?.addLine(to: endPoint);
        context?.strokePath();

        img = UIGraphicsGetImageFromCurrentImageContext()!;
        UIGraphicsEndImageContext();

        return img;
    }


    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        let touch : UITouch = touches.first!;
        for b in btnArray {
            let btn : UIButton = b as! UIButton;
            let btnPit : CGPoint = touch.location(in: btn);
            if btn.point(inside: btnPit, with: nil) {//判断当前touch是否btn范围内,在则存入selectArray 并改变button状态
                selectToArray.add(btn);
                btn.isHighlighted = true;
                startPoint = btn.center;
            }
        }
    }


    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        let touch : UITouch = touches.first!;
        endPoint = touch.location(in: imageView);

        for b in btnArray {
            let btn : UIButton = b as! UIButton;
            let po : CGPoint = touch.location(in: btn);

            if btn.point(inside: po, with: nil) {
                var isAdd : Bool = true;

                for b in selectToArray {
                    let selectBtn : UIButton = b as! UIButton;
                    if selectBtn == btn {
                        isAdd = false;
                        break;
                    }
                }

                if isAdd {
                    selectToArray.add(btn);
                    btn.isHighlighted = true;
                }
            }

        }
        imageView.image = self.drawLine();
    }

    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        imageView.image = nil;
        selectToArray.removeAllObjects();

        for b in btnArray {
            let btn : UIButton = b as! UIButton;
            btn.isHighlighted = false;

        }
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
    }

4. 标记已画

前面已经说了,我们把已经 touch
的点(圆)放到数组中,这个时候需要将这些已经 touch
的点给标记一下,在圆心处画一个小实心圆:

JavaScript

drawPoints: function(){ for (var i = 0 ; i < this.touchCircles.length
; i++) { this.ctx.fillStyle = ‘#FFFFFF’; this.ctx.beginPath();
this.ctx.arc(this.touchCircles[i].x, this.touchCircles[i].y, this.r
/ 2, 0, Math.PI * 2, true); this.ctx.closePath(); this.ctx.fill(); } }

1
2
3
4
5
6
7
8
9
drawPoints: function(){
  for (var i = 0 ; i < this.touchCircles.length ; i++) {
    this.ctx.fillStyle = ‘#FFFFFF’;
    this.ctx.beginPath();
    this.ctx.arc(this.touchCircles[i].x, this.touchCircles[i].y, this.r / 2, 0, Math.PI * 2, true);
    this.ctx.closePath();
    this.ctx.fill();
  }
}

同时添加一个 reset 函数,当 touchend 的时候调用,400ms 调用 reset 重置
canvas。

到现在为止,一个 H5 手势解锁的简易版已经基本完成。

  • 二、object-c实现

password

为了要实现记住和重置密码的功能,把 password 保存在 localStorage
中,但首先要添加必要的 html 和样式。

object-c实现与swift逻辑一致,只是加入在实际使用过程中遇到的问题,使其更有参考性和实际应用价值。
注:代码中有部分资源是中英文适配使用的。如:NSLocalizedString

1. 添加 message 和 单选框

为了尽可能的使界面简洁(越丑越好),直接在 body 后面添加了:

XHTML

<div id=”select”> <div
class=”message”>请输入手势密码</div> <div class=”radio”>
<label><input type=”radio”
name=”pass”>设置手势密码</label> <label><input
type=”radio” name=”pass”>验证手势密码</label> </div>
</div>

1
2
3
4
5
6
7
<div id="select">
  <div class="message">请输入手势密码</div>
  <div class="radio">
    <label><input type="radio" name="pass">设置手势密码</label>
    <label><input type="radio" name="pass">验证手势密码</label>
  </div>
</div>

将添加到 dom 已 option 的形式传给 handLock:

var el = document.getElementById(‘handlock’), info =
el.getElementsByClassName(‘info’)[0], select =
document.getElementById(‘select’), message =
select.getElementsByClassName(‘message’)[0], radio =
select.getElementsByClassName(‘radio’)[0], setPass =
radio.children[0].children[0], checkPass =
radio.children[1].children[0]; new handLock({ el: el, info: info,
message: message, setPass: setPass, checkPass: checkPass, n: 3
}).init();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var el = document.getElementById(‘handlock’),
  info = el.getElementsByClassName(‘info’)[0],
  select = document.getElementById(‘select’),
  message = select.getElementsByClassName(‘message’)[0],
  radio = select.getElementsByClassName(‘radio’)[0],
  setPass = radio.children[0].children[0],
  checkPass = radio.children[1].children[0];
new handLock({
  el: el,
  info: info,
  message: message,
  setPass: setPass,
  checkPass: checkPass,
  n: 3
}).init();

在object-c中考虑到屏幕适配会导致button图片拉伸,所以使用UIView代替UIButton。自定义了一个UIView.
1、RoundRectView.h

2. info 信息显示

关于 info 信息显示,自己写了一个悬浮窗,然后默认为
display: none,然后写了一个 showInfo
函数用来显示提示信息,直接调用:

showInfo: function(message, timer){ // 专门用来显示 info var info =
this.dom.info; info.innerHTML = message; info.style.display = ‘block’;
setTimeout(function(){ info.style.display = ”; }, 1000) }

1
2
3
4
5
6
7
8
showInfo: function(message, timer){ // 专门用来显示 info
  var info = this.dom.info;
  info.innerHTML = message;
  info.style.display = ‘block’;
  setTimeout(function(){
    info.style.display = ”;
  }, 1000)
}

关于 info 的样式,在 html 中呢。

#import <UIKit/UIKit.h>

@interface RoundRectView : UIView
@property (nonatomic, strong) UIView *selectView;

@end

3. 关于密码

先不考虑从 localStorage 读取到情况,新加一个 lsPass
对象,专门用于存储密码,由于密码情况比较多,比如设置密码,二次确认密码,验证密码,为了方便管理,暂时设置了密码的三种模式,分别是:

model:1 验证密码模式

model:2 设置密码模式

model:3 设置密码二次验证

具体看下面这个图:

金沙国际官网 6

这三种 model ,只要处理好它们之间如何跳转就 ok 了,即状态的改变。

所以就有了 initPass:

initPass: function(){ // 将密码初始化 this.lsPass =
w.localStorage.getItem(‘HandLockPass’) ? { model: 1, pass:
w.localStorage.getItem(‘HandLockPass’).split(‘-‘) } : { model: 2 };
this.updateMessage(); }, updateMessage: function(){ //
根据当前模式,更新 dom if(this.lsPass.model == 2){
this.dom.setPass.checked = true; this.dom.message.innerHTML =
‘请设置手势密码’; }else if(this.lsPass.model == 1){
this.dom.checkPass.checked = true; this.dom.message.innerHTML =
‘请验证手势密码’; }else if(this.lsPass.model = 3){
this.dom.setPass.checked = true; this.dom.message.innerHTML =
‘请再次输入密码’; } },

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
initPass: function(){ // 将密码初始化
  this.lsPass = w.localStorage.getItem(‘HandLockPass’) ? {
    model: 1,
    pass: w.localStorage.getItem(‘HandLockPass’).split(‘-‘)
  } : { model: 2 };
  this.updateMessage();
},
 
updateMessage: function(){ // 根据当前模式,更新 dom
  if(this.lsPass.model == 2){
    this.dom.setPass.checked = true;
    this.dom.message.innerHTML = ‘请设置手势密码’;
  }else if(this.lsPass.model == 1){
    this.dom.checkPass.checked = true;
    this.dom.message.innerHTML = ‘请验证手势密码’;
  }else if(this.lsPass.model = 3){
    this.dom.setPass.checked = true;
    this.dom.message.innerHTML = ‘请再次输入密码’;
  }
},

有必要再来介绍一下 lsPass 的格式:

this.lsPass = { model:1, // 表示当前的模式 pass: [0, 1, 2, 4, 5] //
表示当前的密码,可能不存在 }

1
2
3
4
this.lsPass = {
  model:1, // 表示当前的模式
  pass: [0, 1, 2, 4, 5] // 表示当前的密码,可能不存在
}

因为之前已经有了一个基本的实现框架,现在只需要在 touchend
之后,写一个函数,功能就是先对当前的 model
进行判断,实现对应的功能,这里要用到 touchCircles 数组,表示密码的顺序:

JavaScript

checkPass: function(){ var succ, model = this.lsPass.model; //succ
以后会用到 if(model == 2){ // 设置密码 if(this.touchCircles.length <
5){ // 验证密码长度 succ = false; this.showInfo(‘密码长度至少为 5!’,
1000); }else{ succ = true; this.lsPass.temp = []; //
将密码放到临时区存储 for(var i = 0; i < this.touchCircles.length;
i++){ this.lsPass.temp.push(this.touchCircles[i].id); }
this.lsPass.model = 3; this.showInfo(‘请再次输入密码’, 1000);
this.updateMessage(); } }else if(model == 3){// 确认密码 var flag =
true; // 先要验证密码是否正确 if(this.touchCircles.length ==
this.lsPass.temp.length){ var tc = this.touchCircles, lt =
this.lsPass.temp; for(var i = 0; i < tc.length; i++){ if(tc[i].id
!= lt[i]){ flag = false; } } }else{ flag = false; } if(!flag){ succ =
false; this.showInfo(‘两次密码不一致,请重新输入’, 1000);
this.lsPass.model = 2; // 由于密码不正确,重新回到 model 2
this.updateMessage(); }else{ succ = true; // 密码正确,localStorage
存储,并设置状态为 model 1 w.localStorage.setItem(‘HandLockPass’,
this.lsPass.temp.join(‘-‘)); // 存储字符串 this.lsPass.model = 1;
this.lsPass.pass = this.lsPass.temp; this.updateMessage(); } delete
this.lsPass.temp; // 很重要,一定要删掉,bug }else if(model == 1){ //
验证密码 var tc = this.touchCircles, lp = this.lsPass.pass, flag = true;
if(tc.length == lp.length){ for(var i = 0; i < tc.length; i++){
if(tc[i].id != lp[i]){ flag = false; } } }else{ flag = false; }
if(!flag){ succ = false; this.showInfo(‘很遗憾,密码错误’, 1000); }else{
succ = true; this.showInfo(‘恭喜你,验证通过’, 1000); } } },

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
checkPass: function(){
  var succ, model = this.lsPass.model; //succ 以后会用到
  if(model == 2){ // 设置密码
    if(this.touchCircles.length < 5){ // 验证密码长度
      succ = false;
      this.showInfo(‘密码长度至少为 5!’, 1000);
    }else{
      succ = true;
      this.lsPass.temp = []; // 将密码放到临时区存储
      for(var i = 0; i < this.touchCircles.length; i++){
        this.lsPass.temp.push(this.touchCircles[i].id);
      }
      this.lsPass.model = 3;
      this.showInfo(‘请再次输入密码’, 1000);
      this.updateMessage();
    }
  }else if(model == 3){// 确认密码
    var flag = true;
    // 先要验证密码是否正确
    if(this.touchCircles.length == this.lsPass.temp.length){
      var tc = this.touchCircles, lt = this.lsPass.temp;
      for(var i = 0; i < tc.length; i++){
        if(tc[i].id != lt[i]){
          flag = false;
        }
      }
    }else{
      flag = false;
    }
    if(!flag){
      succ = false;
      this.showInfo(‘两次密码不一致,请重新输入’, 1000);
      this.lsPass.model = 2; // 由于密码不正确,重新回到 model 2
      this.updateMessage();
    }else{
      succ = true; // 密码正确,localStorage 存储,并设置状态为 model 1
      w.localStorage.setItem(‘HandLockPass’, this.lsPass.temp.join(‘-‘)); // 存储字符串
      this.lsPass.model = 1;
      this.lsPass.pass = this.lsPass.temp;
      this.updateMessage();
    }
    delete this.lsPass.temp; // 很重要,一定要删掉,bug
  }else if(model == 1){ // 验证密码
    var tc = this.touchCircles, lp = this.lsPass.pass, flag = true;
    if(tc.length == lp.length){
      for(var i = 0; i < tc.length; i++){
        if(tc[i].id != lp[i]){
          flag = false;
        }
      }
    }else{
      flag = false;
    }
    if(!flag){
      succ = false;
      this.showInfo(‘很遗憾,密码错误’, 1000);
    }else{
      succ = true;
      this.showInfo(‘恭喜你,验证通过’, 1000);
    }
  }
},

密码的设置要参考前面那张图,要时刻警惕状态的改变。

2、RoundRectView.m

4. 手动重置密码

思路也很简单,就是添加点击事件,点击之后,改变 model
即可,点击事件如下:

this.dom.setPass.addEventListener(‘click’, function(e){
self.lsPass.model = 2; // 改变 model 为设置密码 self.updateMessage(); //
更新 message self.showInfo(‘请设置密码’, 1000); })
this.dom.checkPass.addEventListener(‘click’, function(e){
if(self.lsPass.pass){ self.lsPass.model = 1; self.updateMessage();
self.showInfo(‘请验证密码’, 1000) }else{ self.showInfo(‘请先设置密码’,
1000); self.updateMessage(); } })

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
this.dom.setPass.addEventListener(‘click’, function(e){
  self.lsPass.model = 2; // 改变 model 为设置密码
  self.updateMessage(); // 更新 message
  self.showInfo(‘请设置密码’, 1000);
})
this.dom.checkPass.addEventListener(‘click’, function(e){
  if(self.lsPass.pass){
    self.lsPass.model = 1;
    self.updateMessage();
    self.showInfo(‘请验证密码’, 1000)
  }else{
    self.showInfo(‘请先设置密码’, 1000);
    self.updateMessage();
  }
})

ps:这里面还有几个小的 bug,因为 model 只有 3
个,所以设置的时候,当点击重置密码的时候,没有设置密码成功,又切成验证密码状态,此时无法提升沿用旧密码,原因是
model 只有三个

#import "RoundRectView.h"
#import "SysConfig.h"

@interface RoundRectView ()
{
    CGFloat _width;
}

@end

@implementation RoundRectView

- (instancetype)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        _width = frame.size.width / 4.0;
        [self initUI];
    }
    return self;
}

- (void)initUI{
    self.backgroundColor = [UIColor whiteColor];
    self.layer.masksToBounds = YES;
    self.layer.cornerRadius = self.frame.size.width/ 2.0;
    self.layer.borderWidth = 2.0;
    self.layer.borderColor = [UIColor hexString:@"#3fb8c8"].CGColor;

    self.selectView = [[UIView alloc] initWithFrame:CGRectMake((self.frame.size.width - _width) / 2, (self.frame.size.width - _width) / 2, _width, _width)];
    self.selectView.backgroundColor = [UIColor hexString:@"#3fb8c8"];
    self.selectView.layer.masksToBounds = YES;
    self.selectView.layer.cornerRadius = _width / 2.0;
    self.selectView.hidden = YES;
    [self addSubview:self.selectView];  
}
@end

5. 添加 touchend 颜色变化

实现这个基本上就大功告成了,这个功能最主要的是给用户一个提醒,若用户划出的密码符合规范,显示绿色,若不符合规范或错误,显示红色警告。

因为之前已经设置了一个 succ 变量,专门用于重绘。

JavaScript

drawEndCircles: function(color){ // end 时重绘已经 touch 的圆 for(var i
= 0; i < this.touchCircles.length; i++){
this.drawCircle(this.touchCircles[i].x, this.touchCircles[i].y,
color); } }, // 调用 if(succ){ this.drawEndCircles(‘#2CFF26’); // 绿色
}else{ this.drawEndCircles(‘red’); // 红色 }

1
2
3
4
5
6
7
8
9
10
11
12
drawEndCircles: function(color){ // end 时重绘已经 touch 的圆
  for(var i = 0; i < this.touchCircles.length; i++){
    this.drawCircle(this.touchCircles[i].x, this.touchCircles[i].y, color);
  }
},
 
// 调用
if(succ){
  this.drawEndCircles(‘#2CFF26’); // 绿色
}else{
  this.drawEndCircles(‘red’); // 红色
}

那么,一个可以演示的版本就生成了,尽管还存在一些
bug,随后会来解决。(详情分支 password)

3、TouchUpViewController.h

一些 bugs

有些 bugs 在做的时候就发现了,一些 bug
后来用手机测试的时候才发现,比如,我用 chrome 的时候,没有察觉这个
bug,当我用 android 手机 chrome 浏览器测试的时候,发现当我 touchmove
向下的时候,会触发浏览器的下拉刷新,解决办法:加了一个
preventDefault,没想到居然成功了。

this.canvas.addEventListener(‘touchmove’, function(e){ e.preventDefault
? e.preventDefault() : null; var p = self.getTouchPos(e);
if(self.touchFlag){ self.update(p); }else{ self.judgePos(p); } }, false)

1
2
3
4
5
6
7
8
9
this.canvas.addEventListener(‘touchmove’, function(e){
  e.preventDefault ? e.preventDefault() : null;
  var p = self.getTouchPos(e);
  if(self.touchFlag){
    self.update(p);
  }else{
    self.judgePos(p);
  }
}, false)
#import <UIKit/UIKit.h>
#import "LXBaseViewController.h"

typedef enum : NSUInteger {
    TouchUnlockCreatePwd,//绘制
    TouchUnlockValidatePwd,//验证
    TouchUnlockUpdatePwd,//修改手势
} UnlockType;

@interface TouchUpViewController : LXBaseViewController

- (instancetype)initWithUnlockType:(UnlockType)type;

@end

关于 showInfo

由于showInfo 中有 setTimeout 函数,可以看到函数里的演出为
1s,导致如果我们操作的速度比较快,在 1s 内连续 show 了很多个
info,后面的 info 会被第一个 info 的 setTimeout 弄乱,显示的时间小于
1s,或更短。比如,当重复点击设置手势密码和验证手势密码,会产生这个 bug。

解决办法有两个,一个是增加一个专门用于显示的数组,每次从数组中取值然后显示。另一种解题思路和防抖动的思路很像,就是当有一个新的
show 到来时,把之前的那个 setTimeout 清除掉。

这里采用第二种思路:

showInfo: function(message, timer){ // 专门用来显示 info
clearTimeout(this.showInfo.timer); var info = this.dom.info;
info.innerHTML = message; info.style.display = ‘block’;
this.showInfo.timer = setTimeout(function(){ info.style.display = ”; },
timer || 1000) },

1
2
3
4
5
6
7
8
9
showInfo: function(message, timer){ // 专门用来显示 info
  clearTimeout(this.showInfo.timer);
  var info = this.dom.info;
  info.innerHTML = message;
  info.style.display = ‘block’;
  this.showInfo.timer = setTimeout(function(){
    info.style.display = ”;
  }, timer || 1000)
},

4、TouchUpViewController.m

解决小尾巴

所谓的小尾巴,如下:

金沙国际官网 7

解决办法也很简单,在 touchend 的时候,先进行 clearRect 就 ok 了。

#import "TouchUpViewController.h"
#import "RoundRectView.h"
#import "SysConfig.h"
#import "AppDelegate.h"
#import "LXNetworkRequest.h"
#import "LXNetworkRequest+LogWithEmail.h"

#define APPDELEGATEREAL (AppDelegate *)[UIApplication sharedApplication].delegate
#define TouchUnlockPwdKey  @"touchUnlockPwdKey"

@interface TouchUpViewController ()
{
    NSMutableArray *_nomarlArray,*_selectArray;
    CGFloat _width,_height;
    UIImageView *_imageView;
    CGPoint _startPoint,_endPoint;
    NSMutableString *_password;
    NSString *_oldPassword;
    UnlockType _unlockType;
    NSInteger _errorCount;

    UIImageView *_headerImageView;
    UILabel *_nickNameLabel,*_tipLabel;
    UIButton *_loginBtn;

    BOOL isUpdatePwd;

    NSInteger _steps;//1 验证密码 2:创建第一遍   3:创建第二遍
}
@end

@implementation TouchUpViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.title = NSLocalizedString(@"Draw Pattern", nil);
    self.view.backgroundColor = [UIColor whiteColor];
    [self initUI];
}

- (void)comeBack:(id)sender
{
    [self.navigationController popViewControllerAnimated:YES];
}

- (instancetype)initWithUnlockType:(UnlockType)type{

    self = [super init];
    if (self) {
        _unlockType = type;
    }
    return self;
}

- (void)initUI{

    NSUserDefaults *def = [NSUserDefaults standardUserDefaults];
    NSString *savePwd = [def objectForKey:TouchUnlockPwdKey];

    _width = [UIScreen mainScreen].bounds.size.width;
    _height = [UIScreen mainScreen].bounds.size.height;

    _password = [NSMutableString string];
    _nomarlArray = [NSMutableArray array];
    _selectArray = [NSMutableArray array];

    _imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, _width, _height)];
    [self.view addSubview:_imageView];


    CGFloat leftSpace = fitScreenWidth(40);
    CGFloat wd = (_width - leftSpace * 2) /8.0;

    NSInteger index = 1;
    for (int i = 0; i < 3; i ++) {
        for (int j = 0; j < 3; j ++) {

            CGRect rect = CGRectMake(leftSpace + wd * 3 * j, _height / 3.0 + wd * 3 * i, wd * 2, wd * 2);
            RoundRectView *ve = [[RoundRectView alloc] initWithFrame:rect];

            ve.tag = index ++;

            [_nomarlArray addObject:ve];
            [_imageView addSubview:ve];
        }
    }


    //    -   -  -  - -  header - - - - - - -  - -
    _headerImageView = [[UIImageView alloc] initWithFrame:CGRectMake((_width - fitScreenWidth(50)) / 2, fitScreenWidth(60), fitScreenWidth(50), fitScreenWidth(50))];
    _headerImageView.backgroundColor = [UIColor whiteColor];
    [self.view addSubview:_headerImageView];

    _nickNameLabel = [[UILabel alloc] initWithFrame:CGRectMake((_width - fitScreenWidth(100)) / 2, CGRectGetMaxY(_headerImageView.frame), fitScreenWidth(100), fitScreenWidth(25))];
    _nickNameLabel.font = [UIFont systemFontOfSize:fitScreenWidth(12.0)];
    _nickNameLabel.hidden = YES;
    _nickNameLabel.textAlignment = NSTextAlignmentCenter;
    [self.view addSubview:_nickNameLabel];

    _tipLabel = [[UILabel alloc] initWithFrame:CGRectMake(20, CGRectGetMaxY(_nickNameLabel.frame), _width - 20 * 2, fitScreenWidth(25))];
    _tipLabel.font = [UIFont systemFontOfSize:fitScreenWidth(13.0)];
    _tipLabel.textAlignment = NSTextAlignmentCenter;
    _tipLabel.textColor = [UIColor lightGrayColor];
    [self.view addSubview:_tipLabel];


    _loginBtn = [[UIButton alloc] initWithFrame:CGRectMake((_width - fitScreenWidth(150)) / 2 , _height - fitScreenWidth(55), fitScreenWidth(150), 30)];
    _loginBtn.titleLabel.font = [UIFont systemFontOfSize:fitScreenWidth(13.0)];

    [_loginBtn setTitle:NSLocalizedString(@"Use login password", nil) forState:UIControlStateNormal];
    [_loginBtn setTitleColor:[UIColor hexString:@"#3fb8c8"] forState:UIControlStateNormal];
    [_loginBtn setHidden:YES];
    [_loginBtn addTarget:self action:@selector(loginWithPwd) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:_loginBtn];


    UIBarButtonItem *resetBtn = [[UIBarButtonItem alloc] initWithTitle:NSLocalizedString(@"Reset", nil) style:UIBarButtonItemStylePlain target:self action:@selector(resetBtnClick)];
    self.navigationItem.rightBarButtonItem = resetBtn;

    if (_unlockType == TouchUnlockCreatePwd) {
        //绘制
        _steps = 2;
        _tipLabel.text = NSLocalizedString(@"Draw Your Pattern", nil);
    }
    else if (_unlockType == TouchUnlockUpdatePwd){
        //更新密码不验证原密码 直接新增 !savePwd || savePwd.length == 0
        _unlockType = TouchUnlockCreatePwd;
        _tipLabel.text = NSLocalizedString(@"Draw new pattern", nil);
        _steps = 2;
    }
    else if (_unlockType == TouchUnlockValidatePwd){
        //验证密码
        _steps = 1;
        _errorCount = 5;
        [_loginBtn setHidden:NO];
        _tipLabel.text = NSLocalizedString(@"Draw Your Pattern", nil);
        _headerImageView.layer.masksToBounds = YES;
        _headerImageView.layer.cornerRadius = fitScreenWidth(50) / 2;
        [_headerImageView setNewImageUrl:APPDELEGATE.userModel.avatar placeHolder:[UIImage imageNamed:@"login_content_Avatar@2x"]];

        _nickNameLabel.hidden = NO;
        _nickNameLabel.text = APPDELEGATE.userModel.nickname;
    }

    if ([savePwd isEqualToString:@"TouchPwdError"]) {
        //密码失效,直接弹出提示框
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            [self alertPwdError];
        });
    }
}

- (void)changeUI:(UnlockType)type{
    //切换头部视图
}

- (void)changeTip:(NSInteger)index{
    //改变警告文字  1:最少四个点,请重新绘制  2:与上次绘制不一致,请重新绘制 3:密码错误还可以输入5次
    _tipLabel.textColor = [UIColor redColor];
    NSString *text = nil;
    switch (index) {
        case 1:
        {
            text = NSLocalizedString(@"At least 4 points required", nil);
        }
            break;

        case 2:
        {
            text = NSLocalizedString(@"Not match. Please draw it again", nil);

        }
            break;

        case 3:
        {
            text = _errorCount == 10000 ? [NSString stringWithFormat:NSLocalizedString(@"Invalid password. Please draw it again", nil)] : [NSLocalizedString(@"Invalid password.*** attempt(s) left", nil) stringByReplacingOccurrencesOfString:@"***" withString:[NSString stringWithFormat:@"%ld",_errorCount -- > 0 ? _errorCount : 0]];
        }
            break;


        default:
            break;
    }

    _tipLabel.text = text;

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

        if (_unlockType == TouchUnlockCreatePwd) {
            if (_steps == 2) {
                [self changeTipForNormal:1];
            }
            else if (_steps == 3){
                [self changeTipForNormal:3];
            }
            return;
        }
        [self changeTipForNormal:1];
    });
}

- (void)changeTipForNormal:(NSInteger)index{
    _tipLabel.textColor = [UIColor lightGrayColor];
    NSString *text = @"";
    switch (index) {
        case 1:
            text = NSLocalizedString(@"Draw Your Pattern", nil);
            break;
        case 2:
            text = @"请输入原手势密码";
            break;
        case 3:
            text = NSLocalizedString(@"Draw pattern again", nil);
            break;
        case 4:
            text = NSLocalizedString(@"Draw Your Pattern", nil);
            break;

        default:
            break;
    }
    _tipLabel.text = text;
}


- (UIImage *)drawline{

    if (_selectArray.count == 0) {
        return nil;
    }
    RoundRectView *ve = _selectArray.firstObject;
    CGPoint vePoint = ve.center;
    _startPoint = vePoint;

    UIImage *img = [[UIImage alloc] init];
    UIColor *color = [UIColor hexString:@"#3fb8c8"];

    UIGraphicsBeginImageContext(_imageView.frame.size);
    CGContextRef context = UIGraphicsGetCurrentContext();
//    CGContextSetAllowsAntialiasing(context,true);
//    CGContextSetShouldAntialias(context, true);

    CGContextSetLineWidth(context, 2);
    CGContextSetStrokeColorWithColor(context, color.CGColor);
    CGContextMoveToPoint(context, _startPoint.x, _startPoint.y);


    if (_selectArray.count == 0) {
        return nil;
    }

    for (RoundRectView *ve in _selectArray) {
        CGPoint po = ve.center;
        CGContextAddLineToPoint(context, po.x, po.y);
        CGContextMoveToPoint(context, po.x, po.y);
    }

    CGContextAddLineToPoint(context, _endPoint.x, _endPoint.y);
    CGContextStrokePath(context);

    img = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();

    return img;
}


- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{

    UITouch *touch = [touches anyObject];
    if (touch) {
        for (RoundRectView *ve in _nomarlArray) {
            CGPoint point = [touch locationInView:ve];

            if ([ve pointInside:point withEvent:nil]) {
                [_selectArray addObject:ve];
                [self savePassWord:ve.tag];
                ve.selectView.hidden = NO;
                _startPoint = ve.center;
            }
        }
    }
}


- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    UITouch *touch = [touches anyObject];
    if (touch) {
        _endPoint = [touch locationInView:_imageView];

        for (RoundRectView *ve in _nomarlArray) {
            CGPoint point = [touch locationInView:ve];

            if ([ve pointInside:point withEvent:nil]) {

                BOOL isAdd = YES;
                for (RoundRectView *vv in _selectArray) {
                    if (vv == ve) {
                        isAdd = NO;
                        break;
                    }
                }

                if (isAdd) {
                    [_selectArray addObject:ve];
                    [self savePassWord:ve.tag];
                    ve.selectView.hidden = NO;
                }
            }
        }
    }
    _imageView.image = [self drawline];
    _imageView.layer.contentsScale = [[UIScreen mainScreen] scale];

}


- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{

    UITouch *touch = [touches anyObject];
    if (touch) {
        for (RoundRectView *ve in _nomarlArray) {
            CGPoint po = [touch locationInView:ve];
            if (![ve pointInside:po withEvent:nil]) {
                RoundRectView *seVe = _selectArray.lastObject;
                _endPoint = seVe.center;
                _imageView.image = [self drawline];
            }
        }
    }
    [self cheakPassword];

    _password = [NSMutableString string];

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

        _imageView.image = nil;
        [_selectArray removeAllObjects];

        for (RoundRectView *ve in _nomarlArray) {
            ve.selectView.hidden = YES;
        }
    });
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
}


- (void)savePassWord:(NSInteger)tag{
    [_password appendFormat:@"%ld",tag];
}


- (void)cheakPassword{

    if (_password.length < 4) {
        [self changeTip:1];
        return; //密码长度不够
    }

    if (_unlockType == TouchUnlockCreatePwd) {

        if (_steps == 2) {
            _oldPassword = _password;
            [self changeTipForNormal:3];
            _steps = 3;
            return;
        }

        if (_steps == 3) {
            if (![_password isEqualToString:_oldPassword]) {
                [self changeTip:2];
                return; //前后不一致
            }

            if ([_oldPassword isEqualToString:_password]) {
                //两次密码一致 存本地
                NSUserDefaults *def = [NSUserDefaults standardUserDefaults];
                [def setObject:_password forKey:TouchUnlockPwdKey];
                [MessageCenter openAlertViewWithMessage:NSLocalizedString(@"Success", nil) duringtime:1];
                [self.navigationController popViewControllerAnimated:YES];
            }
        }
    }

    else if (_unlockType == TouchUnlockValidatePwd || _unlockType == TouchUnlockUpdatePwd){
        //获取保存的密码
        NSUserDefaults *def = [NSUserDefaults standardUserDefaults];
        NSString *savePwd = [def objectForKey:TouchUnlockPwdKey];

        if (![savePwd isEqualToString:_password] && _steps == 1) {
            //原密码错误
            _steps = 1;
            [self changeTip:3];

            if (_errorCount <= 0) {
                //错误次数太多
                [self alertPwdError];
                return;
            }

            return;
        }
        //登陆成功
        if (_unlockType == TouchUnlockValidatePwd) {

            _errorCount = 5;
//            [self dismissViewControllerAnimated:YES completion:nil];

            if (![APPDELEGATEREAL lockScreenWindow].hidden) {
                [APPDELEGATEREAL lockScreenWindow].hidden = YES;
            }
            return;
        }

        if (_steps == 1) {
            [self changeTipForNormal:1];
            _steps = 2;
            return;
        }

        if (_steps == 2) {
            _oldPassword = _password;
            [self changeTipForNormal:3];
            _steps = 3;
            return;
        }

        if (_steps == 3) {
            if (![_password isEqualToString:_oldPassword]) {
                 [self changeTip:2];
                return;
            }

            if ([_oldPassword isEqualToString:_password]) {
                //两次密码一致 存本地
                NSUserDefaults *def = [NSUserDefaults standardUserDefaults];
                [def setObject:_password forKey:TouchUnlockPwdKey];

                NSLog(@"密码更新成功:%@",_password);
                [MessageCenter openAlertViewWithMessage:NSLocalizedString(@"Success", nil) duringtime:1];
                dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                    [self.navigationController popViewControllerAnimated:YES];
                });
            }
        }

    }

}


- (void)alertPwdError{
    //手势密码失效
    [[NSUserDefaults standardUserDefaults] setObject:@"TouchPwdError" forKey:TouchUnlockPwdKey];

    UIAlertController *alert = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Pattern is expired", nil) message:NSLocalizedString(@"Please login with password", nil) preferredStyle:UIAlertControllerStyleAlert];

    NSMutableAttributedString *alertControllerMessageStr = [[NSMutableAttributedString alloc] initWithString:NSLocalizedString(@"Please login with password", nil)];
    [alertControllerMessageStr addAttribute:NSForegroundColorAttributeName value:[UIColor hexString:@"#999999"] range:NSMakeRange(0, alertControllerMessageStr.length)];
    [alertControllerMessageStr addAttribute:NSFontAttributeName value:[UIFont systemFontOfSize:12] range:NSMakeRange(0, alertControllerMessageStr.length)];
    [alert setValue:alertControllerMessageStr forKey:@"attributedMessage"];

    UIAlertAction *toAction = [UIAlertAction actionWithTitle:NSLocalizedString(@"Login again", nil) style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        [self loginWithPwd];
    }];
    [alert addAction:toAction];
    [self presentViewController:alert animated:YES completion:nil];
}


- (void)loginWithPwd{
    LX_LoginAndForgetViewController *loginVc = [[LX_LoginAndForgetViewController alloc] init];
    loginVc.isTouchUnlockIn = YES;
    [self presentViewController:loginVc animated:YES completion:nil];
}

//重设
- (void)resetBtnClick{
    _oldPassword = nil;
    _steps = 2;
    [self changeTipForNormal:1];
}

@end

关于优化

性能优化一直都是一个大问题,不要以为前端不需要考虑内存,就可以随便写代码。

之前在设计自己网页的时候,用到了滚动,鼠标滑轮轻轻一碰,滚动函数就执行了几十多则几百次,之前也考虑过解决办法。

5、AppDelegate中新增UIwindow属性用于遮罩

优化 canvas 部分

对于 touchmove 函数,原理都是一样的,手指一划,就执行了 n
多次,这个问题后面在解决,先来看另一个问题。

touchmove
是一个高频函数,看到这里,如果你并没有仔细看我的代码,那你对我采用的
canvas 画图方式可能不太了解,下面这个是 touchmove 函数干了哪些事:

  1. 先判断,如果当前处于未选中一个密码状态,则继续监视当前的位置,直到选中第一个密码,进入第二步;
  2. 进入 update 函数,update
    函数主要干四件事,重绘圆(密码)、判断当前位置、重绘点、重绘线;

第二步是一个很揪心的动作,为什么每次都要重绘圆,点和线呢?

金沙国际官网 8

上面这个图可以很好的说明问题,因为在设置或验证密码的过程中,我们需要用一条线来连接触点到当前的最后一个密码,并且当
touchmove 的时候,能看到它们在变化。这个功能很棒,可以勾勒出 touchmove
的轨迹。

但是,这就必须要时刻刷新 canvas,性能大大地降低,刷新的那可是整个
canvas。

因为 canvas
只有一个,既要画背景圆(密码),又要画已选密码的点,和折线。这其中好多步骤,自始至终只需要一次就好了,比如背景圆,只需在启动的时候画一次,已选密码,只要当
touchCircles
新加元素的时候才会用一次,还不用重绘,只要画就可以了。折线分成两部分,一部分是已选密码之间的连线,还有就是最后一个密码点到当前触点之间的连线。

如果有两个 canvas 就好了,一个存储静态的,一个专门用于重绘

为什么不可以有呢!

我的解决思路是,现在有两个
canvas,一个在底层,作为描绘静态的圆、点和折线,另一个在上层,一方面监听
touchmove
事件,另一方面不停地重绘最后一个密码点的圆心到当前触点之间的线。如果这样可以的话,touchmove
函数执行一次的效率大大提高。

插入第二个 canvas:

var canvas2 = canvas.cloneNode(canvas, true); canvas2.style.position =
‘absolute’;//让上层 canvas 覆盖底层 canvas canvas2.style.top = ‘0’;
canvas2.style.left = ‘0’; this.el.appendChild(canvas2); this.ctx2 =
canvas2.getContext(‘2d’);

1
2
3
4
5
6
var canvas2 = canvas.cloneNode(canvas, true);
canvas2.style.position = ‘absolute’;//让上层 canvas 覆盖底层 canvas
canvas2.style.top = ‘0’;
canvas2.style.left = ‘0’;
this.el.appendChild(canvas2);
this.ctx2 = canvas2.getContext(‘2d’);

要改换对第二个 ctx2 进行 touch 监听,并设置一个 this.reDraw
参数,表示有新的密码添加进来,需要对点和折线添加新内容, update
函数要改成这样:

update: function(p){ // 更新 touchmove this.judgePos(p); // 每次都要判断
this.drawLine2TouchPos(p); //
新加函数,用于绘最后一个密码点点圆心到触点之间的线 if(this.reDraw){ //
有新的密码加进来 this.reDraw = false; this.drawPoints(); // 添加新点
this.drawLine();// 添加新线 } },

1
2
3
4
5
6
7
8
9
update: function(p){ // 更新 touchmove
  this.judgePos(p); // 每次都要判断
  this.drawLine2TouchPos(p); // 新加函数,用于绘最后一个密码点点圆心到触点之间的线
  if(this.reDraw){ // 有新的密码加进来
    this.reDraw = false;
    this.drawPoints(); // 添加新点
    this.drawLine();// 添加新线
  }
},

drawLine2TouchPos: function(p){ var len = this.touchCircles.length;
if(len >= 1){ this.ctx2.clearRect(0, 0, this.width, this.width); //
先清空 this.ctx2.beginPath(); this.ctx2.lineWidth = 3;
this.ctx2.moveTo(this.touchCircles[len – 1].x, this.touchCircles[len

  • 1].y); this.ctx2.lineTo(p.x, p.y); this.ctx2.stroke();
    this.ctx2.closePath(); } },
1
2
3
4
5
6
7
8
9
10
11
12
drawLine2TouchPos: function(p){
  var len = this.touchCircles.length;
  if(len >= 1){
    this.ctx2.clearRect(0, 0, this.width, this.width); // 先清空
    this.ctx2.beginPath();
    this.ctx2.lineWidth = 3;
    this.ctx2.moveTo(this.touchCircles[len – 1].x, this.touchCircles[len – 1].y);
    this.ctx2.lineTo(p.x, p.y);
    this.ctx2.stroke();
    this.ctx2.closePath();
  }
},

相应的 drawPoints 和 drawLine
函数也要对应修改,由原理画所有的,到现在只需要画新加的。

效果怎么样:

金沙国际官网 9

move 函数执行多次,而其他函数只有当新密码加进来的时候才执行一次。

//didFinishLaunchingWithOptions方法加入
self.lockScreenWindow = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
 self.lockScreenWindow.windowLevel= UIWindowLevelAlert+1;//最高级等级窗口
 self.lockScreenWindow.backgroundColor = [UIColor whiteColor];
 TouchUpViewController *lockvc  =[[TouchUpViewController alloc]initWithUnlockType:TouchUnlockValidatePwd];
 self.lockScreenWindow.rootViewController = lockvc;
 self.lockScreenWindow.hidden = YES;

加入节流函数

之前也已经说过了,这个 touchmove
函数执行的次数比较多,尽管我们已经用两个 canvas 对重绘做了很大的优化,但
touchmove 还是有点大开销。

这个时候我想到了防抖动和节流,首先防抖动肯定是不行的,万一我一直处于
touch
状态,重绘会延迟死的,这个时候节流会好一些。防抖和节流。

先写一个节流函数:

throttle: function(func, delay, mustRun){ var timer, startTime = new
Date(), self = this; return function(){ var curTime = new Date(), args =
arguments; clearTimeout(timer); if(curTime – startTime >= mustRun){
startTime = curTime; func.apply(self, args); }else{ timer =
setTimeout(function(){ func.apply(self, args); }, delay) } } }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
throttle: function(func, delay, mustRun){
  var timer, startTime = new Date(), self = this;
  return function(){
    var curTime = new Date(), args = arguments;
    clearTimeout(timer);
    if(curTime – startTime >= mustRun){
      startTime = curTime;
      func.apply(self, args);
    }else{
      timer = setTimeout(function(){
        func.apply(self, args);
      }, delay)
    }
  }
}

节流函数的意思:在延迟为 delay
的时间内,如果函数再次触发,则重新计时,这个功能和防抖动是一样的,第三个参数
mustRun 是一个时间间隔,表示在时间间隔大于 mustRun
后的一个函数可以立即直接执行。

然后对 touchmove 的回调函数进行改造:

var t = this.throttle(function(e){ e.preventDefault ? e.preventDefault()
: null; e.stopPropagation ? e.stopPropagation() : null; var p =
this.getTouchPos(e); if(this.touchFlag){ this.update(p); }else{
this.judgePos(p); } }, 16, 16)
this.canvas2.addEventListener(‘touchmove’, t, false)

1
2
3
4
5
6
7
8
9
10
11
var t = this.throttle(function(e){
  e.preventDefault ? e.preventDefault() : null;
  e.stopPropagation ? e.stopPropagation() : null;
  var p = this.getTouchPos(e);
  if(this.touchFlag){
    this.update(p);
  }else{
    this.judgePos(p);
  }
}, 16, 16)
this.canvas2.addEventListener(‘touchmove’, t, false)

关于 delay 和 mustRun 的时间间隔问题,web 性能里有一个 16ms
的概念,就是说如果要达到每秒 60 帧,间隔为 1000/60 大约为 16
ms。如果间隔大于 16ms 则 fps 会比 60 低。

鉴于此,我们这里将 delay 和 mustRun 都设为
16,在极端的情况下,也就是最坏的情况下,或许需要 15 + 15 = 30ms
才会执行一次,这个时候要设置两个 8
才合理,不过考虑到手指活动是一个连续的过程,怎么可能会每 15
秒执行一次,经过在线测试,发现设置成 16 效果还不错。

性能真的能优化吗,我们来看两个图片,do 和 wantdo
表示真实执行和放到节流函数中排队准备执行。

当 touchmove 速度一般或很快的时候:

金沙国际官网 10

当 touchmove 速度很慢的时候:

金沙国际官网 11

可以看出来,滑动过程中,速度一般和快速,平均优化了一半,慢速效果也优化了
20 到 30%
之间,平时手势锁解锁时候,肯定速度很快。可见,节流的优化还是很明显的。

关键是,优化之后的流程性,没有受到任何影响。

这个节流函数最终还是出现了一个 bug:由于是延迟执行的,导致
e.preventDefault
失效,在手机浏览器向下滑会出现刷新的情况,这也算事件延迟的一个危害吧。

解决办法:在节流函数提前取消默认事件:

throttle: function(func, delay, mustRun){ var timer, startTime = new
Date(), self = this; return function(e){ if(e){ e.preventDefault ?
e.preventDefault() : null; //提前取消默认事件,不要等到 setTimeout
e.stopPropagation ? e.stopPropagation() : null; } … } }

1
2
3
4
5
6
7
8
9
10
throttle: function(func, delay, mustRun){
  var timer, startTime = new Date(), self = this;
  return function(e){
    if(e){
      e.preventDefault ? e.preventDefault() : null; //提前取消默认事件,不要等到 setTimeout
      e.stopPropagation ? e.stopPropagation() : null;
    }
    …
  }
}

在合适的时候对lockScreenWindow.hidden属性进行操作。

总结

大概花了三天左右的时间,将这个 H5
的手势解锁给完成,自己还是比较满意的,虽然可能达不到评委老师的认可,不过自己在做的过程中,学习到了很多新知识。

通过上述两个类,初步建立了手势密码的逻辑体系。

参考

H5lock
Canvas教程
js获取单选框里面的值
前端高性能滚动 scroll
及页面渲染优化

3 赞 5 收藏
评论

金沙国际官网 12

发表评论

电子邮件地址不会被公开。 必填项已用*标注

网站地图xml地图