Android 自定义 View——手势密码

    Android 自定义 View 当然是十分重要的,笔者这两天写了一个自定义 View 的手势密码,和大家分享分享:


    首先,我们来创建一个表示点的类,Point.java:

public class Point {

    // 点的三种状态
    public static final int POINT_STATUS_NORMAL = 0;
    public static final int POINT_STATUS_CLICK = 1;
    public static final int POINT_STATUS_ERROR = 2;

    // 默认状态
    public int state = POINT_STATUS_NORMAL;

    // 点的坐标
    public float mX;
    public float mY;

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

    // 获取两个点的距离
    public float getInstance(Point a){
        return (float) Math.sqrt((mX-a.mX)*(mX-a.mX)+(mY-a.mY)*(mY-a.mY));
    }

}

    然后我们创建一个 HandleLock.java 继承自 View,并重写其三种构造方法(不重写带两个参数的构造方法会导致程序出错):

    首先,我们先把后面需要用的变量写出来,方便大家明白这些变量是干嘛的:

// 三种画笔
    private Paint mNormalPaint;
    private Paint mClickPaint;
    private Paint mErrorPaint;

    // 点的半径
    private float mRadius;

    // 九个点,使用二维数组
    private Point[][] mPoints = new Point[3][3];

    // 保存手势划过的点
    private ArrayList<Point> mClickPointsList = new ArrayList<Point>();
    // 手势的 x 坐标,y 坐标
    private float mHandleX;
    private float mHandleY;

    private OnDrawFinishListener mListener;

    // 保存滑动路径
    private StringBuilder mRoute = new StringBuilder();
    // 是否在画错误状态
    private boolean isDrawError = false;

    接下来我们来初始化数据:

// 初始化数据
    private void initData() {

        // 初始化三种画笔,正常状态为灰色,点下状态为蓝色,错误为红色
        mNormalPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mNormalPaint.setColor(Color.parseColor("#ABABAB"));
        mClickPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mClickPaint.setColor(Color.parseColor("#1296db"));
        mErrorPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mErrorPaint.setColor(Color.parseColor("#FB0C13"));

        // 获取点间隔
        float offset = 0;
        if (getWidth() > getHeight()) {
            // 横屏
            offset = getHeight() / 7;
            mRadius = offset / 2;
            mPoints[0][0] = new Point(getWidth() / 2 - offset * 2, offset + mRadius);
            mPoints[0][1] = new Point(getWidth() / 2, offset + mRadius);
            mPoints[0][2] = new Point(getWidth() / 2 + offset * 2, offset + mRadius);
            mPoints[1][0] = new Point(getWidth() / 2 - offset * 2, offset * 3 + mRadius);
            mPoints[1][1] = new Point(getWidth() / 2, offset * 3 + mRadius);
            mPoints[1][2] = new Point(getWidth() / 2 + offset * 2, offset * 3 + mRadius);
            mPoints[2][0] = new Point(getWidth() / 2 - offset * 2, offset * 5 + mRadius);
            mPoints[2][1] = new Point(getWidth() / 2, offset * 5 + mRadius);
            mPoints[2][2] = new Point(getWidth() / 2 + offset * 2, offset * 5 + mRadius);
        } else {
            // 竖屏
            offset = getWidth() / 7;
            mRadius = offset / 2;
            mPoints[0][0] = new Point(offset + mRadius, getHeight() / 2 - 2 * offset);
            mPoints[0][1] = new Point(offset * 3 + mRadius, getHeight() / 2 - 2 * offset);
            mPoints[0][2] = new Point(offset * 5 + mRadius, getHeight() / 2 - 2 * offset);
            mPoints[1][0] = new Point(offset + mRadius, getHeight() / 2);
            mPoints[1][1] = new Point(offset * 3 + mRadius, getHeight() / 2);
            mPoints[1][2] = new Point(offset * 5 + mRadius, getHeight() / 2);
            mPoints[2][0] = new Point(offset + mRadius, getHeight() / 2 + 2 * offset);
            mPoints[2][1] = new Point(offset * 3 + mRadius, getHeight() / 2 + 2 * offset);
            mPoints[2][2] = new Point(offset * 5 + mRadius, getHeight() / 2 + 2 * offset);
        }


    }

    大家可以看到,我来给点定坐标是,是按照比较窄的边的 1/7 作为点的直径,这样保证了,不管你怎么定义 handleLock 的宽高,都可以使里面的九个点看起来位置很舒服。

   接下来我们就需要写一些函数,将点、线绘制到控件上,我自己把绘制分成了三部分,一部分是点,一部分是点与点之间的线,一部分是手势的小点和手势到最新点的线。

