几天前,看到极客学院有一个锁屏的课程,然后点进去看了看,最后实现了锁屏,但是最后各个接口并没有完善。后来自己对此进行了总结并完善相关接口。主要内容就两点:
1、锁屏界面的绘制及滑动事件处理;
2、设置锁屏手势以及解锁。
先上效果图:

打开

Android 10 自定义锁屏 手机自定义锁屏_锁屏

错误

Android 10 自定义锁屏 手机自定义锁屏_事件分发机制_02

滑动中

Android 10 自定义锁屏 手机自定义锁屏_极客_03

1、锁屏界面的绘制,这部分我总结为四个步骤:
1.1 初始化,准备相关的尺寸;
1.2 绘制圆点;
1.3 触摸事件;
1.4 绘制触摸事件经过的路径。

我们先完成这一部分再继续研究后面的接口回调。
1.1 初始化,准备相关的尺寸。由于这个锁屏的区域比较复杂,这里我直接以自定义View来实现的。那么在View的构造方法里面,我们需要做什么准备工作呢?答案是没有。因为在初始化的方法中,我们需要拿到具体的尺寸,所以我们直接在OnDraw方法来处理,先上效果图:(圆形以方形代替了,只要理解这个意义即可,下同)

Android 10 自定义锁屏 手机自定义锁屏_锁屏_04

代码如下

/**
         * 是否初始化
         * inited默认为false
         */
        if(!inited)
        {//没有初始化
            inited = true;
            init();
        }



    /**
     * 初始化
     */
    private void init()
    {
        /**
         * 手指按下的画笔
         */
        pressPaint = new Paint();
        /**
         * 抗锯齿
         */
        pressPaint.setAntiAlias(true);
        /**
         * 画笔颜色
         */
        pressPaint.setColor(Color.YELLOW);
        /**
         * 画笔线宽
         */
        pressPaint.setStrokeWidth(5);
        /**
         * 提示错误的画笔
         */
        errorPaint = new Paint();
        errorPaint.setStrokeWidth(5);
        errorPaint.setColor(Color.RED);
        errorPaint.setAntiAlias(true);

        //锁屏的不同状态图片

        //正常
        bitmapNormal = BitmapFactory.decodeResource(getResources(), R.mipmap.normal);
        //按下
        bitmapPress = BitmapFactory.decodeResource(getResources(), R.mipmap.press);
        //错误
        bitmapError = BitmapFactory.decodeResource(getResources(),R.mipmap.error);
        //格子半径
        //上面三张Bitmap的尺寸是一致的
        halfWidth = bitmapError.getWidth()/2;
        /**
         * 锁屏宽度
         */
        int width = getWidth();
        /**
         * 锁屏高度
         */
        int height = getHeight();
        /**
         * 偏移量
         * 目的是后面使锁屏图片居中
         * 值为款高差的一半
         */
        int offet = Math.abs(width-height)/2;
        /**
         * X轴以及Y轴的偏移量
         */
        int offsetx,offsety;
        /**
         * 格子宽度
         */
        int space;

        if(width>height)//横屏的情况
        {
            space = height/4;
            offsetx = offet;
            offsety = 0;
        }else//竖屏的情况
        {
            space = width/4;
            offsetx = 0;
            offsety = offet;
        }

//        第一排
         points.add(new Point(offsetx+space,offsety+space));
         points.add(new Point(offsetx+space*2,offsety+space));
         points.add(new Point(offsetx+space*3,offsety+space));
//        第二排
         points.add(new Point(offsetx+space,offsety+space*2));
         points.add(new Point(offsetx+space*2,offsety+space*2));
         points.add(new Point(offsetx+space*3,offsety+space*2));
//        第三排
         points.add(new Point(offsetx+space,offsety+space*3));
         points.add(new Point(offsetx+space*2,offsety+space*3));
         points.add(new Point(offsetx+space*3,offsety+space*3));

    }

上面的效果应该知道是什么意思,但是这个Point的实体可能不是很清楚,这里我把代码贴出来:

/**
 * 锁屏的格子
 * Created by Vicent on 2016/10/15.
 */

public class Point {
    public static final int STATE_NOMAL = 0;
    public static final int STATE_PRESS = 1;
    public static final int STATE_ERROR = 2;

    public float x,y;
    public int state;

    public Point(float x, float y) {
        this.x = x;
        this.y = y;
    }

