android练习 九宫格解锁

文章目录

  • 思路
  • 样式实现
    • 实践 - 先画一个圆
    • 实践反馈
    • 初步实现样式
  • 实现响应
    • 如何自定义View的响应事件
    • 滑动坐标与不同的圆圈对应
  • 实现画线与画点效果
    • 效果
    • 实现方式
      • 逻辑
      • 代码实现
  • 最终效果与反馈
    • 不足

思路

主界面:
设置密码、验证密码、修改密码
fragment

实现方式,自定义View。
如何自定义View?自定义什么View?自定义View需要什么样子的功能?能否让它更加有可修改性?

参考资料:
Android 自定义控件实现九宫格解锁

样式实现

实践 - 先画一个圆

自定义View的样式实现
获取界面大小,获取中心坐标,获取每个半径的大小

    @Override
    protected void onDraw(Canvas canvas) {
//        super.onDraw(canvas);
        drawDefaultCircle(canvas);

    }

    private void drawDefaultCircle(Canvas canvas) {
        if (viewHeight == 0) { //获取view的长和宽。之后绘制圆形
            Log.d(TAG, "必须在onDraw中,才能获得view的大小");
            viewHeight = getMeasuredHeight();
            viewWidth = getMeasuredWidth();
        }
        float minLength = Math.min(viewHeight, viewWidth); //画正方形式的九宫格
        center_x = (getRight() - getLeft()) / 2;
        center_y = (getBottom() - getTop()) / 2; //获得绘制的中心点

        int pointNumber = getResources().getInteger(R.integer.point_number);
        int rowNumber = (int) Math.sqrt(pointNumber); //画几排?每排几个?因为是正方形所以是排数和行数相同
        circleRadius_default = minLength / rowNumber * 2 + rowNumber + 1; //2n个半径,每个圆相隔1个半径(带系统边界) = n+1

        canvas.drawCircle(center_x, center_y, 
        		circleRadius_default, paint_default); //先画一个看看
    }

android练习 九宫格解锁_第1张图片

实践反馈

  1. 画笔粗一点
  2. 为什么半径大小不对?
    算术优先级- -。 circleRadius_default = minLength / (rowNumber * 2 + rowNumber + 1);

初步实现样式

    private void init() {
        paint_default = new Paint(Paint.ANTI_ALIAS_FLAG);
        paint_default.setStyle(Paint.Style.STROKE);  //描边画笔
        paint_default.setColor(getContext().getColor(R.color.Color8328_1)); //画笔颜色
        paint_default.setStrokeWidth(5);
        //这个时候用getMeasuredHeight()是不是还不会显示的哇。因为View还没有初始化
        viewHeight = getMeasuredHeight();
        viewWidth = getMeasuredWidth();
    }

    @Override
    protected void onDraw(Canvas canvas) {
//        super.onDraw(canvas);
        drawDefaultCircle(canvas);

    }

    private void drawDefaultCircle(Canvas canvas) {
        if (viewHeight == 0) { //获取view的长和宽。之后绘制圆形
            Log.d(TAG, "必须在onDraw中,才能获得view的大小");
            viewHeight = getMeasuredHeight();
            viewWidth = getMeasuredWidth();
        }
        float minLength = Math.min(viewHeight, viewWidth); //画正方形式的九宫格
//        Log.d(TAG, "view的长度为:");
        center_x = (getRight() - getLeft()) / 2;
        center_y = (getBottom() - getTop()) / 2; //获得绘制的中心点

        int pointNumber = getResources().getInteger(R.integer.point_number);
        //画几排?每排几个?因为是正方形所以是排数和行数相同
        int rowNumber = (int) Math.sqrt(pointNumber); 
        Log.d(TAG, "行列数 " + rowNumber);
        //2n个半径,每个圆相隔1个半径(带系统边界) = n+1
        circleRadius_default = minLength / (rowNumber * 2 + rowNumber + 1); 

        for (int i = 0; i < rowNumber; i++) {
            //一排,修改y坐标
            float point_y = center_y - (1 - i) * 3 * circleRadius_default;
            for (int j = 0; j < rowNumber; j++) {
                //修改x坐标
                float point_x = center_x - (1 - j) * 3 * circleRadius_default;
                canvas.drawCircle(point_x, point_y, 
                		circleRadius_default, paint_default);
                Log.d(TAG, "坐标位置 x:" + point_x + " y:" + point_y);
            }
        }
    }

