寫一個圖案解鎖控件
雖然網上有很多的關於圖案解鎖的現成輪子,但是 ,有什麼比自己寫一個輪子更帶勁的事情呢?
首先展示效果:
實現分析
屬性分析
應爲這是一個自定義控件,網上很多的輪子都是通過替換圖片來實現的,但是,我並不想使用圖片(最主要,不會切圖)。那麼我就需要考慮,我應該使用那些自定義屬性。
通過觀察別人的輪子,發現主要的自定義屬性有這些:
- 正常情況下的點顏色
- 按下時點的顏色
- 錯誤時點的顏色
- 連接時線的顏色
- 錯誤時線的顏色
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上,有興趣的可以看看代碼