美文网首页Android进阶之路Android开发经验谈
Android自定义View--实现九宫格解锁图案

Android自定义View--实现九宫格解锁图案

作者: IT枫 | 来源:发表于2016-03-14 19:05 被阅读1459次

    源码下载

    项目中需求用到图案解锁的功能,就自己写了类似的功能:
    说下思路:

    • 1.实现一个子类继承View
    • 2.覆盖onDrow()函数,渲染图像
    • 3.覆盖onTouchEvent()函数
    • 4.监听按下、移动,松开手指的动作
    • 5.重新在onDrow()中渲染对应的的图像

    在描述功能之前,看一下效果图,理解起来会起到事半功倍的作用

    整体效果图.jpg

    说明

    • A、B、C、D、E、F、G、H、I代表九个坐标点
    • 左图中的圆由两个同心圆组成.
    • 中图链接起来的圆由四个同心圆组成,增加了两个绿色的圆,最外层绿色的是空心圆,红色连线是带有宽度的直线.
    • 右图线条由红色条变成了绿色.

    1.实现UnlockAppView类继承View

    实现左图:九个点的坐标,圆的半径及颜色。
    空心圆:同圆心不同半径,绘制颜色不同
    坐标如何确定:由屏幕的宽高决定,按照比例画出的效果图在各种屏幕中看起来协调.
    定义所需参数:

    //屏幕的宽度
    private int width;
    //屏幕的高度
    private int height;
    //大圆半径
    private float rH;
    //小圆半径
    private int rM;
    //A的坐标
    private float a1, b1;
    //B的坐标
    private float a2, b2;
    //C的坐标
    private float a3, b3;
    //D的坐标
    private float a4, b4;
    //E的坐标
    private float a5, b5;
    //F的坐标
    private float a6, b6;
    //G的坐标
    private float a7, b7;
    //H的坐标
    private float a8, b8;
    //I的坐标
    private float a9, b9;
    //绘制大圆用到的画笔
    private Paint mPaint;
    //绘制小圆用到的画笔
    private Paint mPaint0;
    

    参数命名完成,接下来开始赋值:

    DisplayMetrics metric = new DisplayMetrics();
    getWindowManager().getDefaultDisplay().getMetrics(metric);
    //获取屏幕的宽度
    width = metric.widthPixels;
    //获取屏幕的高度
    height = metric.heightPixels;
    //以下计算是根据屏幕调试出来的合理大小,不必深究
    //计算大圆的半径,
    rH = (width / 3) / 5;
    //计算小圆的半径,
    rM = (width / 3) / 10;
    //点A的横坐标,及纵坐标
    a1 = (width / 3) / 2;
    b1 = (width / 3) / 2 + (height - width) / 2;
    //B点坐标
    a2 = (width / 3) + (width / 3) / 2;
    b2 = b1;
    //C点坐标
    a3 = (width / 3) * 2 + (width / 3) / 2;
    b3 = b1;
    //D点坐标
    a4 = a1;
    b4 = (width / 3) + (width / 3) / 2 + (height - width) / 2;
    //E点坐标
    a5 = a2;
    b5 = b4;
    //F点坐标
    a6 = a3;
    b6 = b4;
    //G点坐标
    a7 = a1;
    b7 = (width / 3) * 2 + (width / 3) / 2 + (height - width) / 2;
    //H点坐标
    a8 = a5;
    b8 = b7;
    //I点坐标
    a9 = a6;
    b9 = b7;
    //使位图抗锯齿
    mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
    //颜色,浅灰色
    mPaint.setColor(Color.LTGRAY);
    //使位图抗锯齿
    mPaint0 = new Paint(Paint.ANTI_ALIAS_FLAG);
    //颜色,白色
    mPaint0 = new Paint(Paint.WHITE);
    

    一切准备就绪,重写onDrow()函数,重新渲染

    @Override
    protected void onDraw(Canvas canvas) {
    //每次绘制清空画布
    canvas.drawColor(Color.WHITE);
    
    //渲染大圆,圆心(a1,b1)半径rH,画笔mPaint
    canvas.drawCircle(a1, b1, rH, mPaint);
    canvas.drawCircle(a2, b2, rH, mPaint);
    canvas.drawCircle(a3, b3, rH, mPaint);
    canvas.drawCircle(a4, b4, rH, mPaint);
    canvas.drawCircle(a5, b5, rH, mPaint);
    canvas.drawCircle(a6, b6, rH, mPaint);
    canvas.drawCircle(a7, b7, rH, mPaint);
    canvas.drawCircle(a8, b8, rH, mPaint);
    canvas.drawCircle(a9, b9, rH, mPaint);
    //渲染小圆
    canvas.drawCircle(a1, b1, rM, mPaint0);
    canvas.drawCircle(a2, b2, rM, mPaint0);
    canvas.drawCircle(a3, b3, rM, mPaint0);
    canvas.drawCircle(a4, b4, rM, mPaint0);
    canvas.drawCircle(a5, b5, rM, mPaint0);
    canvas.drawCircle(a6, b6, rM, mPaint0);
    canvas.drawCircle(a7, b7, rM, mPaint0);
    canvas.drawCircle(a8, b8, rM, mPaint0);
    canvas.drawCircle(a9, b9, rM, mPaint0);
    }
    

    以上完成左图的渲染。

    实现中图的效果

    跟踪手指划过的痕迹
    轨迹是否是否经过圆的区域
    说明,这里圆的区域用圆的外切正方形的区域代替。
    矩形对象的contains()方法可判断轨迹经过园的区域。
    代码实例
    rt1.contains(tX, tY)

    定义园的外切正方形变量

    //九个正方形区域
    //左上角坐标(a1 - rH, b1 - rH)及右下角坐标(a1 + rH, b1 + rH)
    private RectF rt1 = 
    new RectF(a1 - rH, b1 - rH, a1 + rH, b1 + rH);
    private RectF rt2 = 
    new RectF(a2 - rH, b2 - rH, a2 + rH, b2 + rH);
    private RectF rt3 = new RectF(a3 - rH, b3 - rH, a3 + rH, b3 + rH);
    private RectF rt4 = new RectF(a4 - rH, b4 - rH, a4 + rH, b4 + rH);
    private RectF rt5 = new RectF(a5 - rH, b5 - rH, a5 + rH, b5 + rH);
    private RectF rt6 = new RectF(a6 - rH, b6 - rH, a6 + rH, b6 + rH);
    private RectF rt7 = new RectF(a7 - rH, b7 - rH, a7 + rH, b7 + rH);
    private RectF rt8 = new RectF(a8 - rH, b8 - rH, a8 + rH, b8 + rH);
    private RectF rt9 = new RectF(a9 - rH, b9 - rH, a9 + rH, b9 + rH);
    

    使用invalidate()方法,刷新整个画布。
    所以需要记录轨迹经过A、B、C、D、E、F、G、H、I九个点经过的先后顺序。

    定义一个String变量passwordValue存储经过坐标点的先后顺序;
    两圆之间的红色线段,passwordValue记录着经过的圆的先后顺利,根据经过圆的先后顺利绘制线段;
    例如:passwordValue ="ACDE"代表经过的圆的顺序圆A->圆C->圆D->圆E,绘制的线段AC、CD、DE;
    线段是由起始坐标,终止坐标表示,所以需要定义一个两行两列的二维数组用来存储起始及终止坐标,第一行代表起始坐标,第二行代表终点坐标;
    由于每次刷新整个画布,需要把二维数据存储在一个列表中,方便遍历渲染;

    圆与线段的渲染分开来讲解,先来看看圆的渲染过程,获取手指滑动坐标,重写onTouchEvent方法

    @Override
    public boolean onTouchEvent(MotionEvent event)
    //捕捉按下的动作
    if (event.getAction() == MotionEvent.ACTION_DOWN) {
    
    } else if (event.getAction() == MotionEvent.ACTION_MOVE) {
        //X坐标点
        float tX = event.getX();
        //Y坐标点
        float tY = event.getY();
        //首次经过圆A
        if (rt1.contains(tX, tY) && !passwordValue.contains("A")) {
             passwordValue += "A";
        } else if (rt2.contains(tX, tY) && !passwordValue.contains("B")) {//首次经过圆B
             passwordValue += "B";
        } else if (rt3.contains(tX, tY) && !passwordValue.contains("C")) {//首次经过圆C
             passwordValue += "C";
        } else if (rt4.contains(tX, tY) && !passwordValue.contains("D")) {//首次经过圆D
             passwordValue += "D";
        } else if (rt5.contains(tX, tY) && !passwordValue.contains("E")) {//首次经过圆E
             passwordValue += "E";
        } else if (rt6.contains(tX, tY) && !passwordValue.contains("F")) {//首次经过圆F
             passwordValue += "F";
        } else if (rt7.contains(tX, tY) && !passwordValue.contains("G")) {//首次经过圆G
            passwordValue += "G";
        } else if (rt8.contains(tX, tY) && !passwordValue.contains("H")) {//首次经过圆H
            passwordValue += "H";
        } else if (rt9.contains(tX, tY) && !passwordValue.contains("I")) {//首次经过圆I
            passwordValue += "I";
        }
        invalidate();// 刷新画布,回调onDraw()方法
    }  else if (event.getAction() == MotionEvent.ACTION_UP)  {
            
    }
    

    确定了圆的顺序,刷新画布,渲染轨迹坐标经过的圆的效果及红色直线的效果

    protected void onDraw(Canvas canvas) {
    ...
    ...
    //轨迹经过圆A
    if (passwordValue.contains("A")) {// (a1,b1)
         canvas.drawCircle(a1, b1, rL, mPaintOKM);
         canvas.drawCircle(a1, b1, rH, mPaintOKH);
    }
    //轨迹经过圆B
    if (passwordValue.contains("B")) {// (a2,b2)
         canvas.drawCircle(a2,b2, rL, mPaintOKM);
         canvas.drawCircle(a2,b2, rH, mPaintOKH);
    }
    //轨迹经过圆C
    if (passwordValue.contains("C")) {// (a3,b3)
         canvas.drawCircle(a3,b3, rL, mPaintOKM);
         canvas.drawCircle(a3,b3, rH, mPaintOKH);
    }
    //轨迹经过圆D
    if (passwordValue.contains("D")) {// (a4,b4)
         canvas.drawCircle(a4,b4, rL, mPaintOKM);
         canvas.drawCircle(a4,b4, rH, mPaintOKH);
    }
    //轨迹经过圆E
    if (passwordValue.contains("E")) {// (a5,b5)
         canvas.drawCircle(a5,b5, rL, mPaintOKM);
         canvas.drawCircle(a5,b5, rH, mPaintOKH);
    }
    //轨迹经过圆F
    if (passwordValue.contains("F")) {// (a6,b6)
         canvas.drawCircle(a6,b6, rL, mPaintOKM);
         canvas.drawCircle(a6,b6, rH, mPaintOKH);
    }
    //轨迹经过圆G
    if (passwordValue.contains("G")) {// (a7,b7)
         canvas.drawCircle(a7,b7, rL, mPaintOKM);
         canvas.drawCircle(a7,b7, rH, mPaintOKH);
    }
    //轨迹经过圆H
    if (passwordValue.contains("H")) {// (a8,b8)
         canvas.drawCircle(a8,b8, rL, mPaintOKM);
         canvas.drawCircle(a8,b8, rH, mPaintOKH);
    }
    //轨迹经过圆I
    if (passwordValue.contains("I")) {// (a9,b9)
         canvas.drawCircle(a9,b9, rL, mPaintOKM);
         canvas.drawCircle(a9,b9, rH, mPaintOKH);
    }
    

    线段的渲染过程,获取线段的端点坐标,重写onTouchEvent方法

    //存储线段起始及终止坐标的二维数组
    float[][] lineCoordinate = new float[2][2]
    //存储二维数据的列表
    List<Float[][]> listCoordinate = new ArrayList();
    //经过圆的数量,num < 4 线段颜色为红色 num >= 4线段颜色为绿色
    int num = 0;
    
    @Override
    public boolean onTouchEvent(MotionEvent event) 
    if (event.getAction() == MotionEvent.ACTION_DOWN) {
    
    } else if (event.getAction() == MotionEvent.ACTION_MOVE) {
       //X坐标点
        float tX = event.getX();
        //Y坐标点
        float tY = event.getY();
        //首次经过圆A
        if (rt1.contains(tX, tY) && !passwordValue.contains("A")) {
             passwordValue += "A";
             //num经过的圆的数量
             //num != 0代表不是第一个经过的圆,第一个经过的圆只能是线段的起始坐标不能是线段的终止坐标
             if (num != 0) {      
                //线段的终止坐标  
                 fts[1] = new float[]{a1, b1};    
                //存储线段起及始终止坐标的二维数组存储到列表中
                listCoordinate.add(fts);
             }
            //初始化存储线段坐标的二维数组
             fts = new float[2][2];
            //线段的起始坐标
            fts[0] = new float[]{a1, b1};
             num += 1;
        } else if (rt2.contains(tX, tY) && !passwordValue.contains("B")) {//首次经过圆B
             passwordValue += "B";
             if (num != 0) {      
                //线段的终止坐标  
                 fts[1] = new float[]{a2, b2};    
                 listCoordinate.add(fts);
             }
            //初始化存储线段坐标的二维数组
             fts = new float[2][2];
            //线段的起始坐标
            fts[0] = new float[]{a2, b2};
            num += 1;
        } else if (rt3.contains(tX, tY) && !passwordValue.contains("C")) {//首次经过圆C
             passwordValue += "C";
             if (num != 0) {      
                //线段的终止坐标  
                 fts[1] = new float[]{a3, b3};    
                 listCoordinate.add(fts);
             }
            //初始化存储线段坐标的二维数组
             fts = new float[2][2];
            //线段的起始坐标
            fts[0] = new float[]{a3, b3};
            num += 1;
        } else if (rt4.contains(tX, tY) && !passwordValue.contains("D")) {//首次经过圆D
             passwordValue += "D";
             if (num != 0) {      
                //线段的终止坐标  
                 fts[1] = new float[]{a4, b4};    
                 listCoordinate.add(fts);
             }
            //初始化存储线段坐标的二维数组
             fts = new float[2][2];
            //线段的起始坐标
            fts[0] = new float[]{a4, b4};
            num += 1;
        } else if (rt5.contains(tX, tY) && !passwordValue.contains("E")) {//首次经过圆E
             passwordValue += "E";
             if (num != 0) {      
                //线段的终止坐标  
                 fts[1] = new float[]{a5, b5};    
                 listCoordinate.add(fts);
             }
            //初始化存储线段坐标的二维数组
             fts = new float[2][2];
            //线段的起始坐标
            fts[0] = new float[]{a5, b5};
            num += 1;
        } else if (rt6.contains(tX, tY) && !passwordValue.contains("F")) {//首次经过圆F
             passwordValue += "F";
             if (num != 0) {      
                //线段的终止坐标  
                 fts[1] = new float[]{a6, b6};    
                 listCoordinate.add(fts);
             }
            //初始化存储线段坐标的二维数组
             fts = new float[2][2];
            //线段的起始坐标
            fts[0] = new float[]{a6, b6};
            num += 1;
        } else if (rt7.contains(tX, tY) && !passwordValue.contains("G")) {//首次经过圆G
            passwordValue += "G";
             if (num != 0) {      
                //线段的终止坐标  
                 fts[1] = new float[]{a7, b7};    
                 listCoordinate.add(fts);
             }
            //初始化存储线段坐标的二维数组
             fts = new float[2][2];
            //线段的起始坐标
            fts[0] = new float[]{a7, b7};
            num += 1;
        } else if (rt8.contains(tX, tY) && !passwordValue.contains("H")) {//首次经过圆H
            passwordValue += "H";
             if (num != 0) {      
                //线段的终止坐标  
                 fts[1] = new float[]{a8, b8};    
                 listCoordinate.add(fts);
             }
            //初始化存储线段坐标的二维数组
             fts = new float[2][2];
            //线段的起始坐标
            fts[0] = new float[]{a8, b8};
            num += 1;
        } else if (rt9.contains(tX, tY) && !passwordValue.contains("I")) {//首次经过圆I
            passwordValue += "I";
             if (num != 0) {      
                //线段的终止坐标  
                 fts[1] = new float[]{a9, b9};    
                 listCoordinate.add(fts);
             }
            //初始化存储线段坐标的二维数组
             fts = new float[2][2];
            //线段的起始坐标
            fts[0] = new float[]{a9, b9};
            num += 1;
        }
        invalidate();// 刷新画布,回调onDraw()方法
    } else if (event.getAction() == MotionEvent.ACTION_UP) {
    
    }
    

    刷新画布,渲染红色线段,

    //初始化渲染红色线段画笔
    //Paint.ANTI_ALIAS_FLAG使图像抗锯齿
    mPaintCancelM = new Paint(Paint.ANTI_ALIAS_FLAG)
    //颜色红色
    mPaintCancelM.setColor(Color.RED);
    //画笔的宽度
    mPaintCancelM.setStrokeWidth(rM);
    
    protected void onDraw(Canvas canvas) {
    ...
    ...
    for (int i = 0; i < listCoordinate.size(); i++) {
          float[][] lineCoordinate = listCoordinate.get(i);
          float startX = lineCoordinate[0][0];
          float startY = lineCoordinate[0][1];
          float stopX = lineCoordinate[1][0];
          float stopY = lineCoordinate[1][1];
         //渲染红色线段
         canvas.drawLine(startX, startY, stopX, stopY, mPaintCancelM)
    }
    ...
    ...
    

    右图跟中图的渲染过程一样,区别在于经过的圆的数量大于等于4,画笔的颜色设置成绿色

    至此,以上左中右图的渲染实现过程完毕,但还有两个中间状态

    不完整的线段效果图.jpg

    右图跟左图的渲染过程一样,讲解左图的实现过程,我们称该状态线段为不完整线段,以区分之前的线段。

    手指滑动未到达圆所在的区域时,线段的起始坐标是轨迹经过的最后一个圆的圆心坐标,我们只需记录终点坐标就可实现以上图中的状态。

    //存储不完整线段起始终止坐标的二维数组
    float[][] lineCrdinateImperfect = new float[2][2]
    lineCrdinateImperfect[0] = new float[2];
    lineCrdinateImperfect[1] = new float[2];
    @Override
    public boolean onTouchEvent(MotionEvent event) {    
    if (event.getAction() == MotionEvent.ACTION_DOWN) {
    
    } else if (event.getAction() == MotionEvent.ACTION_MOVE) {
           float tX = event.getX();
           float tY = event.getY();
           //不完整线段终点坐标赋值
           lineCrdinateImperfect[1] [0] = tX;
           lineCrdinateImperfect[1] [1] = tY;
       //首次经过圆A
        if (rt1.contains(tX, tY) && !passwordValue.contains("A")) {
             passwordValue += "A";
           //不完整线段起始坐标赋值
           lineCrdinateImperfect[0] [0] = a1;
           lineCrdinateImperfect[0] [1] = b1;
        } else if (rt2.contains(tX, tY) && !passwordValue.contains("B")) {//首次经过圆B
             passwordValue += "B";
           //不完整线段起始坐标赋值
           lineCrdinateImperfect[0] [0] = a2;
           lineCrdinateImperfect[0] [1] = b2;
        } else if (rt3.contains(tX, tY) && !passwordValue.contains("C")) {//首次经过圆C
             passwordValue += "C";
           //不完整线段起始坐标赋值
           lineCrdinateImperfect[0] [0] = a3;
           lineCrdinateImperfect[0] [1] = b3;
        } else if (rt4.contains(tX, tY) && !passwordValue.contains("D")) {//首次经过圆D
             passwordValue += "D";
           //不完整线段起始坐标赋值
           lineCrdinateImperfect[0] [0] = a4;
           lineCrdinateImperfect[0] [1] = b4;
        } else if (rt5.contains(tX, tY) && !passwordValue.contains("E")) {//首次经过圆E
             passwordValue += "E";
           //不完整线段起始坐标赋值
           lineCrdinateImperfect[0] [0] = a5;
           lineCrdinateImperfect[0] [1] = b5;
        } else if (rt6.contains(tX, tY) && !passwordValue.contains("F")) {//首次经过圆F
             passwordValue += "F";
           //不完整线段起始坐标赋值
           lineCrdinateImperfect[0] [0] = a6;
           lineCrdinateImperfect[0] [1] = b6;
        } else if (rt7.contains(tX, tY) && !passwordValue.contains("G")) {//首次经过圆G
            passwordValue += "G";
           //不完整线段起始坐标赋值
           lineCrdinateImperfect[0] [0] = a7;
           lineCrdinateImperfect[0] [1] = b7;
        } else if (rt8.contains(tX, tY) && !passwordValue.contains("H")) {//首次经过圆H
            passwordValue += "H";
           //不完整线段起始坐标赋值
           lineCrdinateImperfect[0] [0] = a8;
           lineCrdinateImperfect[0] [1] = b8;
        } else if (rt9.contains(tX, tY) && !passwordValue.contains("I")) {//首次经过圆I
            passwordValue += "I";
           //不完整线段起始坐标赋值
           lineCrdinateImperfect[0] [0] = a9;
           lineCrdinateImperfect[0] [1] = b9;
        }
        invalidate();// 刷新画布,回调onDraw()方法
    } else if (event.getAction() == MotionEvent.ACTION_UP) {
    
    }
    

    刷新画布,渲染不完整线段

    @Override
    protected void onDraw(Canvas canvas) {
    ...
    ...
          //不完整线段坐标赋值
          float startXImperfect = lineCrdinateImperfect[0][0];
          float startYImperfect = lineCrdinateImperfect[0][1];
          float stopXImperfect= lineCrdinateImperfect[1][0];
          float stopYImperfect = lineCrdinateImperfect[1][1];
          //渲染不完整直线
          canvas.drawLine(startXImperfect, startYImperfect, stopXImperfect, stopYImperfect, mPaintCancelM);
    ...
    ...
    }
    

    注意:从一个圆(A)出发,绕过一个圆(B),到达圆另一个圆(C),这样会忽略中间的圆(B),经过的圆的顺序A->C,这样不合理,明明经过了中间圆(B),轨迹应该是A->B->C才对。
    解决思路:计算两圆心坐标中点坐标是否为其他圆的圆心坐标。

    相关文章

      网友评论

        本文标题:Android自定义View--实现九宫格解锁图案

        本文链接:https://www.haomeiwen.com/subject/rnvflttx.html