效果展示

android自定义View——仿九宫格解锁_i++

思路分析

这个一看是属于 交互型自定义控件,所以得按下面这个套路来:

  1. 先画出没有交互时的默认状态
  2. 处理onTouchEvent()的 DOWN 事件,绘制按下时的状态
  3. 处理onTouchEvent()的 MOVE 事件, 绘制手指移动时的状态
  4. 圆画完了之后就是处理画线和画箭头问题了
  5. 最后一点就是手指在移动的过程中与最后一个点的连线
代码编写

一、自定义控件java类

package com.wust.nineview;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

import androidx.annotation.Nullable;

import java.util.ArrayList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;

/**
 * ClassName: selfNineView <br/>
 * Description: <br/>
 * date: 2021/5/26 9:34<br/>
 *
 * @author yiqi<br />
 * @email:1820762465@qq.com
 * @QQ:1820762465
 * @since JDK 1.8
 */
public class selfNineView extends View {

    private boolean initFlag = false;
    private List<myPoint> mPointData;
    private List<myPoint> mSelectData;
    private float downPointX;
    private float downPointY;
    private float outCircleR;
    private float inCircleR;
    private Paint normalPaint;
    private int normalColor = Color.parseColor("#505450");

    private Paint downPaint;
    private int downColor = Color.parseColor("#00ff00");

    private Paint errorPaint;
    private int errorColor = Color.parseColor("#ff0000");

    private Paint linePressPaint;
    private float arrowHeight = 20;
    private int angle = 30;

    private onTouchResult mOnTouchResult;

    private boolean isWrongFlag = false;
    private boolean isPressFlag = false;

    public selfNineView(Context context) {
        this(context, null);
    }

