Android九宫格解锁自定义view

很早以前就想自己写一个九宫格解锁了,现在终于一鼓作气安排上了,难度在自定义view里面算是中等吧。主要是要有实现的思路,然后一步一步去实现你的想法,其中考验的就是你的算法能力。

演示效果:

Github代码地址

实现思路

1.创建一个Dot类存每个圆格的圆心坐标。
2.在onmeasure()方法中设置圆的半径,和9个圆的中心点。
3.在onTouchEvent()方法中,实时对当前触摸点和9个圆做碰撞检测,如果进入该圆,就将该圆序号存储起来。
4.在onDraw()方法中,绘制每个外圆环和实心圆。遍历存储的点绘制路径,以及更改圆的颜色。

核心点

在onTouch事件中,需要实时对触碰点和9个圆做碰撞检测,判定方法为触碰点到圆心之前的距离小于半径,即为经过该圆,需要用到Api有次方公式Math.pow()和开方公司Math.sqrt()。

主要代码实现(思路实现见注释)

/**
 * create by libo
 * create on 2020/7/24
 * description 九宫格解锁自定义view
 */
public class UnlockNineSquaresView extends View {
    /**
     * 记录9个点的坐标集合
     */
    private List<Dot> dots = new ArrayList<>();
    /**
     * 按照顺序记录需要连线的dot的序号,使用LinkedHashSet从而更满足有序不重复的特点
     */
    private LinkedHashSet<Integer> drawDots = new LinkedHashSet();
    private final int DOT_COUNT = 9;
    /* 自身宽度高度 */
    private int width;
    /**
     * 外圆环画笔
     */
    private Paint circlePaint;
    /**
     * 内实心圆画笔
     */
    private Paint innerDotPaint;
    /**
     * 内实心半透明画笔
     */
    private Paint transparentPaint;
    /** 连线画笔 */
    private Paint linePaint;
    /** 外圆半径 */
    private int outerCircleRadius;
    /** 内圆半径 */
    private int innerCircleRadius;
    private int innerTransRadius;
    /** 未选中颜色 */
    private int normalColor;
    /** 选中颜色 */
    private int checkedColor;
    /**
     * 每个单元个宽度
     */
    private int unitWidth;
    private Path linePath;
    private float curX, curY;
    /** 解锁密码数字字符串,默认密码123456 */
    private String password = "123456";
    private OnUnlockListener onUnlockListener;

    public UnlockNineSquaresView(Context context) {
        super(context);
        init();
    }

    public UnlockNineSquaresView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        width = MeasureSpec.getSize(widthMeasureSpec);

        unitWidth = width / 6;  //需要固定为width的1/6
        outerCircleRadius = width / 10;
        innerCircleRadius = width / 45;
        innerTransRadius = width / 30;

        initDotParams();