    /**
     * 计算点到改点圆心的距离
     * @param a
     * @return
     */
    public float checkDistance(Point a)
    {
        return (float)Math.sqrt((x-a.x)*(x-a.x)+(y-a.y)*(y-a.y));

    }
}

OK,接下来我们来继续实现1.2 绘制圆点,这里每次刷新的时候都需要绘制的。但是对于圆心,我们需要移动一点位置,先看效果图(在Excel里面绘制的,大家有没有比较好的绘制工具呢?我还记得一点AutoCAD,但是怕用这个更麻烦,,,,):

Android 10 自定义锁屏 手机自定义锁屏_锁屏_05

//绘制点
        drawPoint(canvas);

 /**
     * 绘制锁屏界面的九宫格
     * @param canvas
     */
    private void drawPoint(Canvas canvas)
    {
        for (int i = 0;i<points.size();i++)
        {

                Point point = points.get(i);
                switch (point.state)
                {
                    //STATE_NOMAL
                    case 0:
                        canvas.drawBitmap(bitmapNormal,point.x-halfWidth,point.y-halfWidth,paint);
                        break;
                    //STATE_PRESS
                    case 1:
                        canvas.drawBitmap(bitmapPress,point.x-halfWidth,point.y-halfWidth,paint);
                        break;
                    //STATE_ERROR
                    case 2:
                        canvas.drawBitmap(bitmapError,point.x-halfWidth,point.y-halfWidth,paint);
                        break;
                }

        }
    }

接下来就应该是1.3触摸事件了。这部分就只有用代码说话了,文字表达的话更抽象。代码如下

@Override
    public boolean onTouchEvent(MotionEvent event) {
        mouseX = event.getX();
        mouseY = event.getY();
        //拿到当前触摸的点
        Point point = getSelctedPoint(mouseX,mouseY);
        switch (event.getAction())
        {
            case MotionEvent.ACTION_DOWN:
                //当前的点在屏幕上
                if(point!=null)
                {
                    //清除之前的屏幕数据
                    restPoints();
                    //需要绘制按下的点
                    isDraw = true;
                    //修改状态
                    point.state = Point.STATE_PRESS;
                    //保存当前的点
                    pointList.add(point);
                }
                postInvalidate();
                break;
            case MotionEvent.ACTION_MOVE:
                if(isDraw)
                {
                //当前的点在屏幕上
                //当前的点没有被保存
                    if(point!=null && !pointList.contains(point))
                    {

                        point.state = Point.STATE_PRESS;
                        pointList.add(point);
                    }
                }
                postInvalidate();
                break;
            case MotionEvent.ACTION_UP:


                isDraw = false;
                //滑动的点太少了
                    if(passList.size()<4){
                        isErr = true;
                        for (Point p : pointList){
                            p.state = Point.STATE_ERROR;
                        }
                        postInvalidate();

                    }else{
                        postInvalidate();

                    }

                break;
        }
        //当前的滑动事件被消费(处理完毕),不用返回父布局处理了。
        return true;
    }

上面调用了的方法: //拿到当前触摸的点
Point point = getSelctedPoint(mouseX,mouseY);

/**
     * 获取选中的格子
     * @param x
     * @param y
     * @return
     */
    private Point getSelctedPoint(float x,float y)
    {

        Point mousePoint = new Point(x, y);
       for (int i=0;i<points.size();i++)
       {
       //之前的实体类里面已经写过checkDistance,这里就不再赘述了。
           if(points.get(i).checkDistance(mousePoint)<halfWidth)
           {

               return points.get(i);
           }
       }
        return null;

    }

接下来就该总结1.4 绘制触摸事件经过的路径,跟上面一样,代码是世界上最美妙最容易理解的语言,我们还是通过代码来看最后干的事情吧!

//绘制滑动过的路径
        if(pointList.size()>0)
        {
                Point a = pointList.get(0);
                for (int i = 1;i<pointList.size();i++)
                {
                    Point b = pointList.get(i);
                    drawLine(canvas,a,b);
                    a = b;
                }
            //滑动的时候绘制经过的每一个点
            if(isDraw)
            {
                drawLine(canvas,a,new Point(mouseX,mouseY));
            }
        }
        //异常状态的刷新
        if(isErr)
            refreshErr();

上面调用了前面没有说到的方法,这里写出来看看这些方法是干什么的,具体做了什么事?