// 画点,按照我们选择的半径画九个圆
    private void drawPoints(Canvas canvas) {
        // 便利所有的点,并且判断这些点的状态
        for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 3; j++) {
                Point point = mPoints[i][j];
                switch (point.state) {
                    case Point.POINT_STATUS_NORMAL:
                        canvas.drawCircle(point.mX, point.mY, mRadius, mNormalPaint);
                        break;
                    case Point.POINT_STATUS_CLICK:
                        canvas.drawCircle(point.mX, point.mY, mRadius, mClickPaint);
                        break;
                    case Point.POINT_STATUS_ERROR:
                        canvas.drawCircle(point.mX, point.mY, mRadius, mErrorPaint);
                        break;
                    default:
                        break;

                }
            }
        }
    }
    // 画点与点之间的线
    private void drawLines(Canvas canvas) {
        // 判断手势是否已经划过点了
        if (mClickPointsList.size() > 0) {
            Point prePoint = mClickPointsList.get(0);
            // 将所有已选择点的按顺序连线
            for (int i = 1; i < mClickPointsList.size(); i++) {
                // 判断已选择点的状态
                if (prePoint.state == Point.POINT_STATUS_CLICK) {
                    mClickPaint.setStrokeWidth(7);
                    canvas.drawLine(prePoint.mX, prePoint.mY, mClickPointsList.get(i).mX, mClickPointsList.get(i).mY, mClickPaint);
                }
                if (prePoint.state == Point.POINT_STATUS_ERROR) {
                    mErrorPaint.setStrokeWidth(7);
                    canvas.drawLine(prePoint.mX, prePoint.mY, mClickPointsList.get(i).mX, mClickPointsList.get(i).mY, mErrorPaint);
                }
                prePoint = mClickPointsList.get(i);
            }

        }

    }
    // 画手势点
    private void drawFinger(Canvas canvas) {
        // 有选择点后再出现手势点
        if (mClickPointsList.size() > 0) {
            canvas.drawCircle(mHandleX, mHandleY, mRadius / 2, mClickPaint);
        }
        // 最新点到手指的连线,判断是否有已选择的点,有才能画
        if (mClickPointsList.size() > 0) {
            canvas.drawLine(mClickPointsList.get(mClickPointsList.size() - 1).mX, mClickPointsList.get(mClickPointsList.size() - 1).mY,
                    mHandleX, mHandleY, mClickPaint);
        }
    }

    上面的代码我们看到需要使用到手势划过的点,我们是怎么选择的呢?

// 获取手指移动中选取的点
private int[] getPositions() {
    Point point = new Point(mHandleX, mHandleY);
    int[] position = new int[2];
    // 遍历九个点,看手势的坐标是否在九个圆内,有则返回这个点的两个下标
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 3; j++) {
            if (mPoints[i][j].getInstance(point) <= mRadius) {
                position[0] = i;
                position[1] = j;
                return position;
            }
        }

    }
    return null;
}

    我们需要重写其 onTouchEvent 来通过手势动作来提交选择的点,并更新视图:

// 重写点击事件
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // 获取手势的坐标
        mHandleX = event.getX();
        mHandleY = event.getY();
        int[] position;
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                position = getPositions();
                // 判断点下时是否选择到点
                if (position != null) {
                    // 添加到已选择点中,并改变其状态
                    mClickPointsList.add(mPoints[position[0]][position[1]]);
                    mPoints[position[0]][position[1]].state = Point.POINT_STATUS_CLICK;
                    // 保存路径,依次保存其横纵下标
                    mRoute.append(position[0]);
                    mRoute.append(position[1]);
                }
                break;
            case MotionEvent.ACTION_MOVE:
                position = getPositions();
                // 判断手势移动时是否选择到点
                if (position != null) {
                    // 判断当前选择的点是否已经被选择过
                    if (!mClickPointsList.contains(mPoints[position[0]][position[1]])) {
                        // 添加到已选择点中,并改变其状态
                        mClickPointsList.add(mPoints[position[0]][position[1]]);
                        mPoints[position[0]][position[1]].state = Point.POINT_STATUS_CLICK;
                        // 保存路径,依次保存其横纵下标
                        mRoute.append(position[0]);
                        mRoute.append(position[1]);
                    }
                }
                break;
            case MotionEvent.ACTION_UP:
                // 重置数据
                resetData();
                break;
            default:
                break;
        }
        // 更新视图
        invalidate();

        return true;
    }