    public selfNineView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public selfNineView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mSelectData = new ArrayList<>();
        normalPaint = initPaint(normalColor);
        downPaint = initPaint(downColor);
        errorPaint = initPaint(errorColor);
        linePressPaint = initPaint(downColor);
    }

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

    @Override
    protected void onDraw(Canvas canvas) {
        if (!initFlag) {
            initCell();
            initFlag = true;
        }
        drawCircle(canvas);
        drawLine(canvas);
    }

    //绘制两个点之间的连线以及箭头
    private void drawLine(Canvas canvas) {
        if (mSelectData != null && mSelectData.size() != 0) {
//            System.out.println(mSelectData.size());
            myPoint lastPoint = mSelectData.get(0);

            if (isWrongFlag){
                linePressPaint.setColor(errorColor);
            }else {
                linePressPaint.setColor(downColor);
            }

            for (int i = 1; i < mSelectData.size(); i++) {
                myPoint curPoint = mSelectData.get(i);
                float d = (float) twoPointDistace(lastPoint.x, lastPoint.y, curPoint.x, curPoint.y);
                float cos_a = (curPoint.x - lastPoint.x) * 1.0f / d;
                float sin_a = (curPoint.y - lastPoint.y) * 1.0f / d;
                canvas.drawLine(lastPoint.x + inCircleR * cos_a, lastPoint.y + inCircleR * sin_a,
                        curPoint.x - inCircleR * cos_a, curPoint.y - inCircleR * sin_a, linePressPaint);

                //绘制箭头
                drawArrow(lastPoint.x, lastPoint.y, d, cos_a, sin_a, canvas);

                lastPoint = curPoint;
            }

            if (isPressFlag){
                //绘制射线
                float d = (float) twoPointDistace(lastPoint.x, lastPoint.y, downPointX, downPointY);
                float cos_a = (downPointX - lastPoint.x) * 1.0f / d;
                float sin_a = (downPointY - lastPoint.y) * 1.0f / d;
                canvas.drawLine(lastPoint.x + inCircleR * cos_a, lastPoint.y + inCircleR * sin_a,
                        downPointX, downPointY, linePressPaint);
            }
        }
    }

    private void drawArrow(float startX, float startY, float d, float cos_a, float sin_a, Canvas canvas) {
        float l = (float) (arrowHeight * (Math.tan(Math.toRadians(angle))));
        float x0 = startX + (d - outCircleR - arrowHeight) * cos_a;
        float y0 = startY + (d - outCircleR - arrowHeight) * sin_a;
        float x1 = startX + (d - outCircleR) * cos_a;
        float y1 = startY + (d - outCircleR) * sin_a;
        float x2 = x0 + l * sin_a;
        float y2 = y0 - l * cos_a;
        float x3 = x0 - l * sin_a;
        float y3 = y0 + l * cos_a;
        Path path = new Path();
        path.moveTo(x1, y1);
        path.lineTo(x2, y2);
        path.lineTo(x3, y3);
        path.close();
        linePressPaint.setStyle(Paint.Style.FILL);
        canvas.drawPath(path, linePressPaint);
    }

    private double twoPointDistace(float startX, float startY, float endX, float endY) {
        return Math.sqrt(Math.pow(startX - endX, 2) + Math.pow(startY - endY, 2));
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        downPointX = event.getX();
        downPointY = event.getY();
        System.out.println(event.getAction());
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                isPressFlag = true;
                for (int i = 0; i < mPointData.size(); i++) {
                    myPoint currentPoint = mPointData.get(i);
                    if (checkIsInCircle(downPointX, downPointY, currentPoint.x, currentPoint.y, outCircleR)) {
                        //记录按下的那个点,等下要以密码的形式返回出去
                        mSelectData.add(currentPoint);
                        currentPoint.state = myPoint.STATE_DOWN;
                    }
                }
                invalidate();
                break;
            case MotionEvent.ACTION_MOVE:
                for (int i = 0; i < mPointData.size(); i++) {
                    myPoint currentPoint = mPointData.get(i);
                    if (checkIsInCircle(downPointX, downPointY, currentPoint.x, currentPoint.y, outCircleR)) {
                        //防止反复记下重复点
                        if (!mSelectData.contains(currentPoint)) {
                            mSelectData.add(currentPoint);
                            currentPoint.state = myPoint.STATE_DOWN;
                        }
                    }
                }
                invalidate();
                break;
            case MotionEvent.ACTION_UP:
                isPressFlag = false;
                if (mSelectData != null && mSelectData.size() != 0) {
                    //抬起的时候,密码回调
                    StringBuilder stringBuilder = new StringBuilder();
                    for (int i = 0; i < mSelectData.size(); i++) {
                        stringBuilder.append(mSelectData.get(i).i);
                    }
//                System.out.println("stringBuilder.toString() ->" + stringBuilder.toString());
                    mOnTouchResult.result(stringBuilder.toString());
                }
                //清除状态
                postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        clearState();
                        invalidate();
                    }
                },1000);
                break;
        }

        return true;
    }


    private void clearState() {
        isWrongFlag = false;
        mSelectData.clear();
        for (int i = 0; i < mPointData.size(); i++) {
            mPointData.get(i).state = myPoint.STATE_NORMAL;
        }
    }

    //检查是否在圈内
    private boolean checkIsInCircle(float downPointX, float downPointY, float centerX, float centerY, float R) {
        return Math.sqrt(Math.pow(downPointX - centerX, 2) + Math.pow(downPointY - centerY, 2)) < R;
    }

    //初始化画笔
    private Paint initPaint(int color) {
        Paint paint = new Paint();
        paint.setDither(true);
        paint.setAntiAlias(true);
        paint.setStyle(Paint.Style.STROKE);
        paint.setColor(color);
        paint.setStrokeWidth(5);
        return paint;
    }

    //画圈
    private void drawCircle(Canvas canvas) {
        System.out.println("drawCircle mSelectData ->" + mSelectData);
        System.out.println("drawCircle mPointData ->" + mPointData);
        for (int i = 0; i < mPointData.size(); i++) {
            if (mPointData.get(i).state == myPoint.STATE_NORMAL) {
                canvas.drawCircle(mPointData.get(i).x, mPointData.get(i).y, outCircleR, normalPaint);
                canvas.drawCircle(mPointData.get(i).x, mPointData.get(i).y, inCircleR, normalPaint);
            } else if (mPointData.get(i).state == myPoint.STATE_DOWN) {
                canvas.drawCircle(mPointData.get(i).x, mPointData.get(i).y, outCircleR, downPaint);
                canvas.drawCircle(mPointData.get(i).x, mPointData.get(i).y, inCircleR, downPaint);
            }else if (mPointData.get(i).state == myPoint.STATE_ERROR) {
                canvas.drawCircle(mPointData.get(i).x, mPointData.get(i).y, outCircleR, errorPaint);
                canvas.drawCircle(mPointData.get(i).x, mPointData.get(i).y, inCircleR, errorPaint);
            }
        }
    }

    //初始化九个单元格
    private void initCell() {
        mPointData = new ArrayList<>();
        //获取布局宽高
        int width = getWidth();
        int height = getHeight();
        //第一个点的位置
        float possionX = 0;
        float possionY = 0;

        //横竖屏兼容,计算第一个点的坐标
        if (width < height) {
            possionX = width * 1.0f / 6;
            possionY = (height - width) * 1.0f / 2 + width * 1.0f / 6;
            height = width;
        } else {
            possionX = (width - height) * 1.0f / 2 + height * 1.0f / 6;
            possionY = height * 1.0f / 6;
            width = height;
        }

        //设置内外圆的半径
        outCircleR = width * 1.0f / 12;
        inCircleR = width * 1.0f / 50;

        //循环产生九个点
        int n = 1;
        for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 3; j++) {
                mPointData.add(new myPoint(possionX + j * width * 1.0f / 3, possionY + i * height * 1.0f / 3, n++));
            }
        }
    }

    public void setOnTouchResultListener(onTouchResult onTouchResult) {
        this.mOnTouchResult = onTouchResult;
    }

    public interface onTouchResult {
        void result(String s);
    }

    public void pwdError() {
        isWrongFlag = true;
        for (int i = 0; i < mSelectData.size(); i++) {
            mSelectData.get(i).state = myPoint.STATE_ERROR;
        }
        System.out.println("mSelectData ->" + mSelectData);
        invalidate();
    }
}