        setMeasuredDimension(width, width);
    }

    private void init() {
        initPaint();
    }

    private void initPaint() {

        normalColor = getResources().getColor(R.color.blue);
        checkedColor = getResources().getColor(R.color.deep_blue);

        circlePaint = new Paint();
        circlePaint.setColor(normalColor);
        circlePaint.setStyle(Paint.Style.STROKE);
        circlePaint.setStrokeWidth(3);
        circlePaint.setAntiAlias(true);

        innerDotPaint = new Paint();
        innerDotPaint.setColor(normalColor);
        innerDotPaint.setAntiAlias(true);

        transparentPaint = new Paint();
        transparentPaint.setColor(getResources().getColor(R.color.trans_blue));
        transparentPaint.setAntiAlias(true);

        linePaint = new Paint();
        linePaint.setColor(checkedColor);
        linePaint.setStyle(Paint.Style.STROKE);
        linePaint.setStrokeWidth(6);
        linePaint.setAntiAlias(true);
        linePath = new Path();
    }

    /**
     * 设置各个dot的位置
     */
    private void initDotParams() {
        drawDots.clear();
        dots.clear(); //重复调用onMeasure需要重置dots

        //根据行列值来设置当前横纵坐标 (j,i)
        for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 3; j++) {
                //i表行数,j表列数,当前为第i行j列位置
                int left = getLeft() + (j * 2 + 1) * unitWidth;
                int top = getTop() + (i * 2 + 1) * unitWidth;
                dots.add(new Dot(left, top));
            }
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
            case MotionEvent.ACTION_MOVE:
                collisionDetection(event.getX(), event.getY());
                break;
            case MotionEvent.ACTION_UP:
                resetState();
                break;
        }

        postInvalidate();
        return true;
    }

    /**
     * 实时与每个圆碰撞检测
     * @param curX 当前触摸x位置
     * @param curY 当前触摸y位置
     */
    private void collisionDetection(float curX, float curY) {
        if (drawDots.size() == password.length()) {  //输完密码结束碰撞检测
            return;
        }

        this.curX = curX;
        this.curY = curY;

        for (int i=0;i<dots.size();i++) {
            //遍历每个圆判断当前触摸点是否在某个圆之内
            double difX = Math.pow(dots.get(i).x - curX, 2);
            double difY = Math.pow(dots.get(i).y - curY, 2);
            if (Math.sqrt(difX + difY) <= outerCircleRadius) {
                //触摸点与圆心距离小于半径,即判断为触摸点在圆内

                if (!drawDots.contains(i)) { //避免重复添加
                    drawDots.add(i);
                }

                checkPsdCorrect();
            }
        }
    }

    /**
     * 校验密码是否正确
     */
    private void checkPsdCorrect() {
        if (drawDots.size() != password.length()) {
            return;
        }

        //输完密码,去验证密码是否正确
        StringBuilder stringBuilder = new StringBuilder();
        for (int num : drawDots) {
            stringBuilder.append(num+1);
        }

        if (stringBuilder.toString().equals(password)) { //stringBuilder.toString()为当前输入密码
            if (onUnlockListener != null) {
                onUnlockListener.unlockSuccess();
            }
        } else {
            if (onUnlockListener != null) {
                onUnlockListener.unlockFail();
                virate();
            }
        }
    }

    /**
     * 解锁失败调用一次震动
     */
    private void virate() {
        Vibrator vibrator = (Vibrator)getContext().getSystemService(Context.VIBRATOR_SERVICE);
        long [] pattern = {100,400,100,400};
        vibrator.vibrate(pattern,-1);
    }

    /**
     * 每次抬手需要重置最初状态,即未输密码状态
     */
    private void resetState() {
        drawDots.clear();  //选中圆清除
        linePath.reset();  //连线清除
    }

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

        circlePaint.setColor(getResources().getColor(R.color.blue));
        innerDotPaint.setColor(getResources().getColor(R.color.blue));
        for (int i = 0; i < DOT_COUNT; i++) {
            canvas.drawCircle(dots.get(i).x, dots.get(i).y, outerCircleRadius, circlePaint);  //画每个dot的外圆

            //画每个dot的内圆
            canvas.drawCircle(dots.get(i).x, dots.get(i).y, innerCircleRadius, innerDotPaint);  //画每个dot的内圆
        }

        //path移动到第一个点
        int curPos;
        if (drawDots.iterator().hasNext()) {
            curPos = drawDots.iterator().next();
            linePath.moveTo(dots.get(curPos).x, dots.get(curPos).y);
        }

        circlePaint.setColor(getResources().getColor(R.color.deep_blue));
        innerDotPaint.setColor(getResources().getColor(R.color.deep_blue));

        //对已经存储的点按照顺序连线
        for (Integer drawDot : drawDots) {
            linePath.lineTo(dots.get(drawDot).x, dots.get(drawDot).y);

            canvas.drawCircle(dots.get(drawDot).x, dots.get(drawDot).y, outerCircleRadius, circlePaint);  //画每个dot的外圆
            canvas.drawCircle(dots.get(drawDot).x, dots.get(drawDot).y, innerCircleRadius, innerDotPaint);  //画每个dot的内圆
            canvas.drawCircle(dots.get(drawDot).x, dots.get(drawDot).y, innerTransRadius, transparentPaint);  //画每个dot的透明圆
        }

        canvas.drawPath(linePath, linePaint);

    }

    public void setPassword(String password) {
        this.password = password;
    }

    /**
     * 每格圆形类
     */
    class Dot {
        private int x;
        private int y;

        public Dot(int x, int y) {
            this.x = x;
            this.y = y;
        }
    }

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

    interface OnUnlockListener {
        void unlockSuccess();

        void unlockFail();
    }
}

MainActivity调用:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        final TextView tvResult = findViewById(R.id.tv_show_result);
        TextView tvTips = findViewById(R.id.tv_tips);
        tvTips.setText("当前密码为Z字型");

        UnlockNineSquaresView unlockNineSquaresView = findViewById(R.id.unlockview);
        unlockNineSquaresView.setPassword("1235789");

        unlockNineSquaresView.setOnUnlockListener(new UnlockNineSquaresView.OnUnlockListener() {
            @Override
            public void unlockSuccess() {
                tvResult.setText("密码正确");
            }

            @Override
            public void unlockFail() {
                tvResult.setText("密码错误");
            }
        });
    }
}

后续优化点:

在今后的优化中,还是朝着选中状态的优化,密码输入检验更完善,密码校验成功变绿色,失败变红色。然后对代码进行封装,实现更多自定义属性方法使用,这些方面进行持续优化。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342