android练习 九宫格解锁_第2张图片

实现响应

如何自定义View的响应事件

给自定义View添加点击事件
重写onTouchEvent方法,在方法中可以获得当前触摸的位置和方式。模板代码如下


@Override
    public boolean onTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();
        int action = event.getAction();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                break;
            case MotionEvent.ACTION_MOVE: 
                break;
            case MotionEvent.ACTION_UP:
                if (x + getLeft() < getRight() && y + getTop() < getBottom()) {
                    mListener.onClick(this);
                    mViewClick.onClick(x - startX, y - startY);
                }
                break;
        }
        return true;
    }

Q&A
长按滑动的时候,是怎么时时刻刻获得位置的呢?
还记得之前做的一个项目里,按住屏幕滑动的话,会不断的触发事件的。事实上,只要手在屏幕上发生动作,就会触发这个方法,所以不必在意。

滑动坐标与不同的圆圈对应

定义实体类Point,让它进行绘制,判断是否发生状态的改变
程序的设计非常重要
我刚开始一直在犯错误,犯错误的问题就在于:
在什么情况下点会被选中,在什么情况下点会被清空

  1. 判断x,y在不在圆形内,每次在圆内,都代表圆的绘制情况需要改变。
    很明显,这是错的
    因为手会经过圆形范围,会多次被判断为在圆内

  2. 判断x,y上一次是不是在圆内,记录上一次的情况。如果这一次和上一次的情况不一样,说明选中的状态发生了改变。
    的确发生了改变,但是这是单选的情况才是这么看。比如从圆内移动到圆外。的确是选择状态发生了改变。可惜在这里,从圆内到圆外说明移动到下一个点,依然是选中状态

  3. 记录上一次的状态,只有当上一次不包含,到这一次包含,才说明状态的改变。情况如下:
    android练习 九宫格解锁_第3张图片
    但是这依然是错的- -。正确的事务逻辑不是这样的。工具就只是工具,即使你知道怎么挥动斧头,但是也不能说明你会劈材(或者劈人)。

实现画线与画点效果

效果

android练习 九宫格解锁_第4张图片

实现方式

逻辑

真的是很需要认真理解软件执行过程中会发生什么,这非常非常重要。
这个小练手项目逻辑还是有点复杂的,我之前就是犯了不考虑清楚逻辑直接上手的错误。不过也有对相关技能不熟练的原因,比如一开始我的确对怎么画出正确位置的圆有点懵。
android练习 九宫格解锁_第5张图片
上面是一个思考这个问题的简单过程,也是我最开始的思考草稿。后来在编写的时候,因为涉及到具体的实现,以及一些没有考虑清楚的点,所以增加了不少小细节。尽量在代码里说清楚

代码实现

学习怎么良好的编码,设计模式,类的职责,各种各样 plz
我尝试自己分了类的职责,但是没有一个指导方法,所以挺难。应该多去看看书啊,比如代码大全,比如计算机编程艺术之类的。哎,我好菜。。。
三个类,LockView类,myPoint类,和一个自定义的数据结构myPointList
分别对应了不同的职责。LockView主要用来画画;myPointList类大概就是和点列表相关的操作,比如获得坐标,获得序号,维护被选择的点等等;myPoint就是坐标和半径以及判断一个坐标是否在圆内。

稍微贴一下代码。myPointmyPointList的代码是很容易理解的。

package com.example.ninepatch.utils;

public class myPoint2 {
    private float f_x, f_y;
    private float f_r;