//创建一个点类 里面主要存储 点的位置和序号 这里为了减少代码量 我就不用 get 和 set 方法了
class myPoint {
    public float x;
    public float y;
    public int i;

    public static final int STATE_NORMAL = 0;
    public static final int STATE_DOWN = 1;
    public static final int STATE_ERROR = 2;
    public int state = STATE_NORMAL;

    public myPoint(float x, float y, int i) {
        this.x = x;
        this.y = y;
        this.i = i;
    }

    @Override
    public String toString() {
        return "myPoint{" +
                "x=" + x +
                ", y=" + y +
                ", i=" + i +
                ", state=" + state +
                '}';
    }
}

二、编写xml文件

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.wust.nineview.selfNineView
        android:id="@+id/snv_selfnineview"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</LinearLayout>

三、调用

package com.wust.nineview;

import androidx.appcompat.app.AppCompatActivity;

import android.content.IntentFilter;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {

    private selfNineView snv_selfnineview;
    private final static String CORRECT_PWD = "13579";
    private Toast mToast;

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

        snv_selfnineview = findViewById(R.id.snv_selfnineview);
        snv_selfnineview.setOnTouchResultListener(new selfNineView.onTouchResult() {
            @Override
            public void result(String s) {
                if (CORRECT_PWD.equals(s)){
                    showToast("密码输入正确");
                }else {
                    showToast("密码输入错误,请重试");
                    snv_selfnineview.pwdError();
                }
            }
        });
    }

    private void showToast(String text) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                if (mToast == null){
                    mToast = Toast.makeText(MainActivity.this,null,Toast.LENGTH_SHORT);
                }
                mToast.setText(text);
                mToast.show();
            }
        });
    }
}
代码分析

在这个自定义View里我觉得我印象最深的是这么两点。

1、箭头怎么画,那个画箭头的函数怎么写出来的?

android自定义View——仿九宫格解锁_java_02

解:大家可以结合这张图对照着我写的函数进行分析。

        2、引用对象的赋值是引用的拷贝,即地址的拷贝,改变任意一个引用对象的值,都是在改变原型值。

解:下面这个函数改变的是 mSelectData 列表中 myPoint 引用对象的值,你会发现,我们绘制的时候是用 mPointData 列表中的 myPoint 引用对象画的,说明这两个引用变量指向的是同一块内存地址,任意一方对该地址内容进行修改,都会导致其内容变化。参考文章 java中调用方法传值问题详解

public void pwdError() {
        isWrongFlag = true;
        for (int i = 0; i < mSelectData.size(); i++) {
            mSelectData.get(i).state = myPoint.STATE_ERROR;
        }
        System.out.println("mSelectData ->" + mSelectData);
        invalidate();
    }
总结

纸上得来终觉浅,绝知此事要躬行。最后,还是希望大家根据本人的思路自己实现一遍,自己动手做出来,才是属于自己的。