/**
     * 绘制手势的直线
     * @param canvas
     * @param a
     * @param b
     */
    private void drawLine(Canvas canvas,Point a,Point b)
    {
        if(a.state == Point.STATE_PRESS)
        {
            canvas.drawLine(a.x,a.y,b.x,b.y,pressPaint);
        }else if(a.state == Point.STATE_ERROR)
        {
            canvas.drawLine(a.x,a.y,b.x,b.y,errorPaint);
        }
    }
/**
     * 异常状态下延时更新为正常状态
     */
    private void refreshErr() {
        for (Point p : pointList){
            p.state = Point.STATE_NOMAL;
        }
        //刷新
        postInvalidateDelayed(1500);
        isErr = false;
    }

OK,上面讲的索引绘制方法都是在OnDraw方法里面执行的,现在已经可以正常的绘制了,但也仅仅是绘制,而且根本不能设置手势的正确与否。那么,接下来我们就来看看怎么实现手势的设置、确认、调用手势检查。
由于上面的内容只是一个自定义类(锁屏View),而后面的内容涉及到自定义View与Activity的通信,所以先看看布局效果:

Android 10 自定义锁屏 手机自定义锁屏_事件分发机制_06


既然已经到这里了,那么就把xml布局文件也贴出来,后面说起来也具体一些,不会太抽象。

代码如下

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/activity_lock"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <!-- 后面的大彩蛋-->
   <ImageView
       android:id="@+id/lock_laugh"
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       android:src="@mipmap/laugh"
       android:visibility="gone"/>


    <!--锁屏界面 -->
    <LinearLayout
        android:id="@+id/lock_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:visibility="visible">


    <TextView
        android:id="@+id/lock_hint"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:padding="5dp"
        android:text="绘制解锁图案"/>


    <view

        android:layout_width="match_parent"
        android:layout_height="450dp"
        class="cn.com.hhqy.lockapplication.view.GestureLock"
        android:id="@+id/lock_view" />
    <TextView
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:background="#AAAAAA"/>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">
        <Button
            android:id="@+id/lock_cancle"
            android:layout_width="0dip"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="center"
            android:background="@null"
            android:text="取消"/>

        <Button
            android:id="@+id/lock_sure"
            android:layout_width="0dip"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:gravity="center"
            android:background="@null"
            android:enabled="false"
            android:text="继续"/>
    </LinearLayout>
    </LinearLayout>
</LinearLayout>

这个通信的过程比较复杂,因为我是自己写的,没(不)有(会)使用mvp等设计模式,所以看上去比较凌乱。这也是为什么我要总结的原因,因为我一高兴起来自己都不知道这是什么代码。。。。。

废话说完了,接下来我们要继续总结第二部分内容,即手势的设置、确认、调用锁屏的检查。为了凑字数还是写一下:

2.1 手势的设置;
2.2 手势的确认;
2.3 调用锁屏(验证手势)

这一部分主要是接口的回调,所以我们需要定义一个接口,这个接口我们可以单独建一个类,也可以直接放在手势的View所承载的Activity或者是View本身,不过从规范来讲,该类如果需要复用的话最好是放到BaseActivity里面。这里因为基本上不会有复用,所以我们就假巴意思(装模作样,四川方言)的写一个接口类。

2.1 手势的设置,这里先建一个设置需要用到的接口,以及手势设置需要用到的方法。

package cn.com.hhqy.lockapplication;

import java.util.List;

/**
 * Activity与GestureLock(锁屏View)的通信
 * Created by Vicent on 2016/11/23.
 */

public interface OnDrawFilshedListener
{
    /**
     *
     * Button设置为可点击
     * 修改提示语为“已记录图案”
     * 修改Button文本为“重画”,“继续”
     */
    void OnTouchEventFinshOk1();

    /**
     * Button1设置为可点击
     * 修改提示语为“至少连接4个点,请重画”
     * 修改Button文本为“重画”,“继续”
     */
    void OnTouchEventFinshErr1();

    /**
     * Button设置为不可点击
     * 修改提示语为“完成后松开手指”
     */
    void TouchEventStart();
}

上面的接口已经把需要实现的方法写得很清楚了,接下来我们需要在GestureLock里面定义该接口对象,并提供一个公共方法来实例化该对象。虽然很简单,但是还是忍不住写一下。。。。。

private OnDrawFilshedListener listener;
 public void setOnDrawFilshedListener(OnDrawFilshedListener listener)
    {
        this.listener = listener;
    }

接下来,我们需要在Activity里面来实现这三个接口方法,将接口里面的要求给它实现了!不废话,直接上代码:

private void initLintener() {
         //确认按钮点击事件
        btnSure.setOnClickListener(this);
        //取消按钮点击事件
        btnCancle.setOnClickListener(this);
        //GestureLock的相关接口方法
        lockView.setOnDrawFilshedListener(new GestureLock.OnDrawFilshedListener() {
            @Override
            public void OnTouchEventFinshOk1() {
                btnSure.setEnabled(true);
                btnCancle.setEnabled(true);
                tvHint.setText("已记录图案");
                btnCancle.setText("重画");
                btnSure.setText("继续");

            }

            @Override
            public void OnTouchEventFinshErr1() {
                btnSure.setEnabled(false);
                btnCancle.setEnabled(true);
                tvHint.setText("至少连接4个点,请重画");
                btnCancle.setText("重画");
                btnSure.setText("继续");

            }


            @Override
            public void TouchEventStart() {
                btnSure.setEnabled(false);
                btnCancle.setEnabled(false);
                tvHint.setText("完成后松开手指");

            }
        });
    }

Ok,上面我们已经完成了界面的显示,但是既然是手势的设置,那么就需要点击按钮来设置。接下来我们看看这个时候按钮的取消与继续会执行上面方法?

/**
     * 取消键响应事件
     */
    private void clickCancle() {
        lockView.restPoints();
         tvHint.setText("绘制解锁图案");
         btnCancle.setText("取消");
         btnSure.setEnabled(false);
    }

等等,报告,这里发现一个坑!

Android 10 自定义锁屏 手机自定义锁屏_Android 10 自定义锁屏_07

什么,听不懂?好吧,我来给你说道说道。你这里的取消点击事件怎么能这么写呢?当用户输入正确或者错误,咋办呢?还是这样直接写吗?好像不对吧?

其实这里只是装逼,为了把枚举引出来而已。因为在我们的逻辑里面,不管设置手势的时候正确与否,界面都是这样显示的。

不过既然要引出枚举来,那么它在这里有什么作用啊?其实就是为后面的状态做一个区分,告诉Activity,当前还只是设置密码,后面还有确认密码、检查密码等状态。说干就干,先来看看枚举我们是怎么定义的?

private enum State{
        Aok/*第一次滑动结果OK*/,
        Aerr/*第一次滑动结果Err*/,
        Bok/*第二次滑动结果OK*/,
        Berr/*第二次滑动结果Err*/,

    };

该State对象默认为空,接下来看看取消键响应事件的内容:

/**
     * 取消键响应事件
     */
    private void clickCancle() {
        if(state==null)
            finish();
        if (state== State.Aerr || state== State.Aok){
            lockView.restPoints();
            tvHint.setText("绘制解锁图案");
            btnCancle.setText("取消");
            btnSure.setEnabled(false);
            state = null;
        }
    }

看到这里可能会疑问这个状态是什么时候赋值,其实应该是在接口的回调里面来判断当前的状态,这里重写之前的接口回调方法。

lockView.setOnDrawFilshedListener(new GestureLock.OnDrawFilshedListener() {
            @Override
            public void OnTouchEventFinshOk1() {
                btnSure.setEnabled(true);
                btnCancle.setEnabled(true);
                tvHint.setText("已记录图案");
                btnCancle.setText("重画");
                btnSure.setText("继续");
                state = State.Aok;
            }

            @Override
            public void OnTouchEventFinshErr1() {
                btnSure.setEnabled(false);
                btnCancle.setEnabled(true);
                tvHint.setText("至少连接4个点,请重画");
                btnCancle.setText("重画");
                btnSure.setText("继续");
                state = State.Aerr;
            }



            @Override
            public void TouchEventStart() {
                btnSure.setEnabled(false);
                btnCancle.setEnabled(false);
                tvHint.setText("完成后松开手指");
                state = null;
            }
        });

OK,接下来我们看看2.1的重点,设置手势。这里我们需要在确认键响应事件里面来完成。我们看看这里完成了什么具体的内容?

/**
     * 确认键响应事件
     */
    private void clickSure() {
        if(state== State.Aok){
            tvHint.setText("再次绘制图案以确认");
            btnCancle.setText("取消");
            btnSure.setText("确认");
            btnSure.setEnabled(false);
            state = null;
            lockView.clickGoOn();
        }
    }

这里不要问我为什么第一次手势错误(比如滑动的时候只经历了3个点,默认必须达到4个点)的状态为什么不干事情,是不是偷懒了?我只能告诉你继续看看前面的代码,错误的时候确认键是禁止点击的。
接下来我们需要到GestureLock里面去看看这个clickGoOn方法是干嘛的?他是怎么设置的?

