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 mClickPointsList = new ArrayList();    // 手势的 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 的使用,首先是在布局文件中使用:

    接下来是代码中使用:

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;                    }                }            }        });
    项目地址: 源代码



更多相关文章

  1. fix android build error : undefined reference to __gxx_perso
  2. Android实现自定义view---绘制图片
  3. android.view.ViewRootImpl$CalledFromWrongThreadException: On
  4. Mac下Android相关配置
  5. android 设置状态栏颜色
  6. Gradle sync failed: Connection refused
  7. 在android状态栏上添加多个图标
  8. android 全面屏、刘海屏等沉浸式状态栏
  9. Android(安卓)shareprefernce

随机推荐

  1. mysql8.0.20配合binlog2sql的配置和简单
  2. MySQL索引失效的几种情况汇总
  3. 详解MySQL 聚簇索引与非聚簇索引
  4. MySQL 索引的优缺点以及创建索引的准则
  5. MySQL MyISAM 与InnoDB 的区别
  6. MySQL btree索引与hash索引区别
  7. mysql group by 对多个字段进行分组操作
  8. 基于JPQL实现纯SQL语句方法详解
  9. MySQL删除表的三种方式(小结)
  10. MySQL复制表的三种方式(小结)