写一个图案解锁控件
虽然网上有很多的关于图案解锁的现成轮子,但是 ,有什么比自己写一个轮子更带劲的事情呢?
首先展示效果:
实现分析
属性分析
应为这是一个自定义控件,网上很多的轮子都是通过替换图片来实现的,但是,我并不想使用图片(最主要,不会切图)。那么我就需要考虑,我应该使用那些自定义属性。
通过观察别人的轮子,发现主要的自定义属性有这些:
- 正常情况下的点颜色
- 按下时点的颜色
- 错误时点的颜色
- 连接时线的颜色
- 错误时线的颜色
PS:应该还有一个点的半径,但是我想想还是写死了算了。
实现分析
先上一张图:
图有点丑,见谅
图中的9个实心点,就是代表我们所需要绘制的九宫的9个点的位置,整个区域是一个正方形(不要问我为什么是正方形),可以从图中看到水平和竖直的3个点把分别把横竖方向上分成了4部分,我们计算的坐标时每个点取1/4就行了。
行为分析
- 在进行连线时,同一个点不能被链接两次
- 横,竖,斜三个方向上不能有跳过 。以横向举例,比如选择了第一个点,不能跳过第二个点,而去直接连接第三个点。
代码实现
### 自定义属性
经过上面的分析,我可以开始动手了。
首先定义5个属性:
<declare-styleable name="PatternDeblockView">
<!-- 按下时的圆的颜色-->
<attr name="pressed_color" format="color"></attr>
<!--错误时的圆的颜色-->
<attr name="error_color" format="color"></attr>
<!--平常时的颜色-->
<attr name="normal_color" format="color"></attr>
<!--错误时的线的颜色-->
<attr name="error_line_color" format="color"></attr>
<!--滑动时的线上的颜色-->
<attr name="pressed_line_color" format="color"></attr>
</declare-styleable>
当5个属性定义完成后,获取这些属性:
private void initAttr(Context context, AttributeSet attrs) {
if (null != attrs) {
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.PatternDeblockView);
int len = array.getIndexCount();
for (int i = 0; i < len; i++) {
int attr = array.getIndex(i);
switch (attr) {
case R.styleable.PatternDeblockView_pressed_color:
mPressedColor = array.getColor(attr, Color.BLUE);
break;
case R.styleable.PatternDeblockView_error_color:
mErrorColor = array.getColor(attr, Color.RED);
break;
case R.styleable.PatternDeblockView_normal_color:
mNormalColor = array.getColor(attr, Color.GRAY);
break;
case R.styleable.PatternDeblockView_pressed_line_color:
mPressedLineColor = array.getColor(attr, Color.BLUE);
break;
case R.styleable.PatternDeblockView_error_line_color:
mErrorLineColor = array.getColor(attr, Color.RED);
break;
}
}
}
mChoosePoints = new ArrayList<Point>();
mChoosePassword = new ArrayList<Integer>();
//设置半径
mRadius = 40;
mPaint = new Paint();
mPaint.setStrokeWidth(5);//设置线宽
mPaint.setAntiAlias(true);
}
在获取自定义属性值得同时对一些属性进行设置,在这里我设置了圆的半径和画笔的线宽。该方法需要在重写的构造方法中调用。
测量
自定义属性获取完成后,我需要通过重写onMeasure()
方法,获取我当前控件的宽高值:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
width = measureWidth(widthMeasureSpec);
height = measureHeight(heightMeasureSpec);
setMeasuredDimension(width, height);
}
private int measureHeight(int heightMeasureSpec) {
int height = 0;
int mode = MeasureSpec.getMode(heightMeasureSpec);
int size = MeasureSpec.getSize(heightMeasureSpec);
if (mode == MeasureSpec.EXACTLY) {
height = size;
} else {
height = 200;
}
return height;
}
measureWidth()
和measureHeight()
方法类似,这几个方法都是基本通用的形式。
绘制
这一步是两个关键部分的其中一个。
在重写的onDraw()
方法中,我需要把九宫格的九宫点,按照之前的分析画出来:
protected void onDraw(Canvas canvas) {
//没有进行初始化
if (mPoints == null) {
// 进行初始化
initPaints();
}
drawPoints(canvas, mPoints);
drawLine(canvas, movingX, movingY);
super.onDraw(canvas);
}
上来首先,对九个点有没有初始化进行判断,如果已经初始化了,就直接画点和画线,如果没有初始化,先进行初始化操作:
private void initPaints() {
float offsetX = 0;
float offsetY = 0;
min = Math.min(width, height);//获取最小值
if (width >= height) {//如果宽比高大
offsetX = (width - height) / 2;
} else {//如果宽比高小
offsetY = (height - width) / 2;
}
mPoints = new Point[3][3];
//初始化完成
mPoints[0][0] = new Point(offsetX + min / 4, offsetY + min / 4, 1);
mPoints[0][1] = new Point(offsetX + min / 2, offsetY + min / 4, 2);
mPoints[0][2] = new Point(offsetX + min - min / 4, offsetY + min / 4, 3);
mPoints[1][0] = new Point(offsetX + min / 4, offsetY + min / 2, 4);
mPoints[1][1] = new Point(offsetX + min / 2, offsetY + min / 2, 5);
mPoints[1][2] = new Point(offsetX + min - min / 4, offsetY + min / 2, 6);
mPoints[2][0] = new Point(offsetX + min / 4, offsetY + min - min / 4, 7);
mPoints[2][1] = new Point(offsetX + min / 2, offsetY + min - min / 4, 8);
mPoints[2][2] = new Point(offsetX + min - min / 4, offsetY + min - min / 4, 9);
}
通过使用3*3的二维数组进行点的初始化,按照下图所示进行说明
由于我们要把九宫放在我们自定义控件展示的中间位置,因此需要获取控件实际的宽和高并获取最小的一个,如上图所示的情况,由于height大于width,因此,在y方向要加上偏移,由于加上偏移后要使得九宫位于中间,因此y的偏移量为(height-widht)/2
。反之。x方向的偏移为(width-height)/2
。
在计算完偏移后就能通过4等分的原则,得到所用点的具体坐标。把9点的位置计算完成后,进行画点:
private void drawPoints(Canvas canvas, Point[][] mPoints) {
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
Point point = mPoints[i][j];
if (point.state == Point.NORMALSTATE) {
mPaint.setColor(mNormalColor);
} else if (point.state == Point.PRESSEDSTATE) {
mPaint.setColor(mPressedColor);
} else {
mPaint.setColor(mErrorColor);
}
canvas.drawCircle(point.x, point.y, mRadius, mPaint);
}
}
}
在画点的时候需要注意的是,根据点的状态,绘制不同的颜色。接下的的任务,就是画线了,划线部分,需要结合对Touch事件的处理一起描述:
private void drawLine(Canvas canvas, float movingX, float movingY) {
float secondX = 0;
float secondY = 0;
float firstX = 0;
float firstY = 0;
if (isError) {
mPaint.setColor(mErrorLineColor);//设置错误时的线颜色
} else {
mPaint.setColor(mPressedLineColor);
}
int len = mChoosePoints.size();
if (len >= 1) {
firstX = mChoosePoints.get(0).x;
firstY = mChoosePoints.get(0).y;
}
//画点中的线
for (int i = 1; i < len; i++) {
secondX = mChoosePoints.get(i).x;
secondY = mChoosePoints.get(i).y;
canvas.drawLine(firstX, firstY, secondX, secondY, mPaint);
firstX = secondX;
firstY = secondY;
}
if (!isFinish) {
//画点到点的线
if (len >= 1) {
canvas.drawLine(firstX, firstY, movingX, movingY, mPaint);
}
}
}
首先是把已经选中的点之间用线连接起来。也要注意状态。如果没有移动事件没有结束同时没有点和最后一个点连接起来,需要把最后一个点和当其的手指点击位置连接起来。
处理Touch事件
这部分是重中之重,先上代码:
public boolean onTouchEvent(MotionEvent event) {
//获得当其移动位置
movingX = event.getX();
movingY = event.getY();
Point point = null;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (!isFinish) {
point = choosePoints(movingX, movingY);//判断落下的点是否在9个点上
if (null != point) {
mChoosePoints.add(point);//如果不为空加入选中的集合中
mChoosePassword.add(point.index);
isChosen = true;// 表示已经与了选择
}
}
break;
case MotionEvent.ACTION_MOVE:
//移动时有两情况需要进考虑,1:在点的范围时需要把点选中,2:重复点不计算,3:一条线上 不能跳过选择
if (!isFinish) {
isRepeat = checkIsRepeat(movingX, movingY);
if (!isRepeat) {//如果不在已经选中的点中,判读是否在点阵中
point = choosePoints(movingX, movingY);
//进行跳过选择
if (null != point) {
mChoosePoints.add(point);
mChoosePassword.add(point.index);
}
parsePoint();
}
}
break;
case MotionEvent.ACTION_UP:
isFinish = true;
//对点数进行判读 如果连起来的点数少于5设置成
int len = mChoosePoints.size();
if (len == 0) {
isChosen = false;
isFinish = false;
}
if (!isPassword) {//不是密码模式
if (len < 5) {
isError = true;
for (int i = 0; i < len; i++) {
point = mChoosePoints.get(i);
point.state = Point.ERRORSTATE;
}
if (null != mListener) {
mListener.drawMessage(false, null);
}
} else {
if (null != mListener) {
mListener.drawMessage(true, mChoosePassword);
}
}
} else {//是密码模式
if (mChoosePassword.equals(mPassword)) {
if (null != mListener) {
mListener.enterPass(true);
}
} else {
if (null != mListener) {
//把颜色改变
len = mChoosePoints.size();
for (int i = 0; i < len; i++) {
point = mChoosePoints.get(i);
point.state = Point.ERRORSTATE;
}
mListener.enterPass(false);
}
}
}
break;
}
postInvalidate();// 进行重绘
return true;
}
在该Touch事件中,需要处理3个Action,ACTION_DOWN
、ACTION_MOVE
、ACTION_UP
三个事件。
在ACTION_DOWN
事件中需要处理,当前用户点击的区域是否在点的范围中,如果在范围中,就把该点添加的被选中的集合中。并把选中的状态进行更改。
判断是否在点的范围中也很简单:
private Point choosePoints(float movingX, float movingY) {
Log.i(TAG, "movingX:" + movingX + ";;;;;movingY:" + movingY);
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
Point point = mPoints[i][j];
if (Point.isInPoint(point, movingX, movingY, mRadius)) {
//修改点的状态(被选中)
point.state = Point.PRESSEDSTATE;
return point;
}
}
}
return null;
}
在ACTION_MOVE
事件中需要对下面三种情况进处理。
- 在点的范围时需要把点选中。
- 重复点不计算。
- 一条线上不能跳过选择。、
第一种情况和ACTION_DOWN
的处理方式一致,第二种情况处理如下:
private boolean checkIsRepeat(float movingX, float movingY) {
int len = mChoosePoints.size();
for (int i = 0; i < len; i++) {
if (Point.isInPoint(mChoosePoints.get(i), movingX, movingY, mRadius)) {
return true;
}
}
return false;
}
也是很简单的循环。
针对第三种情况,我只想到了如下的实现方式,讲道理,应该有其他的方式进行判断:
private void parsePoint() {
int len = mChoosePoints.size();
//如果被选中的点个数不大于1
if (len <= 1) {
return;
}
//拿到最后最后两个Point
Point a = mChoosePoints.get(len - 1);
Point b = mChoosePoints.get(len - 2);
Point p = null;
float x = 0;
float y = 0;
if (a.x == b.x && Math.abs(a.y - b.y) == min / 2) {//在同一条竖线
Log.i(TAG, "a.x=b.x");
x = a.x;
y = a.y - (a.y - b.y) / 2;
} else if (Math.abs(a.x - b.x) == min / 2 && a.y == b.y) {//在同一条直线
x = a.x - (a.x - b.x) / 2;
y = a.y;
} else if (Math.abs(a.x - b.x) == min / 2 && Math.abs(a.y - b.y) == min / 2) {//在同一条斜线上
x = a.x - (a.x - b.x) / 2;
y = a.y - (a.y - b.y) / 2;
}
p = choosePoints(x, y);
Log.i(TAG, "X:" + x + ";;;;;Y:" + y);
if (null != p) {
p.state = Point.PRESSEDSTATE;
mChoosePoints.add(len - 1, p);
mChoosePassword.add(len - 1, p.index);
}
}
在这里,我是通过计算坐标的之间的差进行的,比较蠢。
ACTION_UP
的处理就比较简单了,如果不是密码模式,就检测已经选中点的数量,如果超过范围用正常的方式展示,如果在范围之内,使用错误的方式展示。
最后就是调用postInvalidate()
方法进行重绘。
恩,大体应该就是这么多。
最后说两句
虽然,整个小控件完成了,但是在编写这的控件中发现了很多问题,
- 命名的问题,命名感觉乱七八糟的。
- 思考问题的逻辑还是存在一些问题,逻辑不严谨,对于空判断等一些可能会造成异常的条件,没有进行控制。
以后好好努力吧。
整个项目我放在了github上,有兴趣的可以看看代码