/**
     * 点击继续按钮的响应
     */
    public void clickGoOn(){
        //默认值为0
        state = 2;
        //保存刚刚滑动的时候,滑动的点及顺序
        checkNumner.addAll(passList);
        //重置参数并刷新界面待第二次确认手势
        restPoints();
    }

看到这里可能会有疑问:这个state是干嘛的?什么时候用?怎么用?第二是那个restPoints方法是怎么实现的?这里我们先看看方法的内容:

/**
     * 重置参数以及刷新界面
     */
    public void restPoints()
    {
        //滑动手势经历的点的顺序及数量
        passList.clear();
        //滑动的点
        pointList.clear();
        for (int i = 0;i<points.size();i++)
        {
            points.get(i).state = Point.STATE_NOMAL;
        }
        isErr = false;
        postInvalidate();
    }

这个方法比较简单,就不解释了。接下来看看这个state是在什么地方怎么用的?其实他只是在手指抬起界面的时候来判断是,我们现在在来看看这部分代码:

case MotionEvent.ACTION_UP:


        isDraw = false;
        if(state==0){
            if(passList.size()<4){
                isErr = true;
                for (Point p : pointList){
                    p.state = Point.STATE_ERROR;
                }
                postInvalidate();
                if(listener!=null)
                    listener.OnTouchEventFinshErr1();
            }else{
                postInvalidate();
                if(listener!=null)
                listener.OnTouchEventFinshOk1();
            }
        }else{
            /***/
        }
        break;

唉呀妈呀,终于把这一部分写清楚了。接下来继续第二坑,2.2 手势的确认。其实严格来讲,这一部分和2.1应该是一个整体的。但是为了上面说得顺口,就分成了两部分。这样也有一个好处,就是每种状态比较清楚了。废话少说,我们直接在接口里面增加第二次滑动手势的时候接口回调方法。

/**
 * Activity与GestureLock(锁屏View)的通信
 * Created by Vicent on 2016/11/23.
 */

public interface OnDrawFilshedListener
{
    。。。
    /**
     * Button1设置为可点击
     * 修改提示语为“图案错误,请重试”
     * 修改Button文本为“取消”,“确认”
     */
    void OnTouchEventFinshErr2();
    /**
     * Button设置为可点击
     * 修改提示语为“你的新解锁图案”
     * 修改Button文本为“取消”,“确认”
     * @param number
     */
    void OnTouchEventFinshOk2(List<Integer> number);

   。。。
}

既然这里也已经写好了我们需要实现的内容,那么我们先实现了接口方法再继续看下面的内容。

lockView.setOnDrawFilshedListener(new GestureLock.OnDrawFilshedListener() {


            @Override
            public void OnTouchEventFinshErr2() {
                btnSure.setEnabled(false);
                btnCancle.setEnabled(true);
                tvHint.setText("图案错误,请重试");
                btnCancle.setText("取消");
                btnSure.setText("确认");
                state = State.Berr;

            }

            @Override
            public void OnTouchEventFinshOk2(List<Integer> number) {
                btnSure.setEnabled(true);
                btnCancle.setEnabled(true);
                tvHint.setText("你的新解锁图案");
                btnCancle.setText("取消");
                btnSure.setText("确认");
                state = State.Bok;
                //手势经历的点的顺序及数量
                passwordNumber = number;
            }


        });

接下来我们需要关心的是上面的两个接口方法在什么情况下如何调用的?这里我们还得回到GestureLock的OnTouchEvent方法:

case MotionEvent.ACTION_UP:

                isDraw = false;
                if(state==0){
                    if(passList.size()<4){
                        isErr = true;
                        for (Point p : pointList){
                            p.state = Point.STATE_ERROR;
                        }
                        postInvalidate();
                        if(listener!=null)
                            listener.OnTouchEventFinshErr1();
                    }else{
                        postInvalidate();
                        if(listener!=null)
                        listener.OnTouchEventFinshOk1();
                    }
                }else{
                    if(passList.size()<4 || checkNumber()){
                        isErr = true;
                        for (Point p : pointList){
                            p.state = Point.STATE_ERROR;
                        }
                        postInvalidate();
                        if(listener!=null){

                            listener.OnTouchEventFinshErr2();

                        }

                    }else{
                        postInvalidate();
                        if(listener!=null)
                        listener.OnTouchEventFinshOk2(passList);
                    }
                }
                break;
        }

