Android之高仿手機QQ圖案解鎖
ps:請不要再問我,為什麼匯入之後會亂碼了。
其實,程式碼基本上都是從原生系統中提取的:LockPatternView、加密工具類,以及解鎖邏輯等,我只是稍作修改,大家都知道,原生系統介面比較醜陋,因此,我特意把QQ的apk解壓了,從中拿了幾張圖案解鎖的圖片,一個簡單的例子就這樣誕生了。
好了,廢話不多說,我們來看看效果(最後兩張是最新4.4系統,炫一下,呵呵):
1.最關健的就是那個自定義九宮格View,程式碼來自framework下:LockPatternView,原生系統用的圖片資源比較多,好像有7、8張吧,而且繪製的比較複雜,我找尋半天,眼睛都找瞎了,發現解壓的QQ裡面就3張圖片,一個圈圈,兩個點,沒辦法,只能修改程式碼了,在修改的過程中,才發現,其實可以把原生的LockPatternView給簡化,繪製更少的圖片,達到更好的效果。總共優化有:①去掉了連線的箭頭,②原生的連線只有白色一種,改成根據不同狀態顯示黃色和紅色兩張色,③.原生view是先畫點再畫線,使得線覆蓋在點的上面,影響美觀,改成先畫連線再畫點。
關健部分程式碼onDraw函式:
@Override protected void onDraw(Canvas canvas) { final ArrayList<Cell> pattern = mPattern; final int count = pattern.size(); final boolean[][] drawLookup = mPatternDrawLookup; if (mPatternDisplayMode == DisplayMode.Animate) { // figure out which circles to draw // + 1 so we pause on complete pattern final int oneCycle = (count + 1) * MILLIS_PER_CIRCLE_ANIMATING; final int spotInCycle = (int) (SystemClock.elapsedRealtime() - mAnimatingPeriodStart) % oneCycle; final int numCircles = spotInCycle / MILLIS_PER_CIRCLE_ANIMATING; clearPatternDrawLookup(); for (int i = 0; i < numCircles; i++) { final Cell cell = pattern.get(i); drawLookup[cell.getRow()][cell.getColumn()] = true; } // figure out in progress portion of ghosting line final boolean needToUpdateInProgressPoint = numCircles > 0 && numCircles < count; if (needToUpdateInProgressPoint) { final float percentageOfNextCircle = ((float) (spotInCycle % MILLIS_PER_CIRCLE_ANIMATING)) / MILLIS_PER_CIRCLE_ANIMATING; final Cell currentCell = pattern.get(numCircles - 1); final float centerX = getCenterXForColumn(currentCell.column); final float centerY = getCenterYForRow(currentCell.row); final Cell nextCell = pattern.get(numCircles); final float dx = percentageOfNextCircle * (getCenterXForColumn(nextCell.column) - centerX); final float dy = percentageOfNextCircle * (getCenterYForRow(nextCell.row) - centerY); mInProgressX = centerX + dx; mInProgressY = centerY + dy; } // TODO: Infinite loop here... invalidate(); } final float squareWidth = mSquareWidth; final float squareHeight = mSquareHeight; float radius = (squareWidth * mDiameterFactor * 0.5f); mPathPaint.setStrokeWidth(radius); final Path currentPath = mCurrentPath; currentPath.rewind(); // TODO: the path should be created and cached every time we hit-detect // a cell // only the last segment of the path should be computed here // draw the path of the pattern (unless the user is in progress, and // we are in stealth mode) final boolean drawPath = (!mInStealthMode || mPatternDisplayMode == DisplayMode.Wrong); // draw the arrows associated with the path (unless the user is in // progress, and // we are in stealth mode) boolean oldFlag = (mPaint.getFlags() & Paint.FILTER_BITMAP_FLAG) != 0; mPaint.setFilterBitmap(true); // draw with higher quality since we // render with transforms // draw the lines if (drawPath) { boolean anyCircles = false; for (int i = 0; i < count; i++) { Cell cell = pattern.get(i); // only draw the part of the pattern stored in // the lookup table (this is only different in the case // of animation). if (!drawLookup[cell.row][cell.column]) { break; } anyCircles = true; float centerX = getCenterXForColumn(cell.column); float centerY = getCenterYForRow(cell.row); if (i == 0) { currentPath.moveTo(centerX, centerY); } else { currentPath.lineTo(centerX, centerY); } } // add last in progress section if ((mPatternInProgress || mPatternDisplayMode == DisplayMode.Animate) && anyCircles) { currentPath.lineTo(mInProgressX, mInProgressY); } // chang the line color in different DisplayMode if (mPatternDisplayMode == DisplayMode.Wrong) mPathPaint.setColor(Color.RED); else mPathPaint.setColor(Color.YELLOW); canvas.drawPath(currentPath, mPathPaint); } // draw the circles final int paddingTop = getPaddingTop(); final int paddingLeft = getPaddingLeft(); for (int i = 0; i < 3; i++) { float topY = paddingTop + i * squareHeight; // float centerY = mPaddingTop + i * mSquareHeight + (mSquareHeight // / 2); for (int j = 0; j < 3; j++) { float leftX = paddingLeft + j * squareWidth; drawCircle(canvas, (int) leftX, (int) topY, drawLookup[i][j]); } } mPaint.setFilterBitmap(oldFlag); // restore default flag }
2.第二個值得學習的地方是(程式碼來自設定應用中):在建立解鎖圖案時的列舉使用,原生程式碼中使用了很多列舉,將繪製圖案時的狀態、底部兩個按鈕狀態、頂部一個TextView顯示的提示文字都緊密的聯絡起來。因此,只用監聽LockPatternView動態變化,對應改變底部Button和頂部TextView的狀態即可實現聯動,簡單的方法可以實現很多程式碼才能實現的邏輯,個人很喜歡。
①全域性的狀態:
/** * Keep track internally of where the user is in choosing a pattern. */ protected enum Stage { // 初始狀態 Introduction(R.string.lockpattern_recording_intro_header, LeftButtonMode.Cancel, RightButtonMode.ContinueDisabled, ID_EMPTY_MESSAGE, true), // 幫助狀態 HelpScreen(R.string.lockpattern_settings_help_how_to_record, LeftButtonMode.Gone, RightButtonMode.Ok, ID_EMPTY_MESSAGE, false), // 繪製過短 ChoiceTooShort(R.string.lockpattern_recording_incorrect_too_short, LeftButtonMode.Retry, RightButtonMode.ContinueDisabled, ID_EMPTY_MESSAGE, true), // 第一次繪製圖案 FirstChoiceValid(R.string.lockpattern_pattern_entered_header, LeftButtonMode.Retry, RightButtonMode.Continue, ID_EMPTY_MESSAGE, false), // 需要再次繪製確認 NeedToConfirm(R.string.lockpattern_need_to_confirm, LeftButtonMode.Cancel, RightButtonMode.ConfirmDisabled, ID_EMPTY_MESSAGE, true), // 確認出錯 ConfirmWrong(R.string.lockpattern_need_to_unlock_wrong, LeftButtonMode.Cancel, RightButtonMode.ConfirmDisabled, ID_EMPTY_MESSAGE, true), // 選擇確認 ChoiceConfirmed(R.string.lockpattern_pattern_confirmed_header, LeftButtonMode.Cancel, RightButtonMode.Confirm, ID_EMPTY_MESSAGE, false); /** * @param headerMessage * The message displayed at the top. * @param leftMode * The mode of the left button. * @param rightMode * The mode of the right button. * @param footerMessage * The footer message. * @param patternEnabled * Whether the pattern widget is enabled. */ Stage(int headerMessage, LeftButtonMode leftMode, RightButtonMode rightMode, int footerMessage, boolean patternEnabled) { this.headerMessage = headerMessage; this.leftMode = leftMode; this.rightMode = rightMode; this.footerMessage = footerMessage; this.patternEnabled = patternEnabled; } final int headerMessage; final LeftButtonMode leftMode; final RightButtonMode rightMode; final int footerMessage; final boolean patternEnabled; }
②.底部兩個按鈕的狀態列舉:
/**
* The states of the left footer button.
*/
enum LeftButtonMode {
// 取消
Cancel(android.R.string.cancel, true),
// 取消時禁用
CancelDisabled(android.R.string.cancel, false),
// 重試
Retry(R.string.lockpattern_retry_button_text, true),
// 重試時禁用
RetryDisabled(R.string.lockpattern_retry_button_text, false),
// 消失
Gone(ID_EMPTY_MESSAGE, false);
/**
* @param text
* The displayed text for this mode.
* @param enabled
* Whether the button should be enabled.
*/
LeftButtonMode(int text, boolean enabled) {
this.text = text;
this.enabled = enabled;
}
final int text;
final boolean enabled;
}
/**
* The states of the right button.
*/
enum RightButtonMode {
// 繼續
Continue(R.string.lockpattern_continue_button_text, true),
//繼續時禁用
ContinueDisabled(R.string.lockpattern_continue_button_text, false),
//確認
Confirm(R.string.lockpattern_confirm_button_text, true),
//確認是禁用
ConfirmDisabled(R.string.lockpattern_confirm_button_text, false),
//OK
Ok(android.R.string.ok, true);
/**
* @param text
* The displayed text for this mode.
* @param enabled
* Whether the button should be enabled.
*/
RightButtonMode(int text, boolean enabled) {
this.text = text;
this.enabled = enabled;
}
final int text;
final boolean enabled;
}
就這樣,只要LockPatternView的狀態一發生改變,就會動態改變底部兩個Button的文字和狀態。很簡潔,邏輯性很強。
3.第三個個人覺得比較有用的就是加密這一塊了,為了以後方便使用,我把圖案加密和字元加密分成兩個工具類:LockPatternUtils和LockPasswordUtils兩個檔案,本文使用到的是LockPatternUtils。其實所謂的圖案加密也是將其通過SHA-1加密轉化成二進位制數再儲存到檔案中(原生系統儲存在/system/目錄下,我這裡沒有許可權,就儲存到本應用目錄下),解密時,也是將獲取到使用者的輸入通過同樣的方法加密,再與儲存到檔案中的對比,相同則密碼正確,不同則密碼錯誤。關健程式碼就是以下4個函式:
/**
* Serialize a pattern. 加密
*
* @param pattern
* The pattern.
* @return The pattern in string form.
*/
public static String patternToString(List<LockPatternView.Cell> pattern) {
if (pattern == null) {
return "";
}
final int patternSize = pattern.size();
byte[] res = new byte[patternSize];
for (int i = 0; i < patternSize; i++) {
LockPatternView.Cell cell = pattern.get(i);
res[i] = (byte) (cell.getRow() * 3 + cell.getColumn());
}
return new String(res);
}
/**
* Save a lock pattern.
*
* @param pattern
* The new pattern to save.
* @param isFallback
* Specifies if this is a fallback to biometric weak
*/
public void saveLockPattern(List<LockPatternView.Cell> pattern) {
// Compute the hash
final byte[] hash = LockPatternUtils.patternToHash(pattern);
try {
// Write the hash to file
RandomAccessFile raf = new RandomAccessFile(sLockPatternFilename,
"rwd");
// Truncate the file if pattern is null, to clear the lock
if (pattern == null) {
raf.setLength(0);
} else {
raf.write(hash, 0, hash.length);
}
raf.close();
} catch (FileNotFoundException fnfe) {
// Cant do much, unless we want to fail over to using the settings
// provider
Log.e(TAG, "Unable to save lock pattern to " + sLockPatternFilename);
} catch (IOException ioe) {
// Cant do much
Log.e(TAG, "Unable to save lock pattern to " + sLockPatternFilename);
}
}
/*
* Generate an SHA-1 hash for the pattern. Not the most secure, but it is at
* least a second level of protection. First level is that the file is in a
* location only readable by the system process.
*
* @param pattern the gesture pattern.
*
* @return the hash of the pattern in a byte array.
*/
private static byte[] patternToHash(List<LockPatternView.Cell> pattern) {
if (pattern == null) {
return null;
}
final int patternSize = pattern.size();
byte[] res = new byte[patternSize];
for (int i = 0; i < patternSize; i++) {
LockPatternView.Cell cell = pattern.get(i);
res[i] = (byte) (cell.getRow() * 3 + cell.getColumn());
}
try {
MessageDigest md = MessageDigest.getInstance("SHA-1");
byte[] hash = md.digest(res);
return hash;
} catch (NoSuchAlgorithmException nsa) {
return res;
}
}
/**
* Check to see if a pattern matches the saved pattern. If no pattern
* exists, always returns true.
*
* @param pattern
* The pattern to check.
* @return Whether the pattern matches the stored one.
*/
public boolean checkPattern(List<LockPatternView.Cell> pattern) {
try {
// Read all the bytes from the file
RandomAccessFile raf = new RandomAccessFile(sLockPatternFilename,
"r");
final byte[] stored = new byte[(int) raf.length()];
int got = raf.read(stored, 0, stored.length);
raf.close();
if (got <= 0) {
return true;
}
// Compare the hash from the file with the entered pattern's hash
return Arrays.equals(stored,
LockPatternUtils.patternToHash(pattern));
} catch (FileNotFoundException fnfe) {
return true;
} catch (IOException ioe) {
return true;
}
}
好了,程式碼就分析到這裡,非常感謝你看到了文章末尾,很晚了,睡覺去,如果大家有什麼問題或建議,歡迎留言,一起討論,謝謝!