// 重置数据
    private void resetData() {
        // 将所有选择过的点的状态改为正常
        for (Point point :
                mClickPointsList) {
            point.state = Point.POINT_STATUS_NORMAL;
        }
        // 清空已选择点
        mClickPointsList.clear();
        // 清空保存的路径
        mRoute = new StringBuilder();
        // 不再画错误状态
        isDrawError = false;
    }

    那我们怎么绘制视图呢?我们通过重写其 onDraw() 方法:

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 判断是否画错误状态,画错误状态不需要画手势点已经于最新选择点的连线
        if (isDrawError) {
            drawPoints(canvas);
            drawLines(canvas);
        } else {
            drawPoints(canvas);
            drawLines(canvas);
            drawFinger(canvas);
        }
    }

    那么这个手势密码绘制过程就结束了,但是整个控件还没有结束,我们还需要给它一个监听器,监听其绘制完成,选择后续事件:

private OnDrawFinishListener mListener;

    // 定义绘制完成的接口
    public interface OnDrawFinishListener {
        public boolean drawFinish(String route);
    }

    // 定义绘制完成的方法,传入接口
    public void setOnDrawFinishListener(OnDrawFinishListener listener) {
        this.mListener = listener;
    }

    然后我们就需要在手势离开的时候 ,来进行绘制完成时的事件:

case MotionEvent.ACTION_UP:
                // 完成时回调绘制完成的方法,返回比对结果,判断手势密码是否正确
                mListener.drawFinish(mRoute.toString());
                // 返回错误,则将所有已选择点状态改为错误
                if (!mListener.drawFinish(mRoute.toString())) {
                    for (Point point :
                            mClickPointsList) {
                        point.state = Point.POINT_STATUS_ERROR;
                    }
                    // 将是否绘制错误设为 true
                    isDrawError = true;
                    // 刷新视图
                    invalidate();
                    // 这里我们使用 handler 异步操作,使其错误状态保持 0.5s
                    new Thread(new Runnable() {
                        @Override
                        public void run() {
                            if (!mListener.drawFinish(mRoute.toString())) {
                                Message message = new Message();
                                message.arg1 = 0;
                                handler.sendMessage(message);
                            }
                        }
                    }).run();
                } else {
                    resetData();
                }
                invalidate();

                break;
private Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.arg1) {
                case 0:
                    try {
                        // 沉睡 0.5s
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    // 重置数据,并刷新视图
                    resetData();
                    invalidate();
                    break;
                default:
                    break;
            }

        }
    };

    好了,handleLock,整个过程就结束了,笔者这里定义了一个监听器只是给大家提供一种思路,笔者将保存的大路径传给了使用者,是为了保证使用者可以自己保存密码,并作相关操作,大家也可以使用 HandleLock 来  保存密码,不传给使用者,根据自己的需求写出更多更丰富的监听器,而且这里笔者在 MotionEvent.ACTION_UP 中直接回调了 drawFinish() 方法,就意味着要使用该 HandleLock 就必须给它设置监听器。

    接下来我们说说 HandleLock 的使用,首先是在布局文件中使用:

<com.example.a01378359.testapp.lock.HandleLock
        android:id="@+id/handlelock_test"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    接下来是代码中使用:

handleLock = findViewById(R.id.handlelock_test);
        handleLock.setOnDrawFinishListener(new HandleLock.OnDrawFinishListener() {
            @Override
            public boolean drawFinish(String route) {
                // 第一次滑动,则保存密码
                if (count == 0){
                    password = route;
                    count++;
                    Toast.makeText(LockTestActivity.this,"已保存密码",Toast.LENGTH_SHORT).show();
                    return true;
                }else {
                    // 与保存密码比较,返回结果,并且做出相应事件
                    if (password.equals(route)){
                        Toast.makeText(LockTestActivity.this,"密码正确",Toast.LENGTH_SHORT).show();
                        return true;
                    }else {
                        Toast.makeText(LockTestActivity.this,"密码错误",Toast.LENGTH_SHORT).show();
                        return false;
                    }
                }
            }
        });
    项目地址: 源代码



猜你喜欢

转载自blog.csdn.net/young_time/article/details/80856817