    public myPoint2(float f_x, float f_y, float f_r) {
        this.f_x = f_x;
        this.f_y = f_y;
        this.f_r = f_r; }

    public float getF_x() { return f_x; }

    public float getF_y() { return f_y; }

    public float getF_r() { return f_r; }

    public boolean isInside(float f_touchX, float f_touchY) {
        double distance = (Math.pow((f_touchX - this.f_x), 2) + 
        		Math.pow(f_touchY - this.f_y, 2)) - Math.pow(f_r, 2); //如果距离大于半径,则>0
        if (distance >= 0) {
            return false; //距离大于半径,就没有包含
        } else {
            return true;
        } }}

myPointList部分的代码主要做的事情

  1. 以一维数组的方式维护数据,然后提供方法返回数据的坐标位置
  2. 初始化数据
  3. 维护被选择的Point列表
  4. 满足逻辑中需要的一系列方法,比如:获得什么方向是退回的方向,返回所有被选择的点,移除尾点…等等
public class myPointList {
    private static final String TAG = "myPointList";
    private int size, rows;
    private myPoint2[] points; //点的集合
    private ArrayList<Integer> selectList = new ArrayList<>(); //已经被选择的list,用order序号存

    public myPointList(int size) {
        this.size = size;
        points = new myPoint2[this.size];
        rows = (int) Math.sqrt(size); }

    public  myPoint2 findByXY(int position_x, int position_y) {
        return points[position_x * rows + position_y]; }

    public myPoint2 findByOrder(int order) {
        return points[order]; }

    public int getXByOrder(int order) {
        return order - 3 * getYByOrder(order); }

    public int getYByOrder(int order) {
        return order / rows; }

    public int getOrderByXY(int x, int y) {
        return x * rows + y; }

    public void addSeclectPoint(int x, int y) {
        int order = getOrderByXY(x, y);
        selectList.add(order); }

    public void addSelectPoint(int order) {
        selectList.add(order); }

    public myPoint2 getLastSelectPoint() {
        return findByOrder(selectList.get(selectList.size() - 1)); }
    /**
     * 用该点的序号查看它是否已经被选择了。
     * 如果没有被选择,则重绘
     * 如果被选择,但是是最后一个,没有行为
     * 如果被选择,而是是次点,那就把尾点unSelect
     * 如果已经被选择,且不是尾点和次点,则结束行为。
     * @return
     */
    public int checkSelected(int x, int y) {
        int order = getOrderByXY(x, y);
        return checkSelected(order); }

    public int checkSelected(int order) {
        for (int i = 0; i < selectList.size(); i++) {
            if (selectList.get(i) == order) {
                if (i == selectList.size() - 1) {
                    return ViewConfig.LASTSELECT;
                } else if (i == selectList.size() - 2) {
                    return ViewConfig.LAST2SELECT;
                } else {
                    return ViewConfig.PASTSELECT;
                }
            }
        }
        return ViewConfig.NEWSELECT; }

    /**
     * 查看尾点和次点的相对关系,以便于绘制触摸点的连线。
     * @return
     */
    //返回的方向
    public int backDirection() {
        if (selectList.size() >= 2) {
            int lastX = getXByOrder(selectList.get(selectList.size() - 1));
            int lastY = getYByOrder(selectList.get(selectList.size() - 1));

            int last2X = getXByOrder(selectList.get(selectList.size() - 2));
            int last2Y = getYByOrder(selectList.get(selectList.size() - 2));

            if ((lastX - last2X) > 0) {
                //尾点在次点右边
                if ((lastY - last2Y) > 0) {
                    //尾点在次点下面
                    return ViewConfig.W2N;
                } else if (lastY == last2Y) {
                    //尾点和次点同行
                    return ViewConfig.WEST;
                } else {
                    //尾点在次点上方
                    return ViewConfig.S2W;
                }
            } else if (lastX == last2X) {
                //尾点和次点在同一列
                if ((lastY - last2Y) > 0) {
                    //尾点在次点下面
                    return ViewConfig.NORTH;
                } else if (lastY == last2Y) {
                    //尾点和次点同行
                    return Integer.MIN_VALUE; //尾点和次点是同一个,绝对有问题
                } else {
                    //尾点在次点上方
                    return ViewConfig.SOUTH;
                }
            } else {
                //尾点在次点左边
                if ((lastY - last2Y) > 0) {
                    //尾点在次点下面
                    return ViewConfig.N2E;
                } else if (lastY == last2Y) {
                    //尾点和次点同行
                    return ViewConfig.EAST;
                } else {
                    //尾点在次点上方
                    return ViewConfig.E2S;
                }
            }
        }
        else { // 只有一个点
            return Integer.MAX_VALUE;
        }
    }