其实这里就是在第二次触摸的时候增加了一个判断是否为错误手势的标准,即checkNumber方法,该方法返回true则该手势为错误手势。我们进入这个手势里面去看看这里是怎么来判断的?

/**
     * 确认九宫格密码是否错误
     * @return
     */
    private boolean checkNumber() {
        //当前经历的点和设置的点数量是否一致
        if(passList.size()!=checkNumner.size())
        return true;

        for (int i = 0; i < checkNumner.size(); i++) {
            //每个点的顺序是否一致
            if (passList.get(i)!=checkNumner.get(i))
                return true;
        }
        return false;
    }

OK,这里我们已经完成了手势的确认,并返回了手势经历点的顺序及数量。这里我们还要完善两个方法:

/**
     * 确认键响应事件
     */
    private void clickSure() {
        if(state== State.Aok){
            tvHint.setText("再次绘制图案以确认");
            btnCancle.setText("取消");
            btnSure.setText("确认");
            btnSure.setEnabled(false);
            state = null;
            lockView.clickGoOn();
        }else if(state == State.Bok){
            //TODO 保存顺序
            if(passwordNumber!=null){
                //保存手势数据
                saveArray();
                //TODO 手势设置好了,打算干什么?
                startActivity(new Intent(this,MainActivity.class));
                //关闭
                finish();
            }
        }
    }

这里插入一个方法,该方法是通过SharedPreferences保存List数据,方法也挺简单:

/**
     * 保存List数据
     * @return
     */
    public void saveArray() {

        SharedPreferences.Editor mEdit1 = sp.edit();
        mEdit1.putInt("Status_size",passwordNumber.size()); /*sKey is an array*/

        for(int i=0;i<passwordNumber.size();i++) {
            mEdit1.remove("Status_" + i);
            mEdit1.putInt("Status_" + i, passwordNumber.get(i));
        }

        mEdit1.apply();
    }

然后完善第二个需要完善的方法:

/**
     * 取消键响应事件
     */
    private void clickCancle() {
        if(state==null)
            finish();
        if (state== State.Aerr || state== State.Aok){
            lockView.restPoints();
            tvHint.setText("绘制解锁图案");
            btnCancle.setText("取消");
            btnSure.setEnabled(false);
            state = null;
        }else if(state== State.Berr || state == State.Bok ){
            finish();
        }
    }

到这里我们就完成了2.2 手势的确认。最后一个任务是2.3 调用锁屏(验证手势)。这个任务其实很简单,我们直接在Activity的OnCreate方法里面即可判断当前是否需要调用验证手势的功能,只需要调用一个initTwice方法即可。方法内容如下:

/**
     * 判断是否解锁初始化
     */
    private void initTwice() {
        //加载手势数据
        loadArray();
        if(passwordNumber.size()!=0){
            setTitle("绘制图案以解锁");
            btnSure.setEnabled(true);
            btnCancle.setEnabled(true);
            btnSure.setText("忘记图案");
            //切换当前的状态
            state = State.Twice;
             lockView.setTwice(passwordNumber);
        }
    }

这里我们又调用了一个GestureLock的setTwice方法,来设置验证手势,接下来我们再去分析这个方法具体做了什么?

/**
     * 验证手势
     * @param password
     */
    public void setTwice(List<Integer> password){
        checkNumner.clear();
        checkNumner = password;
        state = 4;
    }

上面我们将密码传了过去,然后将GestureLock的状态设置为验证手势的状态,接下来我们一样的需要到接口里面增加相关的两个方法,即手势错误与正确的时候界面作何反应?

/**
     * 验证手势时连续输错五次
     * 禁止解锁30秒
     */
    void OnError5Times();

    /**
     * 当手势验证通过
     */
    void OnTouchCheckOk();

既然这里写了这几个方法,那么就需要我们去实现接口方法,否则Activity会一直报错。

锁屏的实现:

@Override
            public void OnError5Times() {
                //拦截滑动事件,禁止滑动
                lock.getParent().requestDisallowInterceptTouchEvent(false);
                //禁止点击
                btnSure.setEnabled(false);
                //开启定时器
                timer.schedule(new TimerTask() {
                    @Override
                    public void run() {
                        handler.sendEmptyMessage(0);
                    }
                },0,1000);
            }

解锁通过:

