自定義View之--九宮格圖形密碼鎖
前言:
很多金融和幾大商業銀行的APP,都使用了九宮格圖形密碼鎖來增強資金賬戶的安全。我也是金融公司的一員,在空餘的時候,寫下這個view,可以說是明智之舉。
效果預覽
這樣一個邏輯差不多可以滿足基本的需求了。接下來就看程式碼咯。
NineSquareView的成長
1、重寫構造方法和初始化屬性
private Paint pointPaint; //畫點的畫筆
private Paint linePaint; // 畫線的畫筆
private Path path; //路徑
private static int SQUAREWIDRH = 300 ; //預設正方形的邊長
private float mSquarewidth = SQUAREWIDRH; //每個正方形的邊長 9個
private float x, y; //手指在滑動的時候那個點的座標
private float startX, startY; //手指首次接觸View的那個點的座標
private LinkedHashMap<String,Point> points = new LinkedHashMap<>(); //存放手指連線的點
private OnFinishGestureListener finishGestureListener ; //當手指擡起時,觸發的監聽
public NineSquareView(Context context) {
this(context, null);
}
public NineSquareView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public NineSquareView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
linePaint = new Paint();
linePaint.setStyle(Paint.Style.STROKE);
linePaint.setColor(Color.CYAN);
linePaint.setStrokeWidth(5);
linePaint.setAntiAlias(true);
linePaint.setStrokeCap(Paint.Cap.ROUND);
pointPaint = new Paint();
pointPaint.setStyle(Paint.Style.FILL);
pointPaint.setColor(Color.parseColor("#cbd0de"));
pointPaint.setStrokeWidth(40);
pointPaint.setAntiAlias(true);
pointPaint.setStrokeCap(Paint.Cap.ROUND);
path =new Path();
}
public interface OnFinishGestureListener {
void onfinish(LinkedHashMap<String,Point> points);
}
2、重寫onMeasure();
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int wideSize = MeasureSpec.getSize(widthMeasureSpec);
int wideMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int width, height;
if (wideMode == MeasureSpec.EXACTLY) { //精確值 或matchParent
width = wideSize;
} else {
width = (int) (mSquarewidth * 3 + getPaddingLeft() + getPaddingRight());
if (wideMode == MeasureSpec.AT_MOST) {
width = Math.min(width, wideSize);
}
}
if (heightMode == MeasureSpec.EXACTLY) { //精確值 或matchParent
height = heightSize;
} else {
height = (int) (mSquarewidth * 3 + getPaddingTop() + getPaddingBottom());
if (heightMode == MeasureSpec.AT_MOST) {
height = Math.min(height, heightSize);
}
}
setMeasuredDimension(width, height);
mSquarewidth = (int) (Math.min(width - getPaddingLeft() - getPaddingRight(),
height - getPaddingTop() - getPaddingBottom()) * 1.0f / 3);
}
mSquarewidth始終是View的三分之一的寬度。對OnMeasure()方法還不是很懂的。可以去看看鴻神寫的部落格Android 自定義View (二) 進階。
3、重寫onTouchEvent();
@Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
startX = ev.getX();
startY = ev.getY();
break;
case MotionEvent.ACTION_MOVE:
x = ev.getX();
y = ev.getY();
invalidate();
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
x = 0;
y = 0;
startX = 0;
startY = 0;
finishGestureListener.onfinish(points);
points.clear();
invalidate();
break;
}
return true;
}
在手指離開螢幕的時候,就是繪製完成的時候,所有資料清零。並觸發finishGestureListener,去處理當前使用者連線的points.
4.重寫onDraw();
最重要的,最精彩的部分來了。首先我們得把九個灰點畫出來。來個雙層for迴圈就搞定。
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
pointPaint.setColor(Color.parseColor("#cbd0de"));
canvas.drawPoint(mSquarewidth * (0.5f + i),mSquarewidth * (0.5f + j),pointPaint);
}
}
每個灰色的點都畫在正方形的中央。可接下來有個問題就要思考了,我們的手指去繪製的時候,要判斷手指觸碰的點是不是正好是那些個灰點。判斷兩個座標是否相等?NONONO,我們畫的點比我們的手指要細些。手指要精確的觸碰到那個灰點,估計有點困難。照這樣下去,你的app早就被使用者解除安裝了。
我們可以給一個範圍,這個範圍是使用者觸碰的點離最近的那個灰點的距離。比如mSquarewidth * 0.3f,如果手指觸控在這個範圍內,就說明使用者想要繪製這個點。這個範圍不能超過mSquarewidth * 0.5f,然後,我們把這個點加入到集合中。
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (Math.abs(startX - mSquarewidth * (0.5f + i)) < mSquarewidth * 0.3f &&
Math.abs(startY - mSquarewidth * (0.5f + j)) < mSquarewidth * 0.3f) {
path.moveTo(mSquarewidth * (0.5f + i), mSquarewidth * (0.5f + j));
path.lineTo(x, y);
canvas.drawPath(path,linePaint);
path.reset();
Point point =new Point(mSquarewidth * (0.5f + i),mSquarewidth * (0.5f + j));
points.put(i+":"+j,point);
System.out.println(points.size());
System.out.println(i+"//"+j);
}
}
}
這樣寫完後,執行寫程式碼。結果就是,只能加入手指點下去的第一個點,想連線下一個點,怎麼辦?繼續思考,寫程式碼。剛才,我們已經連線到了第一個點,想要連線到第二個點,我們必須滑動我們的手指,滑動的時候,座標變為了x,y.而且時時刻刻在變動。再來一次範圍判斷,是不是就可以連線到第二個點了?答案是正確的!
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (Math.abs(x - mSquarewidth * (0.5f + i)) < mSquarewidth * 0.3f &&
Math.abs(y - mSquarewidth * (0.5f + j)) <mSquarewidth * 0.3f
) {
Iterator<Point> iterator2 = collection.iterator();
while(iterator2.hasNext()){
Point point = iterator2.next();
if(mSquarewidth * (0.5f + i)==point.getX() && mSquarewidth * (0.5f + j)==point.getY()){
return;
}
}
startX = mSquarewidth * (0.5f + i);
startY = mSquarewidth * (0.5f + j);
}
}
}
但要排除下,我們已經連線過的點。並把這連線好的第二個點設為起始點。這樣就可以迴圈的連線點了。在一開始的效果預覽中可以看到,連線過的點,會變一種顏色,而且還會有一個小圓環,點與點之間會有一根線連線著,不會消失。這也好辦。
Collection<Point> collection = points.values();
Iterator<Point> iterator = collection.iterator();
if(iterator.hasNext()){
Point point = iterator.next();
drawCyanPoint(canvas,point);
System.out.println("moveTo:"+point.getX()+"===="+point.getY());
path.moveTo(point.getX(),point.getY());
}
while (iterator.hasNext()) {
Point point = iterator.next();
drawCyanPoint(canvas,point);
System.out.println("lineTo:"+point.getX()+"===="+point.getY());
path.lineTo(point.getX(),point.getY());
}
canvas.drawPath(path,linePaint);
path.reset();
在畫了灰點後,可以把map中的points連線起來。改變畫筆的顏色,畫上圓圈,這個圓圈的半徑最好是你設定的那個範圍的大小。我的是mSquarewidth * 0.3f。
//繪製手指劃到的那個點,點外加上一層圈。
public void drawCyanPoint(Canvas canvas, Point point){
String s =getKey(point);
String [] strings = s.split(":");
int i= Integer.parseInt(strings[0]);
int j=Integer.parseInt(strings[1]);
pointPaint.setColor(Color.CYAN);
canvas.drawPoint(mSquarewidth * (0.5f + i),mSquarewidth * (0.5f + j),pointPaint);
canvas.drawCircle(mSquarewidth * (0.5f + i),mSquarewidth * (0.5f + j),mSquarewidth * 0.3f,linePaint);
}
//根據value取key值
public String getKey(Point value)
{
String key = "";
Set<Map.Entry<String, Point>> set = points.entrySet();
for(Map.Entry<String, Point> entry : set){
if(entry.getValue().equals(value)){
key = entry.getKey();
break;
}
}
return key;
}
NinePointView的成長
這個View就是在繪製玩手勢後的一個簡單顯示繪製的點的位置。
這個就比較簡單了,很多都是 copy NineSquaredView的程式碼,就不細說了。
PswActivity的成長。
Activity中的就是邏輯和UI了。PswActivity包含設定密碼鎖和解鎖並跳轉到其他介面。大致邏輯我們都懂的,就不細說了。唯一要說的就是比較兩次設定的密碼是否一致,以及設定密碼與解鎖密碼是否一致。我們要比較兩次的密碼是否一致,其實就是比較兩次繪製時的繪製點的個數,位置是否一致。
public boolean isEquals(LinkedHashMap<String, Point> pointsOne,LinkedHashMap<String, Point> pointsTwo) {
Iterator<String> iterator = pointsOne.keySet().iterator();
Iterator<String> iterator2 = pointsTwo.keySet().iterator();
if (pointsOne.size() != pointsTwo.size()) {
return false;
}
while (iterator.hasNext()) {
String s = iterator.next();
String s2 = iterator2.next();
if (!s.equals(s2)) {
return false;
}
}
return true;
}
因為LinkedHashMap是有序的,所以才能這樣一個一個對應的去比較。我們設定密碼後,密碼是需要存放在本地的,SharedPreferences來幫忙了。等到下一次開啟APP的時候,才能與解鎖密碼作比較。可尋遍了SharedPreferences中的put相關方法,就是沒有能把LinkedHashMap放進去的。剛還思考著呢,Stream來幫忙了。通過寫流和讀流,這樣操作更加安全。
public String map2String(LinkedHashMap<String, Point> hashmap) {
// 例項化一個ByteArrayOutputStream物件,用來裝載壓縮後的位元組檔案。
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
String sceneListString = null;
// 然後將得到的字元資料裝載到ObjectOutputStream
ObjectOutputStream objectOutputStream = null;
try {
objectOutputStream = new ObjectOutputStream(
byteArrayOutputStream);
// writeObject 方法負責寫入特定類的物件的狀態,以便相應的 readObject 方法可以還原它
objectOutputStream.writeObject(hashmap);
// 最後,用Base64.encode將位元組檔案轉換成Base64編碼儲存在String中
sceneListString = new String(Base64.encode(
byteArrayOutputStream.toByteArray(), Base64.DEFAULT),"utf8");
// 關閉objectOutputStream
objectOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
return sceneListString;
}
public LinkedHashMap<String, Point> getHashMap() {
String liststr = preferences.getString(PREFERENCENAME, null);
try {
return string2Map(liststr);
} catch (StreamCorruptedException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
public LinkedHashMap<String, Point> string2Map(
String SceneListString) throws
IOException, ClassNotFoundException {
byte[] mobileBytes = Base64.decode(SceneListString.getBytes(),
Base64.DEFAULT);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(
mobileBytes);
ObjectInputStream objectInputStream = new ObjectInputStream(
byteArrayInputStream);
LinkedHashMap<String, Point> SceneList = (LinkedHashMap<String, Point>) objectInputStream
.readObject();
objectInputStream.close();
return SceneList;
}
所有程式碼連結:
That all,歡迎評論和交流!