    public void initPoints(float f_centerX, float f_centerY, float f_r) {
        for (int order = 0; order < size; order++) {
            int position_x = getXByOrder(order);
            int position_y = getYByOrder(order);
//             从0起
            float offset = (rows - 1) / 2;
            float f_x = f_centerX - (offset - position_x) * 3 * f_r;
            float f_y = f_centerY - (offset - position_y) * 3 * f_r;
            points[order] = new myPoint2(f_x, f_y, f_r);
        } }

    /**
     * 如果找到了包含的圆,就返回圆的序号order
     * 如果没有找到圆,就返回最小值
     * @param fx
     * @param fy
     * @return
     */
    public int findIncludePoint(float fx, float fy) {
        for (int order = 0; order < size; order++) {
            if (points[order].isInside(fx, fy)) {
                //如果包含
                return order; } }
        return Integer.MIN_VALUE; }

    public ArrayList<Integer> getSelectList() { return selectList; }

    public int getSize() { return size; }

    public void removeLastSelect() { selectList.remove(selectList.size() - 1); }
}

LockView的代码是相对复杂的,复杂主要在逻辑,所以不理清楚逻辑是没有办法编写的。
绘制的时候,也就是在复写的onDraw()方法中,涉及到重绘,所以需要考虑不同的情况。

  1. 第一种情况就是刚创建这个自定义View,还没有什么点击事件,这个时候我需要初始化一系列的数据,才能绘制。这些数据作为类的实体变量会一直在重绘的时候被使用,但是不需要重复赋值降低性能。
  2. 然后发生了点击事件的时候要怎么处理
  3. 松开手指的时候要怎么处理
  4. 如果画解锁手势的时候遇到了重复点事件要怎么处理。

而这一部分讲解的主要是touch事件
逻辑上就是先画圆形,然后根据当前确定的被选中的点来画圆
接着画线。画线的方式就是:点之间先连线,然后尾点和当前的touch点连线

    @Override
    protected void onDraw(Canvas canvas) {
        if (DrawEvent == EventNew) {
            f_viewHeight = getMeasuredHeight();
            f_viewWidth = getMeasuredWidth();
            float minLength = Math.min(f_viewHeight, f_viewWidth); //画正方形式的九宫格
            f_centerX = (getRight() - getLeft()) / 2;
            f_centerY = (getBottom() - getTop()) / 2; //获得绘制的中心点
            f_r = minLength / (rows * 2 + rows + 1); //获得半径的长度。2n个半径,每个圆相隔1个半径(带系统边界) = n+1
            points.initPoints(f_centerX, f_centerY, f_r); //初始化所有点

            draw_defaultCircle(canvas);
        }

        if (DrawEvent == EventTouch) {
            draw_defaultCircle(canvas);
            draw_fillPoint(canvas);
            draw_lines(canvas);
        }

        if (DrawEvent == EventCheck) {
        }

        if (DrawEvent == EventRepeat) {
        }
    }