@Override
            public void OnTouchCheckOk() {
                //TODO 待执行相关事宜
                ivLaugh.setImageResource(R.mipmap.fly);
                ivLaugh.setVisibility(View.VISIBLE);
                isForget = false;
                state = null;
            }

上面看到在锁屏过程中,我们通过计时器Timer来计时的,那这里发送消息后又坐了什么呢?打开看一看?

private Handler handler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            if(msg.what==0){
                tvHint.setText("请"+stopTouchEventTimes+"秒后重试");
                //减时
                stopTouchEventTimes--;
                if(stopTouchEventTimes==0){
                    //取消滑动事件拦截(取消锁屏)
                    lockView.getParent().requestDisallowInterceptTouchEvent(true);
                    //取消定时器
                    timer.cancel();
                    btnSure.setEnabled(true);
                    stopTouchEventTimes = 25;
                    return;
                }
            }
        }
    };

现在弄明白了接口方法是怎么实现的,那接下来就看看接口在什么地方调用的呢?老规矩,还是在手势抬起来的时候,一言不合就上代码:

case MotionEvent.ACTION_UP:


                isDraw = false;
                if(state==0){
               。。。
                }
                else if(state==4){
                    if(passList.size()<4 || checkNumber()){
                        isErr = true;
                        for (Point p : pointList){
                            p.state = Point.STATE_ERROR;
                        }
                        postInvalidate();
                        errTimes++;
                        if(errTimes==5){
                            if(listener!=null)
                                listener.OnError5Times();
                            errTimes = 0;
                        }

                    }else{
                        postInvalidate();
                        if(listener!=null)
                            listener.OnTouchCheckOk();
                        state = 0;
                    }
                }
                break;

上面有一个errTimes来对错误次数计数,默认为0。当错误次数达到5次就接口的锁屏方法——OnError5Times方法,调用之后将错误次数重新重置为0 ,如果在验证手势的时候成功了,同样是需要把该次数重置为0。

接下来我们可以测试一下接口调用的三个方法。

设置锁屏手势:

Android 10 自定义锁屏 手机自定义锁屏_自定义View_08

确认锁屏手势:

Android 10 自定义锁屏 手机自定义锁屏_Android 10 自定义锁屏_09

验证手势错误:

Android 10 自定义锁屏 手机自定义锁屏_自定义View_10

验证手势正确:

Android 10 自定义锁屏 手机自定义锁屏_锁屏_11

怎么样,可以吧?

Android 10 自定义锁屏 手机自定义锁屏_极客_12

小心,这里还有大坑!你试一试下面的这组手势看看是不是也可以“开心到飞起”?

Android 10 自定义锁屏 手机自定义锁屏_自定义View_13

估计测试以后你也不能开心到飞起了,因为现在这个手势只能验证滑动经历的路径(点)长度(数量),不能验证路径位置(点的顺序)。那么我们这里又需要重头来研究我们是怎么记录顺序的?(好像前头跳过去了没有讲。。。。。)

