/* * Copyright (C) 2007 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.internal.widget; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Path; import android.graphics.Rect; import android.os.Debug; import android.os.Parcel; import android.os.Parcelable; import android.os.SystemClock; import android.util.AttributeSet; import android.view.HapticFeedbackConstants; import android.view.MotionEvent; import android.view.View; import android.view.accessibility.AccessibilityManager; import com.android.internal.R; import java.util.ArrayList; import java.util.List; /** * Displays and detects the user's unlock attempt, which is a drag of a finger * across 9 regions of the screen. * * Is also capable of displaying a static pattern in "in progress", "wrong" or * "correct" states. */ public class LockPatternView extends View { private static final String TAG = "LockPatternView"; // Aspect to use when rendering this view private static final int ASPECT_SQUARE = 0; // View will be the minimum of width/height private static final int ASPECT_LOCK_WIDTH = 1; // Fixed width; height will be minimum of (w,h) private static final int ASPECT_LOCK_HEIGHT = 2; // Fixed height; width will be minimum of (w,h) private static final boolean PROFILE_DRAWING = false; private boolean mDrawingProfilingStarted = false; private Paint mPaint = new Paint(); private Paint mPathPaint = new Paint(); // TODO: make this common with PhoneWindow static final int STATUS_BAR_HEIGHT = 25; /** * How many milliseconds we spend animating each circle of a lock pattern * if the animating mode is set. The entire animation should take this * constant * the length of the pattern to complete. */ private static final int MILLIS_PER_CIRCLE_ANIMATING = 700; private OnPatternListener mOnPatternListener; private ArrayList mPattern = new ArrayList(9); /** * Lookup table for the circles of the pattern we are currently drawing. * This will be the cells of the complete pattern unless we are animating, * in which case we use this to hold the cells we are drawing for the in * progress animation. */ private boolean[][] mPatternDrawLookup = new boolean[3][3]; /** * the in progress point: * - during interaction: where the user's finger is * - during animation: the current tip of the animating line */ private float mInProgressX = -1; private float mInProgressY = -1; private long mAnimatingPeriodStart; private DisplayMode mPatternDisplayMode = DisplayMode.Correct; private boolean mInputEnabled = true; private boolean mInStealthMode = false; private boolean mEnableHapticFeedback = true; private boolean mPatternInProgress = false; private float mDiameterFactor = 0.10f; // TODO: move to attrs private final int mStrokeAlpha = 128; private float mHitFactor = 0.6f; private float mSquareWidth; private float mSquareHeight; private Bitmap mBitmapBtnDefault; private Bitmap mBitmapBtnTouched; private Bitmap mBitmapCircleDefault; private Bitmap mBitmapCircleGreen; private Bitmap mBitmapCircleRed; private Bitmap mBitmapArrowGreenUp; private Bitmap mBitmapArrowRedUp; private final Path mCurrentPath = new Path(); private final Rect mInvalidate = new Rect(); private int mBitmapWidth; private int mBitmapHeight; private int mAspect; private final Matrix mArrowMatrix = new Matrix(); private final Matrix mCircleMatrix = new Matrix(); /** * Represents a cell in the 3 X 3 matrix of the unlock pattern view. */ public static class Cell { int row; int column; // keep # objects limited to 9 static Cell[][] sCells = new Cell[3][3]; static { for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { sCells[i][j] = new Cell(i, j); } } } /** * @param row The row of the cell. * @param column The column of the cell. */ private Cell(int row, int column) { checkRange(row, column); this.row = row; this.column = column; } public int getRow() { return row; } public int getColumn() { return column; } /** * @param row The row of the cell. * @param column The column of the cell. */ public static synchronized Cell of(int row, int column) { checkRange(row, column); return sCells[row][column]; } private static void checkRange(int row, int column) { if (row < 0 || row > 2) { throw new IllegalArgumentException("row must be in range 0-2"); } if (column < 0 || column > 2) { throw new IllegalArgumentException("column must be in range 0-2"); } } public String toString() { return "(row=" + row + ",clmn=" + column + ")"; } } /** * How to display the current pattern. */ public enum DisplayMode { /** * The pattern drawn is correct (i.e draw it in a friendly color) */ Correct, /** * Animate the pattern (for demo, and help). */ Animate, /** * The pattern is wrong (i.e draw a foreboding color) */ Wrong } /** * The call back interface for detecting patterns entered by the user. */ public static interface OnPatternListener { /** * A new pattern has begun. */ void onPatternStart(); /** * The pattern was cleared. */ void onPatternCleared(); /** * The user extended the pattern currently being drawn by one cell. * @param pattern The pattern with newly added cell. */ void onPatternCellAdded(List pattern); /** * A pattern was detected from the user. * @param pattern The pattern. */ void onPatternDetected(List pattern); } public LockPatternView(Context context) { this(context, null); } public LockPatternView(Context context, AttributeSet attrs) { super(context, attrs); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LockPatternView); final String aspect = a.getString(R.styleable.LockPatternView_aspect); if ("square".equals(aspect)) { mAspect = ASPECT_SQUARE; } else if ("lock_width".equals(aspect)) { mAspect = ASPECT_LOCK_WIDTH; } else if ("lock_height".equals(aspect)) { mAspect = ASPECT_LOCK_HEIGHT; } else { mAspect = ASPECT_SQUARE; } setClickable(true); mPathPaint.setAntiAlias(true); mPathPaint.setDither(true); mPathPaint.setColor(Color.WHITE); // TODO this should be from the style mPathPaint.setAlpha(mStrokeAlpha); mPathPaint.setStyle(Paint.Style.STROKE); mPathPaint.setStrokeJoin(Paint.Join.ROUND); mPathPaint.setStrokeCap(Paint.Cap.ROUND); // lot's of bitmaps! mBitmapBtnDefault = getBitmapFor(R.drawable.btn_code_lock_default_holo); mBitmapBtnTouched = getBitmapFor(R.drawable.btn_code_lock_touched_holo); mBitmapCircleDefault = getBitmapFor(R.drawable.indicator_code_lock_point_area_default_holo); mBitmapCircleGreen = getBitmapFor(R.drawable.indicator_code_lock_point_area_green_holo); mBitmapCircleRed = getBitmapFor(R.drawable.indicator_code_lock_point_area_red_holo); mBitmapArrowGreenUp = getBitmapFor(R.drawable.indicator_code_lock_drag_direction_green_up); mBitmapArrowRedUp = getBitmapFor(R.drawable.indicator_code_lock_drag_direction_red_up); // bitmaps have the size of the largest bitmap in this group final Bitmap bitmaps[] = { mBitmapBtnDefault, mBitmapBtnTouched, mBitmapCircleDefault, mBitmapCircleGreen, mBitmapCircleRed }; for (Bitmap bitmap : bitmaps) { mBitmapWidth = Math.max(mBitmapWidth, bitmap.getWidth()); mBitmapHeight = Math.max(mBitmapHeight, bitmap.getHeight()); } } private Bitmap getBitmapFor(int resId) { return BitmapFactory.decodeResource(getContext().getResources(), resId); } /** * @return Whether the view is in stealth mode. */ public boolean isInStealthMode() { return mInStealthMode; } /** * @return Whether the view has tactile feedback enabled. */ public boolean isTactileFeedbackEnabled() { return mEnableHapticFeedback; } /** * Set whether the view is in stealth mode. If true, there will be no * visible feedback as the user enters the pattern. * * @param inStealthMode Whether in stealth mode. */ public void setInStealthMode(boolean inStealthMode) { mInStealthMode = inStealthMode; } /** * Set whether the view will use tactile feedback. If true, there will be * tactile feedback as the user enters the pattern. * * @param tactileFeedbackEnabled Whether tactile feedback is enabled */ public void setTactileFeedbackEnabled(boolean tactileFeedbackEnabled) { mEnableHapticFeedback = tactileFeedbackEnabled; } /** * Set the call back for pattern detection. * @param onPatternListener The call back. */ public void setOnPatternListener( OnPatternListener onPatternListener) { mOnPatternListener = onPatternListener; } /** * Set the pattern explicitely (rather than waiting for the user to input * a pattern). * @param displayMode How to display the pattern. * @param pattern The pattern. */ public void setPattern(DisplayMode displayMode, List pattern) { mPattern.clear(); mPattern.addAll(pattern); clearPatternDrawLookup(); for (Cell cell : pattern) { mPatternDrawLookup[cell.getRow()][cell.getColumn()] = true; } setDisplayMode(displayMode); } /** * Set the display mode of the current pattern. This can be useful, for * instance, after detecting a pattern to tell this view whether change the * in progress result to correct or wrong. * @param displayMode The display mode. */ public void setDisplayMode(DisplayMode displayMode) { mPatternDisplayMode = displayMode; if (displayMode == DisplayMode.Animate) { if (mPattern.size() == 0) { throw new IllegalStateException("you must have a pattern to " + "animate if you want to set the display mode to animate"); } mAnimatingPeriodStart = SystemClock.elapsedRealtime(); final Cell first = mPattern.get(0); mInProgressX = getCenterXForColumn(first.getColumn()); mInProgressY = getCenterYForRow(first.getRow()); clearPatternDrawLookup(); } invalidate(); } private void notifyCellAdded() { sendAccessEvent(R.string.lockscreen_access_pattern_cell_added); if (mOnPatternListener != null) { mOnPatternListener.onPatternCellAdded(mPattern); } } private void notifyPatternStarted() { sendAccessEvent(R.string.lockscreen_access_pattern_start); if (mOnPatternListener != null) { mOnPatternListener.onPatternStart(); } } private void notifyPatternDetected() { sendAccessEvent(R.string.lockscreen_access_pattern_detected); if (mOnPatternListener != null) { mOnPatternListener.onPatternDetected(mPattern); } } private void notifyPatternCleared() { sendAccessEvent(R.string.lockscreen_access_pattern_cleared); if (mOnPatternListener != null) { mOnPatternListener.onPatternCleared(); } } /** * Clear the pattern. */ public void clearPattern() { resetPattern(); } /** * Reset all pattern state. */ private void resetPattern() { mPattern.clear(); clearPatternDrawLookup(); mPatternDisplayMode = DisplayMode.Correct; invalidate(); } /** * Clear the pattern lookup table. */ private void clearPatternDrawLookup() { for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { mPatternDrawLookup[i][j] = false; } } } /** * Disable input (for instance when displaying a message that will * timeout so user doesn't get view into messy state). */ public void disableInput() { mInputEnabled = false; } /** * Enable input. */ public void enableInput() { mInputEnabled = true; } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { final int width = w - mPaddingLeft - mPaddingRight; mSquareWidth = width / 3.0f; final int height = h - mPaddingTop - mPaddingBottom; mSquareHeight = height / 3.0f; } private int resolveMeasured(int measureSpec, int desired) { int result = 0; int specSize = MeasureSpec.getSize(measureSpec); switch (MeasureSpec.getMode(measureSpec)) { case MeasureSpec.UNSPECIFIED: result = desired; break; case MeasureSpec.AT_MOST: result = Math.max(specSize, desired); break; case MeasureSpec.EXACTLY: default: result = specSize; } return result; } @Override protected int getSuggestedMinimumWidth() { // View should be large enough to contain 3 side-by-side target bitmaps return 3 * mBitmapWidth; } @Override protected int getSuggestedMinimumHeight() { // View should be large enough to contain 3 side-by-side target bitmaps return 3 * mBitmapWidth; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { final int minimumWidth = getSuggestedMinimumWidth(); final int minimumHeight = getSuggestedMinimumHeight(); int viewWidth = resolveMeasured(widthMeasureSpec, minimumWidth); int viewHeight = resolveMeasured(heightMeasureSpec, minimumHeight); switch (mAspect) { case ASPECT_SQUARE: viewWidth = viewHeight = Math.min(viewWidth, viewHeight); break; case ASPECT_LOCK_WIDTH: viewHeight = Math.min(viewWidth, viewHeight); break; case ASPECT_LOCK_HEIGHT: viewWidth = Math.min(viewWidth, viewHeight); break; } // Log.v(TAG, "LockPatternView dimensions: " + viewWidth + "x" + viewHeight); setMeasuredDimension(viewWidth, viewHeight); } /** * Determines whether the point x, y will add a new point to the current * pattern (in addition to finding the cell, also makes heuristic choices * such as filling in gaps based on current pattern). * @param x The x coordinate. * @param y The y coordinate. */ private Cell detectAndAddHit(float x, float y) { final Cell cell = checkForNewHit(x, y); if (cell != null) { // check for gaps in existing pattern Cell fillInGapCell = null; final ArrayList pattern = mPattern; if (!pattern.isEmpty()) { final Cell lastCell = pattern.get(pattern.size() - 1); int dRow = cell.row - lastCell.row; int dColumn = cell.column - lastCell.column; int fillInRow = lastCell.row; int fillInColumn = lastCell.column; if (Math.abs(dRow) == 2 && Math.abs(dColumn) != 1) { fillInRow = lastCell.row + ((dRow > 0) ? 1 : -1); } if (Math.abs(dColumn) == 2 && Math.abs(dRow) != 1) { fillInColumn = lastCell.column + ((dColumn > 0) ? 1 : -1); } fillInGapCell = Cell.of(fillInRow, fillInColumn); } if (fillInGapCell != null && !mPatternDrawLookup[fillInGapCell.row][fillInGapCell.column]) { addCellToPattern(fillInGapCell); } addCellToPattern(cell); if (mEnableHapticFeedback) { performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY, HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING | HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING); } return cell; } return null; } private void addCellToPattern(Cell newCell) { mPatternDrawLookup[newCell.getRow()][newCell.getColumn()] = true; mPattern.add(newCell); notifyCellAdded(); } // helper method to find which cell a point maps to private Cell checkForNewHit(float x, float y) { final int rowHit = getRowHit(y); if (rowHit < 0) { return null; } final int columnHit = getColumnHit(x); if (columnHit < 0) { return null; } if (mPatternDrawLookup[rowHit][columnHit]) { return null; } return Cell.of(rowHit, columnHit); } /** * Helper method to find the row that y falls into. * @param y The y coordinate * @return The row that y falls in, or -1 if it falls in no row. */ private int getRowHit(float y) { final float squareHeight = mSquareHeight; float hitSize = squareHeight * mHitFactor; float offset = mPaddingTop + (squareHeight - hitSize) / 2f; for (int i = 0; i < 3; i++) { final float hitTop = offset + squareHeight * i; if (y >= hitTop && y <= hitTop + hitSize) { return i; } } return -1; } /** * Helper method to find the column x fallis into. * @param x The x coordinate. * @return The column that x falls in, or -1 if it falls in no column. */ private int getColumnHit(float x) { final float squareWidth = mSquareWidth; float hitSize = squareWidth * mHitFactor; float offset = mPaddingLeft + (squareWidth - hitSize) / 2f; for (int i = 0; i < 3; i++) { final float hitLeft = offset + squareWidth * i; if (x >= hitLeft && x <= hitLeft + hitSize) { return i; } } return -1; } @Override public boolean onHoverEvent(MotionEvent event) { if (AccessibilityManager.getInstance(mContext).isTouchExplorationEnabled()) { final int action = event.getAction(); switch (action) { case MotionEvent.ACTION_HOVER_ENTER: event.setAction(MotionEvent.ACTION_DOWN); break; case MotionEvent.ACTION_HOVER_MOVE: event.setAction(MotionEvent.ACTION_MOVE); break; case MotionEvent.ACTION_HOVER_EXIT: event.setAction(MotionEvent.ACTION_UP); break; } onTouchEvent(event); event.setAction(action); } return super.onHoverEvent(event); } @Override public boolean onTouchEvent(MotionEvent event) { if (!mInputEnabled || !isEnabled()) { return false; } switch(event.getAction()) { case MotionEvent.ACTION_DOWN: handleActionDown(event); return true; case MotionEvent.ACTION_UP: handleActionUp(event); return true; case MotionEvent.ACTION_MOVE: handleActionMove(event); return true; case MotionEvent.ACTION_CANCEL: if (mPatternInProgress) { mPatternInProgress = false; resetPattern(); notifyPatternCleared(); } if (PROFILE_DRAWING) { if (mDrawingProfilingStarted) { Debug.stopMethodTracing(); mDrawingProfilingStarted = false; } } return true; } return false; } private void handleActionMove(MotionEvent event) { // Handle all recent motion events so we don't skip any cells even when the device // is busy... final int historySize = event.getHistorySize(); for (int i = 0; i < historySize + 1; i++) { final float x = i < historySize ? event.getHistoricalX(i) : event.getX(); final float y = i < historySize ? event.getHistoricalY(i) : event.getY(); final int patternSizePreHitDetect = mPattern.size(); Cell hitCell = detectAndAddHit(x, y); final int patternSize = mPattern.size(); if (hitCell != null && patternSize == 1) { mPatternInProgress = true; notifyPatternStarted(); } // note current x and y for rubber banding of in progress patterns final float dx = Math.abs(x - mInProgressX); final float dy = Math.abs(y - mInProgressY); if (dx + dy > mSquareWidth * 0.01f) { float oldX = mInProgressX; float oldY = mInProgressY; mInProgressX = x; mInProgressY = y; if (mPatternInProgress && patternSize > 0) { final ArrayList pattern = mPattern; final float radius = mSquareWidth * mDiameterFactor * 0.5f; final Cell lastCell = pattern.get(patternSize - 1); float startX = getCenterXForColumn(lastCell.column); float startY = getCenterYForRow(lastCell.row); float left; float top; float right; float bottom; final Rect invalidateRect = mInvalidate; if (startX < x) { left = startX; right = x; } else { left = x; right = startX; } if (startY < y) { top = startY; bottom = y; } else { top = y; bottom = startY; } // Invalidate between the pattern's last cell and the current location invalidateRect.set((int) (left - radius), (int) (top - radius), (int) (right + radius), (int) (bottom + radius)); if (startX < oldX) { left = startX; right = oldX; } else { left = oldX; right = startX; } if (startY < oldY) { top = startY; bottom = oldY; } else { top = oldY; bottom = startY; } // Invalidate between the pattern's last cell and the previous location invalidateRect.union((int) (left - radius), (int) (top - radius), (int) (right + radius), (int) (bottom + radius)); // Invalidate between the pattern's new cell and the pattern's previous cell if (hitCell != null) { startX = getCenterXForColumn(hitCell.column); startY = getCenterYForRow(hitCell.row); if (patternSize >= 2) { // (re-using hitcell for old cell) hitCell = pattern.get(patternSize - 1 - (patternSize - patternSizePreHitDetect)); oldX = getCenterXForColumn(hitCell.column); oldY = getCenterYForRow(hitCell.row); if (startX < oldX) { left = startX; right = oldX; } else { left = oldX; right = startX; } if (startY < oldY) { top = startY; bottom = oldY; } else { top = oldY; bottom = startY; } } else { left = right = startX; top = bottom = startY; } final float widthOffset = mSquareWidth / 2f; final float heightOffset = mSquareHeight / 2f; invalidateRect.set((int) (left - widthOffset), (int) (top - heightOffset), (int) (right + widthOffset), (int) (bottom + heightOffset)); } invalidate(invalidateRect); } else { invalidate(); } } } } private void sendAccessEvent(int resId) { announceForAccessibility(mContext.getString(resId)); } private void handleActionUp(MotionEvent event) { // report pattern detected if (!mPattern.isEmpty()) { mPatternInProgress = false; notifyPatternDetected(); invalidate(); } if (PROFILE_DRAWING) { if (mDrawingProfilingStarted) { Debug.stopMethodTracing(); mDrawingProfilingStarted = false; } } } private void handleActionDown(MotionEvent event) { resetPattern(); final float x = event.getX(); final float y = event.getY(); final Cell hitCell = detectAndAddHit(x, y); if (hitCell != null) { mPatternInProgress = true; mPatternDisplayMode = DisplayMode.Correct; notifyPatternStarted(); } else if (mPatternInProgress) { mPatternInProgress = false; notifyPatternCleared(); } if (hitCell != null) { final float startX = getCenterXForColumn(hitCell.column); final float startY = getCenterYForRow(hitCell.row); final float widthOffset = mSquareWidth / 2f; final float heightOffset = mSquareHeight / 2f; invalidate((int) (startX - widthOffset), (int) (startY - heightOffset), (int) (startX + widthOffset), (int) (startY + heightOffset)); } mInProgressX = x; mInProgressY = y; if (PROFILE_DRAWING) { if (!mDrawingProfilingStarted) { Debug.startMethodTracing("LockPatternDrawing"); mDrawingProfilingStarted = true; } } } private float getCenterXForColumn(int column) { return mPaddingLeft + column * mSquareWidth + mSquareWidth / 2f; } private float getCenterYForRow(int row) { return mPaddingTop + row * mSquareHeight + mSquareHeight / 2f; } @Override protected void onDraw(Canvas canvas) { final ArrayList 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(); // draw the circles final int paddingTop = mPaddingTop; final int paddingLeft = mPaddingLeft; 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]); } } // 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 if (drawPath) { for (int i = 0; i < count - 1; i++) { Cell cell = pattern.get(i); Cell next = pattern.get(i + 1); // only draw the part of the pattern stored in // the lookup table (this is only different in the case // of animation). if (!drawLookup[next.row][next.column]) { break; } float leftX = paddingLeft + cell.column * squareWidth; float topY = paddingTop + cell.row * squareHeight; drawArrow(canvas, leftX, topY, cell, next); } } 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); } canvas.drawPath(currentPath, mPathPaint); } mPaint.setFilterBitmap(oldFlag); // restore default flag } private void drawArrow(Canvas canvas, float leftX, float topY, Cell start, Cell end) { boolean green = mPatternDisplayMode != DisplayMode.Wrong; final int endRow = end.row; final int startRow = start.row; final int endColumn = end.column; final int startColumn = start.column; // offsets for centering the bitmap in the cell final int offsetX = ((int) mSquareWidth - mBitmapWidth) / 2; final int offsetY = ((int) mSquareHeight - mBitmapHeight) / 2; // compute transform to place arrow bitmaps at correct angle inside circle. // This assumes that the arrow image is drawn at 12:00 with it's top edge // coincident with the circle bitmap's top edge. Bitmap arrow = green ? mBitmapArrowGreenUp : mBitmapArrowRedUp; final int cellWidth = mBitmapWidth; final int cellHeight = mBitmapHeight; // the up arrow bitmap is at 12:00, so find the rotation from x axis and add 90 degrees. final float theta = (float) Math.atan2( (double) (endRow - startRow), (double) (endColumn - startColumn)); final float angle = (float) Math.toDegrees(theta) + 90.0f; // compose matrix float sx = Math.min(mSquareWidth / mBitmapWidth, 1.0f); float sy = Math.min(mSquareHeight / mBitmapHeight, 1.0f); mArrowMatrix.setTranslate(leftX + offsetX, topY + offsetY); // transform to cell position mArrowMatrix.preTranslate(mBitmapWidth/2, mBitmapHeight/2); mArrowMatrix.preScale(sx, sy); mArrowMatrix.preTranslate(-mBitmapWidth/2, -mBitmapHeight/2); mArrowMatrix.preRotate(angle, cellWidth / 2.0f, cellHeight / 2.0f); // rotate about cell center mArrowMatrix.preTranslate((cellWidth - arrow.getWidth()) / 2.0f, 0.0f); // translate to 12:00 pos canvas.drawBitmap(arrow, mArrowMatrix, mPaint); } /** * @param canvas * @param leftX * @param topY * @param partOfPattern Whether this circle is part of the pattern. */ private void drawCircle(Canvas canvas, int leftX, int topY, boolean partOfPattern) { Bitmap outerCircle; Bitmap innerCircle; if (!partOfPattern || (mInStealthMode && mPatternDisplayMode != DisplayMode.Wrong)) { // unselected circle outerCircle = mBitmapCircleDefault; innerCircle = mBitmapBtnDefault; } else if (mPatternInProgress) { // user is in middle of drawing a pattern outerCircle = mBitmapCircleGreen; innerCircle = mBitmapBtnTouched; } else if (mPatternDisplayMode == DisplayMode.Wrong) { // the pattern is wrong outerCircle = mBitmapCircleRed; innerCircle = mBitmapBtnDefault; } else if (mPatternDisplayMode == DisplayMode.Correct || mPatternDisplayMode == DisplayMode.Animate) { // the pattern is correct outerCircle = mBitmapCircleGreen; innerCircle = mBitmapBtnDefault; } else { throw new IllegalStateException("unknown display mode " + mPatternDisplayMode); } final int width = mBitmapWidth; final int height = mBitmapHeight; final float squareWidth = mSquareWidth; final float squareHeight = mSquareHeight; int offsetX = (int) ((squareWidth - width) / 2f); int offsetY = (int) ((squareHeight - height) / 2f); // Allow circles to shrink if the view is too small to hold them. float sx = Math.min(mSquareWidth / mBitmapWidth, 1.0f); float sy = Math.min(mSquareHeight / mBitmapHeight, 1.0f); mCircleMatrix.setTranslate(leftX + offsetX, topY + offsetY); mCircleMatrix.preTranslate(mBitmapWidth/2, mBitmapHeight/2); mCircleMatrix.preScale(sx, sy); mCircleMatrix.preTranslate(-mBitmapWidth/2, -mBitmapHeight/2); canvas.drawBitmap(outerCircle, mCircleMatrix, mPaint); canvas.drawBitmap(innerCircle, mCircleMatrix, mPaint); } @Override protected Parcelable onSaveInstanceState() { Parcelable superState = super.onSaveInstanceState(); return new SavedState(superState, LockPatternUtils.patternToString(mPattern), mPatternDisplayMode.ordinal(), mInputEnabled, mInStealthMode, mEnableHapticFeedback); } @Override protected void onRestoreInstanceState(Parcelable state) { final SavedState ss = (SavedState) state; super.onRestoreInstanceState(ss.getSuperState()); setPattern( DisplayMode.Correct, LockPatternUtils.stringToPattern(ss.getSerializedPattern())); mPatternDisplayMode = DisplayMode.values()[ss.getDisplayMode()]; mInputEnabled = ss.isInputEnabled(); mInStealthMode = ss.isInStealthMode(); mEnableHapticFeedback = ss.isTactileFeedbackEnabled(); } /** * The parecelable for saving and restoring a lock pattern view. */ private static class SavedState extends BaseSavedState { private final String mSerializedPattern; private final int mDisplayMode; private final boolean mInputEnabled; private final boolean mInStealthMode; private final boolean mTactileFeedbackEnabled; /** * Constructor called from {@link LockPatternView#onSaveInstanceState()} */ private SavedState(Parcelable superState, String serializedPattern, int displayMode, boolean inputEnabled, boolean inStealthMode, boolean tactileFeedbackEnabled) { super(superState); mSerializedPattern = serializedPattern; mDisplayMode = displayMode; mInputEnabled = inputEnabled; mInStealthMode = inStealthMode; mTactileFeedbackEnabled = tactileFeedbackEnabled; } /** * Constructor called from {@link #CREATOR} */ private SavedState(Parcel in) { super(in); mSerializedPattern = in.readString(); mDisplayMode = in.readInt(); mInputEnabled = (Boolean) in.readValue(null); mInStealthMode = (Boolean) in.readValue(null); mTactileFeedbackEnabled = (Boolean) in.readValue(null); } public String getSerializedPattern() { return mSerializedPattern; } public int getDisplayMode() { return mDisplayMode; } public boolean isInputEnabled() { return mInputEnabled; } public boolean isInStealthMode() { return mInStealthMode; } public boolean isTactileFeedbackEnabled(){ return mTactileFeedbackEnabled; } @Override public void writeToParcel(Parcel dest, int flags) { super.writeToParcel(dest, flags); dest.writeString(mSerializedPattern); dest.writeInt(mDisplayMode); dest.writeValue(mInputEnabled); dest.writeValue(mInStealthMode); dest.writeValue(mTactileFeedbackEnabled); } public static final Parcelable.Creator CREATOR = new Creator() { public SavedState createFromParcel(Parcel in) { return new SavedState(in); } public SavedState[] newArray(int size) { return new SavedState[size]; } }; } }