但是怎么确定我的点究竟选没选中也是一个很大的问题。逻辑上有一些判断,比如我在尾点内沿着反方向滑动,应该是想要取消选择。这个东西涉及到移动的角度问题,分为8个方向,然后如果移动的方向是连线的方向,就取消选择当前尾点。
已知两点,求两点连线与水平线的夹角 - 这个文章里之所以需要一个负号,是因为android的象限和标准象限不同

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        f_touchX = event.getX();
        f_touchY= event.getY();
        int action = event.getAction();
        int order; //判断触摸点在哪个圆内

        switch (action) {
            case MotionEvent.ACTION_DOWN:
                DrawEvent = EventTouch;
                order = points.findIncludePoint(f_touchX, f_touchY);
                if (order != Integer.MIN_VALUE) {
                    //如果找到了
                    isLastInCircle = true;
                    points.addSelectPoint(order);
                    this.postInvalidate(); //重绘
                }
                break;
            case MotionEvent.ACTION_MOVE:
                Log.d(TAG, "onTouchEvent: action_move");
                DrawEvent = EventTouch;
                order = points.findIncludePoint(f_touchX, f_touchY);
                if (order != Integer.MIN_VALUE) { //如果触摸点在圆内
                    isLastInCircle = true;
                    int checkState = points.checkSelected(order); //判断是否曾经被选择
                    if (checkState == ViewConfig.NEWSELECT) { //发现未被选择
                        points.addSelectPoint(order);
                        this.postInvalidate();
                    } else if (checkState == ViewConfig.LASTSELECT) {//发现是尾点
                        //绘线即可
                        this.postInvalidate();
                    } else if (checkState == ViewConfig.LAST2SELECT){ //发现是次点,消除当前尾点
                        points.removeLastSelect();
                        this.postInvalidate();
                    }else if (checkState == ViewConfig.PASTSELECT) { //发现是非尾点,非次点
                        //结束这一次密码解锁/设置
                        //如果是设置密码,不允许这么设置。如果是check密码,就进行比较
                        if (setPassword) {
                            DrawEvent = EventRepeat;
                        } else {
                            DrawEvent = EventCheck;
                        }
                        this.postInvalidate();
                    }
                } else { //如果在圆外,要判断是否为取消选择尾点的路径
                    Log.d(TAG, "触摸点在圆外");
                    if (isLastInCircle) { //如果上一次是在圆中的,说明这是一次离开圆的行为,需要判断是否取消选择圆点。
                        isLastInCircle = false;
                        Log.d(TAG, "上一次触摸点在圆内");
                        int backDirection = points.backDirection(); //什么方向是取消选择的方向
                        Log.d(TAG, "取消选择的方向是:" + backDirection);
                        myPoint2 point1 = points.getLastSelectPoint();
                        float lastX = point1.getF_x(); //获得尾点的坐标
                        float lastY = point1.getF_y();
                        Log.d(TAG, "我的选择方向是: " + ViewConfig.getTowards(lastX, lastY, f_touchX, f_touchY));
                        if (ViewConfig.getTowards(lastX, lastY, f_touchX, f_touchY) == backDirection) {
                            //如果是返回方向
                            points.removeLastSelect();
                            this.postInvalidate();//重绘
                        } else {
                            //如果一直在圆外,不会取消选中的点
                            this.postInvalidate();
                        }
                    } else { //重绘线
                        this.postInvalidate();
                    }
                }

                break;
            case MotionEvent.ACTION_UP:
                DrawEvent = EventCheck;
                this.postInvalidate();
                break;
        }
        return true;
    }

最终效果与反馈

通过android 自定义View -传值,获取是否在设置密码,是否在检查密码,检查密码时候传递正确的密码等行为。然后设置重绘方法。实现最终效果如下
android练习 九宫格解锁_第6张图片

不足

  1. fragment怎么传值给activity???主动传,这里是写个方法,让activity去调用
  2. 各种类应该怎么耦合聚合?才能响应式的实现消息传递。比如我这里,我不是在设置密码起手之后,传密码给activity和fragment的。我其实是在跳转的时候获取密码的。响应式?
  3. 没有实现重新输入的能力
  4. 没有判断repeat的情况

你可能感兴趣的:(Android)