LockPatternView.java revision 54b6cfa9a9e5b861a9930af873580d6dc20f773c
1/* 2 * Copyright (C) 2007 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.internal.widget; 18 19 20import com.android.internal.R; 21 22import android.content.Context; 23import android.graphics.Canvas; 24import android.graphics.Color; 25import android.graphics.Matrix; 26import android.graphics.Paint; 27import android.graphics.Path; 28import android.graphics.Rect; 29import android.graphics.Bitmap; 30import android.graphics.BitmapFactory; 31import android.os.Parcel; 32import android.os.Parcelable; 33import android.os.SystemClock; 34import android.os.Debug; 35import android.util.AttributeSet; 36import android.view.MotionEvent; 37import android.view.View; 38import android.view.WindowManager; 39 40import java.util.ArrayList; 41import java.util.List; 42 43/** 44 * Displays and detects the user's unlock attempt, which is a drag of a finger 45 * across 9 regions of the screen. 46 * 47 * Is also capable of displaying a static pattern in "in progress", "wrong" or 48 * "correct" states. 49 */ 50public class LockPatternView extends View { 51 private static final boolean PROFILE_DRAWING = false; 52 private boolean mDrawingProfilingStarted = false; 53 54 private Paint mPaint = new Paint(); 55 private Paint mPathPaint = new Paint(); 56 57 // TODO: make this common with PhoneWindow 58 static final int STATUS_BAR_HEIGHT = 25; 59 60 /** 61 * How many milliseconds we spend animating each circle of a lock pattern 62 * if the animating mode is set. The entire animation should take this 63 * constant * the length of the pattern to complete. 64 */ 65 private static final int MILLIS_PER_CIRCLE_ANIMATING = 700; 66 67 private OnPatternListener mOnPatternListener; 68 private ArrayList<Cell> mPattern = new ArrayList<Cell>(9); 69 70 /** 71 * Lookup table for the circles of the pattern we are currently drawing. 72 * This will be the cells of the complete pattern unless we are animating, 73 * in which case we use this to hold the cells we are drawing for the in 74 * progress animation. 75 */ 76 private boolean[][] mPatternDrawLookup = new boolean[3][3]; 77 78 /** 79 * the in progress point: 80 * - during interaction: where the user's finger is 81 * - during animation: the current tip of the animating line 82 */ 83 private float mInProgressX = -1; 84 private float mInProgressY = -1; 85 86 private long mAnimatingPeriodStart; 87 88 private DisplayMode mPatternDisplayMode = DisplayMode.Correct; 89 private boolean mInputEnabled = true; 90 private boolean mInStealthMode = false; 91 private boolean mPatternInProgress = false; 92 93 private float mDiameterFactor = 0.5f; 94 private float mHitFactor = 0.6f; 95 96 private float mSquareWidth; 97 private float mSquareHeight; 98 99 private Bitmap mBitmapBtnDefault; 100 private Bitmap mBitmapBtnTouched; 101 private Bitmap mBitmapCircleDefault; 102 private Bitmap mBitmapCircleGreen; 103 private Bitmap mBitmapCircleRed; 104 105 private Bitmap mBitmapArrowGreenUp; 106 private Bitmap mBitmapArrowRedUp; 107 108 private final Path mCurrentPath = new Path(); 109 private final Rect mInvalidate = new Rect(); 110 111 private int mBitmapWidth; 112 private int mBitmapHeight; 113 114 115 /** 116 * Represents a cell in the 3 X 3 matrix of the unlock pattern view. 117 */ 118 public static class Cell { 119 int row; 120 int column; 121 122 // keep # objects limited to 9 123 static Cell[][] sCells = new Cell[3][3]; 124 static { 125 for (int i = 0; i < 3; i++) { 126 for (int j = 0; j < 3; j++) { 127 sCells[i][j] = new Cell(i, j); 128 } 129 } 130 } 131 132 /** 133 * @param row The row of the cell. 134 * @param column The column of the cell. 135 */ 136 private Cell(int row, int column) { 137 checkRange(row, column); 138 this.row = row; 139 this.column = column; 140 } 141 142 public int getRow() { 143 return row; 144 } 145 146 public int getColumn() { 147 return column; 148 } 149 150 /** 151 * @param row The row of the cell. 152 * @param column The column of the cell. 153 */ 154 public static synchronized Cell of(int row, int column) { 155 checkRange(row, column); 156 return sCells[row][column]; 157 } 158 159 private static void checkRange(int row, int column) { 160 if (row < 0 || row > 2) { 161 throw new IllegalArgumentException("row must be in range 0-2"); 162 } 163 if (column < 0 || column > 2) { 164 throw new IllegalArgumentException("column must be in range 0-2"); 165 } 166 } 167 168 public String toString() { 169 return "(row=" + row + ",clmn=" + column + ")"; 170 } 171 } 172 173 /** 174 * How to display the current pattern. 175 */ 176 public enum DisplayMode { 177 178 /** 179 * The pattern drawn is correct (i.e draw it in a friendly color) 180 */ 181 Correct, 182 183 /** 184 * Animate the pattern (for demo, and help). 185 */ 186 Animate, 187 188 /** 189 * The pattern is wrong (i.e draw a foreboding color) 190 */ 191 Wrong 192 } 193 194 /** 195 * The call back interface for detecting patterns entered by the user. 196 */ 197 public static interface OnPatternListener { 198 199 /** 200 * A new pattern has begun. 201 */ 202 void onPatternStart(); 203 204 /** 205 * The pattern was cleared. 206 */ 207 void onPatternCleared(); 208 209 /** 210 * A pattern was detected from the user. 211 * @param pattern The pattern. 212 */ 213 void onPatternDetected(List<Cell> pattern); 214 } 215 216 public LockPatternView(Context context) { 217 this(context, null); 218 } 219 220 public LockPatternView(Context context, AttributeSet attrs) { 221 super(context, attrs); 222 223 setClickable(true); 224 225 mPathPaint.setAntiAlias(true); 226 mPathPaint.setDither(true); 227 mPathPaint.setColor(Color.WHITE); // TODO this should be from the style 228 mPathPaint.setAlpha(128); 229 mPathPaint.setStyle(Paint.Style.STROKE); 230 mPathPaint.setStrokeJoin(Paint.Join.ROUND); 231 mPathPaint.setStrokeCap(Paint.Cap.ROUND); 232 233 // lot's of bitmaps! 234 mBitmapBtnDefault = getBitmapFor(R.drawable.btn_code_lock_default); 235 mBitmapBtnTouched = getBitmapFor(R.drawable.btn_code_lock_touched); 236 mBitmapCircleDefault = getBitmapFor(R.drawable.indicator_code_lock_point_area_default); 237 mBitmapCircleGreen = getBitmapFor(R.drawable.indicator_code_lock_point_area_green); 238 mBitmapCircleRed = getBitmapFor(R.drawable.indicator_code_lock_point_area_red); 239 240 mBitmapArrowGreenUp = getBitmapFor(R.drawable.indicator_code_lock_drag_direction_green_up); 241 mBitmapArrowRedUp = getBitmapFor(R.drawable.indicator_code_lock_drag_direction_red_up); 242 243 // we assume all bitmaps have the same size 244 mBitmapWidth = mBitmapBtnDefault.getWidth(); 245 mBitmapHeight = mBitmapBtnDefault.getHeight(); 246 } 247 248 private Bitmap getBitmapFor(int resId) { 249 return BitmapFactory.decodeResource(getContext().getResources(), resId); 250 } 251 252 /** 253 * @return Whether the view is in stealth mode. 254 */ 255 public boolean isInStealthMode() { 256 return mInStealthMode; 257 } 258 259 /** 260 * Set whether the view is in stealth mode. If true, there will be no 261 * visible feedback as the user enters the pattern. 262 * 263 * @param inStealthMode Whether in stealth mode. 264 */ 265 public void setInStealthMode(boolean inStealthMode) { 266 mInStealthMode = inStealthMode; 267 } 268 269 /** 270 * Set the call back for pattern detection. 271 * @param onPatternListener The call back. 272 */ 273 public void setOnPatternListener( 274 OnPatternListener onPatternListener) { 275 mOnPatternListener = onPatternListener; 276 } 277 278 /** 279 * Set the pattern explicitely (rather than waiting for the user to input 280 * a pattern). 281 * @param displayMode How to display the pattern. 282 * @param pattern The pattern. 283 */ 284 public void setPattern(DisplayMode displayMode, List<Cell> pattern) { 285 mPattern.clear(); 286 mPattern.addAll(pattern); 287 clearPatternDrawLookup(); 288 for (Cell cell : pattern) { 289 mPatternDrawLookup[cell.getRow()][cell.getColumn()] = true; 290 } 291 292 setDisplayMode(displayMode); 293 } 294 295 /** 296 * Set the display mode of the current pattern. This can be useful, for 297 * instance, after detecting a pattern to tell this view whether change the 298 * in progress result to correct or wrong. 299 * @param displayMode The display mode. 300 */ 301 public void setDisplayMode(DisplayMode displayMode) { 302 mPatternDisplayMode = displayMode; 303 if (displayMode == DisplayMode.Animate) { 304 if (mPattern.size() == 0) { 305 throw new IllegalStateException("you must have a pattern to " 306 + "animate if you want to set the display mode to animate"); 307 } 308 mAnimatingPeriodStart = SystemClock.elapsedRealtime(); 309 final Cell first = mPattern.get(0); 310 mInProgressX = getCenterXForColumn(first.getColumn()); 311 mInProgressY = getCenterYForRow(first.getRow()); 312 clearPatternDrawLookup(); 313 } 314 invalidate(); 315 } 316 317 /** 318 * Clear the pattern. 319 */ 320 public void clearPattern() { 321 resetPattern(); 322 } 323 324 /** 325 * Reset all pattern state. 326 */ 327 private void resetPattern() { 328 mPattern.clear(); 329 clearPatternDrawLookup(); 330 mPatternDisplayMode = DisplayMode.Correct; 331 invalidate(); 332 } 333 334 /** 335 * Clear the pattern lookup table. 336 */ 337 private void clearPatternDrawLookup() { 338 for (int i = 0; i < 3; i++) { 339 for (int j = 0; j < 3; j++) { 340 mPatternDrawLookup[i][j] = false; 341 } 342 } 343 } 344 345 /** 346 * Disable input (for instance when displaying a message that will 347 * timeout so user doesn't get view into messy state). 348 */ 349 public void disableInput() { 350 mInputEnabled = false; 351 } 352 353 /** 354 * Enable input. 355 */ 356 public void enableInput() { 357 mInputEnabled = true; 358 } 359 360 @Override 361 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 362 final int width = w - mPaddingLeft - mPaddingRight; 363 mSquareWidth = width / 3.0f; 364 365 final int height = h - mPaddingTop - mPaddingBottom; 366 mSquareHeight = height / 3.0f; 367 } 368 369 @Override 370 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 371 final WindowManager wm = (WindowManager) getContext() 372 .getSystemService(Context.WINDOW_SERVICE); 373 final int width = wm.getDefaultDisplay().getWidth(); 374 final int height = wm.getDefaultDisplay().getHeight(); 375 int squareSide = Math.min(width, height); 376 377 // if in landscape... 378 if (width > height) { 379 squareSide -= STATUS_BAR_HEIGHT; 380 } 381 382 setMeasuredDimension(squareSide, squareSide); 383 } 384 385 /** 386 * Determines whether the point x, y will add a new point to the current 387 * pattern (in addition to finding the cell, also makes heuristic choices 388 * such as filling in gaps based on current pattern). 389 * @param x The x coordinate. 390 * @param y The y coordinate. 391 */ 392 private Cell detectAndAddHit(float x, float y) { 393 final Cell cell = checkForNewHit(x, y); 394 if (cell != null) { 395 396 // check for gaps in existing pattern 397 Cell fillInGapCell = null; 398 final ArrayList<Cell> pattern = mPattern; 399 if (!pattern.isEmpty()) { 400 final Cell lastCell = pattern.get(pattern.size() - 1); 401 int dRow = cell.row - lastCell.row; 402 int dColumn = cell.column - lastCell.column; 403 404 int fillInRow = lastCell.row; 405 int fillInColumn = lastCell.column; 406 407 if (Math.abs(dRow) == 2 && Math.abs(dColumn) != 1) { 408 fillInRow = lastCell.row + ((dRow > 0) ? 1 : -1); 409 } 410 411 if (Math.abs(dColumn) == 2 && Math.abs(dRow) != 1) { 412 fillInColumn = lastCell.column + ((dColumn > 0) ? 1 : -1); 413 } 414 415 fillInGapCell = Cell.of(fillInRow, fillInColumn); 416 } 417 418 if (fillInGapCell != null && 419 !mPatternDrawLookup[fillInGapCell.row][fillInGapCell.column]) { 420 addCellToPattern(fillInGapCell); 421 } 422 addCellToPattern(cell); 423 return cell; 424 } 425 return null; 426 } 427 428 private void addCellToPattern(Cell newCell) { 429 mPatternDrawLookup[newCell.getRow()][newCell.getColumn()] = true; 430 mPattern.add(newCell); 431 } 432 433 // helper method to find which cell a point maps to 434 private Cell checkForNewHit(float x, float y) { 435 436 final int rowHit = getRowHit(y); 437 if (rowHit < 0) { 438 return null; 439 } 440 final int columnHit = getColumnHit(x); 441 if (columnHit < 0) { 442 return null; 443 } 444 445 if (mPatternDrawLookup[rowHit][columnHit]) { 446 return null; 447 } 448 return Cell.of(rowHit, columnHit); 449 } 450 451 /** 452 * Helper method to find the row that y falls into. 453 * @param y The y coordinate 454 * @return The row that y falls in, or -1 if it falls in no row. 455 */ 456 private int getRowHit(float y) { 457 458 final float squareHeight = mSquareHeight; 459 float hitSize = squareHeight * mHitFactor; 460 461 float offset = mPaddingTop + (squareHeight - hitSize) / 2f; 462 for (int i = 0; i < 3; i++) { 463 464 final float hitTop = offset + squareHeight * i; 465 if (y >= hitTop && y <= hitTop + hitSize) { 466 return i; 467 } 468 } 469 return -1; 470 } 471 472 /** 473 * Helper method to find the column x fallis into. 474 * @param x The x coordinate. 475 * @return The column that x falls in, or -1 if it falls in no column. 476 */ 477 private int getColumnHit(float x) { 478 final float squareWidth = mSquareWidth; 479 float hitSize = squareWidth * mHitFactor; 480 481 float offset = mPaddingLeft + (squareWidth - hitSize) / 2f; 482 for (int i = 0; i < 3; i++) { 483 484 final float hitLeft = offset + squareWidth * i; 485 if (x >= hitLeft && x <= hitLeft + hitSize) { 486 return i; 487 } 488 } 489 return -1; 490 } 491 492 @Override 493 public boolean onTouchEvent(MotionEvent motionEvent) { 494 if (!mInputEnabled || !isEnabled()) { 495 return false; 496 } 497 498 final float x = motionEvent.getX(); 499 final float y = motionEvent.getY(); 500 Cell hitCell; 501 switch(motionEvent.getAction()) { 502 case MotionEvent.ACTION_DOWN: 503 resetPattern(); 504 hitCell = detectAndAddHit(x, y); 505 if (hitCell != null && mOnPatternListener != null) { 506 mPatternInProgress = true; 507 mPatternDisplayMode = DisplayMode.Correct; 508 mOnPatternListener.onPatternStart(); 509 } else if (mOnPatternListener != null) { 510 mPatternInProgress = false; 511 mOnPatternListener.onPatternCleared(); 512 } 513 if (hitCell != null) { 514 final float startX = getCenterXForColumn(hitCell.column); 515 final float startY = getCenterYForRow(hitCell.row); 516 517 final float widthOffset = mSquareWidth / 2f; 518 final float heightOffset = mSquareHeight / 2f; 519 520 invalidate((int) (startX - widthOffset), (int) (startY - heightOffset), 521 (int) (startX + widthOffset), (int) (startY + heightOffset)); 522 } 523 mInProgressX = x; 524 mInProgressY = y; 525 if (PROFILE_DRAWING) { 526 if (!mDrawingProfilingStarted) { 527 Debug.startMethodTracing("LockPatternDrawing"); 528 mDrawingProfilingStarted = true; 529 } 530 } 531 return true; 532 case MotionEvent.ACTION_UP: 533 // report pattern detected 534 if (!mPattern.isEmpty() && mOnPatternListener != null) { 535 mPatternInProgress = false; 536 mOnPatternListener.onPatternDetected(mPattern); 537 invalidate(); 538 } 539 if (PROFILE_DRAWING) { 540 if (mDrawingProfilingStarted) { 541 Debug.stopMethodTracing(); 542 mDrawingProfilingStarted = false; 543 } 544 } 545 return true; 546 case MotionEvent.ACTION_MOVE: 547 final int patternSizePreHitDetect = mPattern.size(); 548 hitCell = detectAndAddHit(x, y); 549 final int patternSize = mPattern.size(); 550 if (hitCell != null && (mOnPatternListener != null) && (patternSize == 1)) { 551 mPatternInProgress = true; 552 mOnPatternListener.onPatternStart(); 553 } 554 // note current x and y for rubber banding of in progress 555 // patterns 556 final float dx = Math.abs(x - mInProgressX); 557 final float dy = Math.abs(y - mInProgressY); 558 if (dx + dy > mSquareWidth * 0.01f) { 559 float oldX = mInProgressX; 560 float oldY = mInProgressY; 561 562 mInProgressX = x; 563 mInProgressY = y; 564 565 if (mPatternInProgress) { 566 final ArrayList<Cell> pattern = mPattern; 567 final float radius = mSquareWidth * mDiameterFactor * 0.5f; 568 569 Cell cell = pattern.get(patternSize - 1); 570 571 float startX = getCenterXForColumn(cell.column); 572 float startY = getCenterYForRow(cell.row); 573 574 float left; 575 float top; 576 float right; 577 float bottom; 578 579 final Rect invalidateRect = mInvalidate; 580 581 if (startX < x) { 582 left = startX; 583 right = x; 584 } else { 585 left = x; 586 right = startX; 587 } 588 589 if (startY < y) { 590 top = startY; 591 bottom = y; 592 } else { 593 top = y; 594 bottom = startY; 595 } 596 597 // Invalidate between the pattern's last cell and the current location 598 invalidateRect.set((int) (left - radius), (int) (top - radius), 599 (int) (right + radius), (int) (bottom + radius)); 600 601 if (startX < oldX) { 602 left = startX; 603 right = oldX; 604 } else { 605 left = oldX; 606 right = startX; 607 } 608 609 if (startY < oldY) { 610 top = startY; 611 bottom = oldY; 612 } else { 613 top = oldY; 614 bottom = startY; 615 } 616 617 // Invalidate between the pattern's last cell and the previous location 618 invalidateRect.union((int) (left - radius), (int) (top - radius), 619 (int) (right + radius), (int) (bottom + radius)); 620 621 // Invalidate between the pattern's new cell and the pattern's previous cell 622 if (hitCell != null) { 623 startX = getCenterXForColumn(hitCell.column); 624 startY = getCenterYForRow(hitCell.row); 625 626 if (patternSize >= 2) { 627 // (re-using hitcell for old cell) 628 hitCell = pattern.get(patternSize - 1 - (patternSize - patternSizePreHitDetect)); 629 oldX = getCenterXForColumn(hitCell.column); 630 oldY = getCenterYForRow(hitCell.row); 631 632 if (startX < oldX) { 633 left = startX; 634 right = oldX; 635 } else { 636 left = oldX; 637 right = startX; 638 } 639 640 if (startY < oldY) { 641 top = startY; 642 bottom = oldY; 643 } else { 644 top = oldY; 645 bottom = startY; 646 } 647 } else { 648 left = right = startX; 649 top = bottom = startY; 650 } 651 652 final float widthOffset = mSquareWidth / 2f; 653 final float heightOffset = mSquareHeight / 2f; 654 655 invalidateRect.set((int) (left - widthOffset), 656 (int) (top - heightOffset), (int) (right + widthOffset), 657 (int) (bottom + heightOffset)); 658 } 659 660 invalidate(invalidateRect); 661 } else { 662 invalidate(); 663 } 664 } 665 return true; 666 case MotionEvent.ACTION_CANCEL: 667 resetPattern(); 668 if (mOnPatternListener != null) { 669 mPatternInProgress = false; 670 mOnPatternListener.onPatternCleared(); 671 } 672 if (PROFILE_DRAWING) { 673 if (mDrawingProfilingStarted) { 674 Debug.stopMethodTracing(); 675 mDrawingProfilingStarted = false; 676 } 677 } 678 return true; 679 } 680 return false; 681 } 682 683 private float getCenterXForColumn(int column) { 684 return mPaddingLeft + column * mSquareWidth + mSquareWidth / 2f; 685 } 686 687 private float getCenterYForRow(int row) { 688 return mPaddingTop + row * mSquareHeight + mSquareHeight / 2f; 689 } 690 691 @Override 692 protected void onDraw(Canvas canvas) { 693 final ArrayList<Cell> pattern = mPattern; 694 final int count = pattern.size(); 695 final boolean[][] drawLookup = mPatternDrawLookup; 696 697 if (mPatternDisplayMode == DisplayMode.Animate) { 698 699 // figure out which circles to draw 700 701 // + 1 so we pause on complete pattern 702 final int oneCycle = (count + 1) * MILLIS_PER_CIRCLE_ANIMATING; 703 final int spotInCycle = (int) (SystemClock.elapsedRealtime() - 704 mAnimatingPeriodStart) % oneCycle; 705 final int numCircles = spotInCycle / MILLIS_PER_CIRCLE_ANIMATING; 706 707 clearPatternDrawLookup(); 708 for (int i = 0; i < numCircles; i++) { 709 final Cell cell = pattern.get(i); 710 drawLookup[cell.getRow()][cell.getColumn()] = true; 711 } 712 713 // figure out in progress portion of ghosting line 714 715 final boolean needToUpdateInProgressPoint = numCircles > 0 716 && numCircles < count; 717 718 if (needToUpdateInProgressPoint) { 719 final float percentageOfNextCircle = 720 ((float) (spotInCycle % MILLIS_PER_CIRCLE_ANIMATING)) / 721 MILLIS_PER_CIRCLE_ANIMATING; 722 723 final Cell currentCell = pattern.get(numCircles - 1); 724 final float centerX = getCenterXForColumn(currentCell.column); 725 final float centerY = getCenterYForRow(currentCell.row); 726 727 final Cell nextCell = pattern.get(numCircles); 728 final float dx = percentageOfNextCircle * 729 (getCenterXForColumn(nextCell.column) - centerX); 730 final float dy = percentageOfNextCircle * 731 (getCenterYForRow(nextCell.row) - centerY); 732 mInProgressX = centerX + dx; 733 mInProgressY = centerY + dy; 734 } 735 // TODO: Infinite loop here... 736 invalidate(); 737 } 738 739 final float squareWidth = mSquareWidth; 740 final float squareHeight = mSquareHeight; 741 742 float radius = (squareWidth * mDiameterFactor * 0.5f); 743 mPathPaint.setStrokeWidth(radius); 744 745 final Path currentPath = mCurrentPath; 746 currentPath.rewind(); 747 748 // TODO: the path should be created and cached every time we hit-detect a cell 749 // only the last segment of the path should be computed here 750 // draw the path of the pattern (unless the user is in progress, and 751 // we are in stealth mode) 752 final boolean drawPath = (!mInStealthMode || mPatternDisplayMode == DisplayMode.Wrong); 753 if (drawPath) { 754 boolean anyCircles = false; 755 for (int i = 0; i < count; i++) { 756 Cell cell = pattern.get(i); 757 758 // only draw the part of the pattern stored in 759 // the lookup table (this is only different in the case 760 // of animation). 761 if (!drawLookup[cell.row][cell.column]) { 762 break; 763 } 764 anyCircles = true; 765 766 float centerX = getCenterXForColumn(cell.column); 767 float centerY = getCenterYForRow(cell.row); 768 if (i == 0) { 769 currentPath.moveTo(centerX, centerY); 770 } else { 771 currentPath.lineTo(centerX, centerY); 772 } 773 } 774 775 // add last in progress section 776 if ((mPatternInProgress || mPatternDisplayMode == DisplayMode.Animate) 777 && anyCircles) { 778 currentPath.lineTo(mInProgressX, mInProgressY); 779 } 780 canvas.drawPath(currentPath, mPathPaint); 781 } 782 783 // draw the circles 784 final int paddingTop = mPaddingTop; 785 final int paddingLeft = mPaddingLeft; 786 787 for (int i = 0; i < 3; i++) { 788 float topY = paddingTop + i * squareHeight; 789 //float centerY = mPaddingTop + i * mSquareHeight + (mSquareHeight / 2); 790 for (int j = 0; j < 3; j++) { 791 float leftX = paddingLeft + j * squareWidth; 792 drawCircle(canvas, (int) leftX, (int) topY, drawLookup[i][j]); 793 } 794 } 795 796 // draw the arrows associated with the path (unless the user is in progress, and 797 // we are in stealth mode) 798 boolean oldFlag = (mPaint.getFlags() & Paint.FILTER_BITMAP_FLAG) != 0; 799 mPaint.setFilterBitmap(true); // draw with higher quality since we render with transforms 800 if (drawPath) { 801 for (int i = 0; i < count - 1; i++) { 802 Cell cell = pattern.get(i); 803 Cell next = pattern.get(i + 1); 804 805 // only draw the part of the pattern stored in 806 // the lookup table (this is only different in the case 807 // of animation). 808 if (!drawLookup[next.row][next.column]) { 809 break; 810 } 811 812 float leftX = paddingLeft + cell.column * squareWidth; 813 float topY = paddingTop + cell.row * squareHeight; 814 815 drawArrow(canvas, leftX, topY, cell, next); 816 } 817 } 818 mPaint.setFilterBitmap(oldFlag); // restore default flag 819 } 820 821 private void drawArrow(Canvas canvas, float leftX, float topY, Cell start, Cell end) { 822 boolean green = mPatternDisplayMode != DisplayMode.Wrong; 823 824 final int endRow = end.row; 825 final int startRow = start.row; 826 final int endColumn = end.column; 827 final int startColumn = start.column; 828 829 // offsets for centering the bitmap in the cell 830 final int offsetX = ((int) mSquareWidth - mBitmapWidth) / 2; 831 final int offsetY = ((int) mSquareHeight - mBitmapHeight) / 2; 832 833 // compute transform to place arrow bitmaps at correct angle inside circle. 834 // This assumes that the arrow image is drawn at 12:00 with it's top edge 835 // coincident with the circle bitmap's top edge. 836 Bitmap arrow = green ? mBitmapArrowGreenUp : mBitmapArrowRedUp; 837 Matrix matrix = new Matrix(); 838 final int cellWidth = mBitmapCircleDefault.getWidth(); 839 final int cellHeight = mBitmapCircleDefault.getHeight(); 840 841 // the up arrow bitmap is at 12:00, so find the rotation from x axis and add 90 degrees. 842 final float theta = (float) Math.atan2( 843 (double) (endRow - startRow), (double) (endColumn - startColumn)); 844 final float angle = (float) Math.toDegrees(theta) + 90.0f; 845 846 // compose matrix 847 matrix.setTranslate(leftX + offsetX, topY + offsetY); // transform to cell position 848 matrix.preRotate(angle, cellWidth / 2.0f, cellHeight / 2.0f); // rotate about cell center 849 matrix.preTranslate((cellWidth - arrow.getWidth()) / 2.0f, 0.0f); // translate to 12:00 pos 850 canvas.drawBitmap(arrow, matrix, mPaint); 851 } 852 853 /** 854 * @param canvas 855 * @param leftX 856 * @param topY 857 * @param partOfPattern Whether this circle is part of the pattern. 858 */ 859 private void drawCircle(Canvas canvas, int leftX, int topY, boolean partOfPattern) { 860 Bitmap outerCircle; 861 Bitmap innerCircle; 862 863 if (!partOfPattern || (mInStealthMode && mPatternDisplayMode != DisplayMode.Wrong)) { 864 // unselected circle 865 outerCircle = mBitmapCircleDefault; 866 innerCircle = mBitmapBtnDefault; 867 } else if (mPatternInProgress) { 868 // user is in middle of drawing a pattern 869 outerCircle = mBitmapCircleGreen; 870 innerCircle = mBitmapBtnTouched; 871 } else if (mPatternDisplayMode == DisplayMode.Wrong) { 872 // the pattern is wrong 873 outerCircle = mBitmapCircleRed; 874 innerCircle = mBitmapBtnDefault; 875 } else if (mPatternDisplayMode == DisplayMode.Correct || 876 mPatternDisplayMode == DisplayMode.Animate) { 877 // the pattern is correct 878 outerCircle = mBitmapCircleGreen; 879 innerCircle = mBitmapBtnDefault; 880 } else { 881 throw new IllegalStateException("unknown display mode " + mPatternDisplayMode); 882 } 883 884 final int width = mBitmapWidth; 885 final int height = mBitmapHeight; 886 887 final float squareWidth = mSquareWidth; 888 final float squareHeight = mSquareHeight; 889 890 int offsetX = (int) ((squareWidth - width) / 2f); 891 int offsetY = (int) ((squareHeight - height) / 2f); 892 893 canvas.drawBitmap(outerCircle, leftX + offsetX, topY + offsetY, mPaint); 894 canvas.drawBitmap(innerCircle, leftX + offsetX, topY + offsetY, mPaint); 895 } 896 897 @Override 898 protected Parcelable onSaveInstanceState() { 899 Parcelable superState = super.onSaveInstanceState(); 900 return new SavedState(superState, 901 LockPatternUtils.patternToString(mPattern), 902 mPatternDisplayMode.ordinal(), 903 mInputEnabled, mInStealthMode); 904 } 905 906 @Override 907 protected void onRestoreInstanceState(Parcelable state) { 908 final SavedState ss = (SavedState) state; 909 super.onRestoreInstanceState(ss.getSuperState()); 910 setPattern( 911 DisplayMode.Correct, 912 LockPatternUtils.stringToPattern(ss.getSerializedPattern())); 913 mPatternDisplayMode = DisplayMode.values()[ss.getDisplayMode()]; 914 mInputEnabled = ss.isInputEnabled(); 915 mInStealthMode = ss.isInStealthMode(); 916 } 917 918 /** 919 * The parecelable for saving and restoring a lock pattern view. 920 */ 921 private static class SavedState extends BaseSavedState { 922 923 private final String mSerializedPattern; 924 private final int mDisplayMode; 925 private final boolean mInputEnabled; 926 private final boolean mInStealthMode; 927 928 /** 929 * Constructor called from {@link LockPatternView#onSaveInstanceState()} 930 */ 931 private SavedState(Parcelable superState, String serializedPattern, int displayMode, 932 boolean inputEnabled, boolean inStealthMode) { 933 super(superState); 934 mSerializedPattern = serializedPattern; 935 mDisplayMode = displayMode; 936 mInputEnabled = inputEnabled; 937 mInStealthMode = inStealthMode; 938 } 939 940 /** 941 * Constructor called from {@link #CREATOR} 942 */ 943 private SavedState(Parcel in) { 944 super(in); 945 mSerializedPattern = in.readString(); 946 mDisplayMode = in.readInt(); 947 mInputEnabled = (Boolean) in.readValue(null); 948 mInStealthMode = (Boolean) in.readValue(null); 949 } 950 951 public String getSerializedPattern() { 952 return mSerializedPattern; 953 } 954 955 public int getDisplayMode() { 956 return mDisplayMode; 957 } 958 959 public boolean isInputEnabled() { 960 return mInputEnabled; 961 } 962 963 public boolean isInStealthMode() { 964 return mInStealthMode; 965 } 966 967 @Override 968 public void writeToParcel(Parcel dest, int flags) { 969 super.writeToParcel(dest, flags); 970 dest.writeString(mSerializedPattern); 971 dest.writeInt(mDisplayMode); 972 dest.writeValue(mInputEnabled); 973 dest.writeValue(mInStealthMode); 974 } 975 976 public static final Parcelable.Creator<SavedState> CREATOR = 977 new Creator<SavedState>() { 978 public SavedState createFromParcel(Parcel in) { 979 return new SavedState(in); 980 } 981 982 public SavedState[] newArray(int size) { 983 return new SavedState[size]; 984 } 985 }; 986 } 987} 988