@Override
    public boolean onTouchEvent(MotionEvent event) {
        mouseX = event.getX();
        mouseY = event.getY();
        //拿到当前触摸的点
        Point point = getSelctedPoint(mouseX,mouseY);
        switch (event.getAction())
        {
            case MotionEvent.ACTION_DOWN:
                //当前的点在屏幕上
                if(point!=null)
                {
                    //清除之前的屏幕数据
                    restPoints();
                    //需要绘制按下的点
                    isDraw = true;
                    //修改状态
                    point.state = Point.STATE_PRESS;
                    //保存当前的点
                    pointList.add(point);
                    passList.add(pointList.size());
                    if(listener!=null){
                        listener.TouchEventStart();
                    }
                }
                postInvalidate();
                break;
            case MotionEvent.ACTION_MOVE:
                if(isDraw)
                {
                    if(point!=null && !pointList.contains(point))
                    {

                        point.state = Point.STATE_PRESS;
                        pointList.add(point);
                        passList.add(pointList.size();
                    }
                }
                postInvalidate();
                break;
                ...
            }   
    }

前面我们记录路径的位置,是通过路径的点来判断的,这个方法只能记录数量,并不能准确的记录点的顺序。所以我们需要对点增加标记位,记录点相对于View的位置,然后在每个滑动的时候获得不为空的点,就单独记录这个点位于整个View的位置。

首先修改实体类:

/**
 * 锁屏的格子
 * Created by Vicent on 2016/10/15.
 */

public class Point {
...
    public int index = -1;



    public Point(float x, float y, int index) {
        this.x = x;
        this.y = y;
        this.index = index;
    }

}

然后在初始化每个点的时候对每个点的位置进行标注:

//        第一排
         points.add(new Point(offsetx+space,offsety+space,0));
         points.add(new Point(offsetx+space*2,offsety+space,1));
         points.add(new Point(offsetx+space*3,offsety+space,2));
//        第二排
         points.add(new Point(offsetx+space,offsety+space*2,3));
         points.add(new Point(offsetx+space*2,offsety+space*2,4));
         points.add(new Point(offsetx+space*3,offsety+space*2,5));
//        第三排
         points.add(new Point(offsetx+space,offsety+space*3,6));
         points.add(new Point(offsetx+space*2,offsety+space*3,7));
         points.add(new Point(offsetx+space*3,offsety+space*3,8));

然后在记录路径的位置时,我们记录各个点的index值,来确保每个点在View的位置是唯一的,记录方法修改为:

passList.add(point.index);

接下来再来测试一下?

现在又可以开心得飞起来吧了!!

Android 10 自定义锁屏 手机自定义锁屏_锁屏_14

不要以为完了,这里还有一个大坑!就是我们对于事件拦截的用法测试的时候你会发现完全不起作用?这是为什么呢?

先看看我们这个方法的源码:

@Override
    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {

        if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
            // We're already in this state, assume our ancestors are too
            return;
        }

        if (disallowIntercept) {
            mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
        } else {
            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        }

        // Pass it up to our parent
        if (mParent != null) {
            mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
        }
    }

其实这个方法我也没有怎么看懂,反正主要就是对标志位FLAG_DISALLOW_INTERCEPT进行赋值,因为在事件分发机制的dispatchTouchEvent方法会检查该标记位,源码:

// Check for interception.
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            } else {
                // There are no touch targets and this action is not an initial down
                // so this view group continues to intercept touches.
                intercepted = true;
            }

这部分内容较多,主要是MotionEvent为ACTION_DOWN或者滑动事件没有被消费的情况下,会执行后面的方法,这里可以当做邮局(ViewGroup)收到邮件(手势)后,判断这个邮件是不是需要发往外地(拦截),如果需要就进入了方法内,这里就会检查标记位是否被设置(地址是不是本地的?),如果设置了的话就会判断是否可不发往外地(是否可拦截);没有设置的话直接发出去(不拦截)。

如果看了上面的还是没有看懂也没有关系,我就直接摘抄一段《Android开发艺术探索》的一部分内容:
147页:在子View设置FLAG_DISALLOW_INTERCEPT标记位后,ViewGroup将无法拦截除了ACTION_DOWN以外的其它事件,原因为ViewGroup在事件分发时,如果是 ACTION_DOWN就会重置FLAG_DISALLOW_INTERCEPT标记位;
160页,内部拦截法典型代码解释部分:除了子元素需要处理以外,父元素也要默认拦截除了ACTION_DOWN以外的其它事件,这样子当子元素调用getParent().requestDisallowInterceptTouchEvent(false)方法时,父元素才能拦截所需要的事件。

OK,这里已经解释了为什么这个getParent().requestDisallowInterceptTouchEvent方法在本例中失效且不适用的问题,因为我们不能在设置手势密码或者验证手势密码的时候不能拦截ACTION_MOVE等事件。如果还是不清楚的话,只有多看看上面提到的两处源码了!

既然这个方法不能用,我们也知道了事件分发机制(就是三个方法,dispatchTouchEvent方法,onInterceptTouchEvent方法和onTouchEvent方法,这里假装讲了)。那我们可以不在父布局的拦截和分发处动手,因为这两个方法是私有方法,需要自定义父布局,代码量太多了。我们直接在View的onTouchEvent方法来实现拦截,具体实现方法如下:

@Override
    public boolean onTouchEvent(MotionEvent event) {
        mouseX = event.getX();
        mouseY = event.getY();
        //判断是否拦截该事件
        //这里只会拦截ACTION_DOWN,原因是其它事件根本过不来直接被父布局拦截了
        if(!isInterceptTouchEvent){
            return isInterceptTouchEvent;
        }
        ......
    }

OK,现在终于把坑填满了!!

最后有一个疑问:如何实现滑动解锁的时候经历的这个点实现放大缩小的动画?求大神指教!!
最最后的一个疑问:如何通过设计模式,使得该demo思路更加清晰?比如说MVP。