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 android.content.Context; 21import android.content.res.TypedArray; 22import android.graphics.Bitmap; 23import android.graphics.BitmapFactory; 24import android.graphics.Canvas; 25import android.graphics.Color; 26import android.graphics.Matrix; 27import android.graphics.Paint; 28import android.graphics.Path; 29import android.graphics.Rect; 30import android.os.Debug; 31import android.os.Parcel; 32import android.os.Parcelable; 33import android.os.SystemClock; 34import android.util.AttributeSet; 35import android.view.HapticFeedbackConstants; 36import android.view.MotionEvent; 37import android.view.View; 38import android.view.accessibility.AccessibilityManager; 39 40import com.android.internal.R; 41 42import java.util.ArrayList; 43import java.util.List; 44 45/** 46 * Displays and detects the user's unlock attempt, which is a drag of a finger 47 * across 9 regions of the screen. 48 * 49 * Is also capable of displaying a static pattern in "in progress", "wrong" or 50 * "correct" states. 51 */ 52public class LockPatternView extends View { 53 private static final String TAG = "LockPatternView"; 54 // Aspect to use when rendering this view 55 private static final int ASPECT_SQUARE = 0; // View will be the minimum of width/height 56 private static final int ASPECT_LOCK_WIDTH = 1; // Fixed width; height will be minimum of (w,h) 57 private static final int ASPECT_LOCK_HEIGHT = 2; // Fixed height; width will be minimum of (w,h) 58 59 private static final boolean PROFILE_DRAWING = false; 60 private boolean mDrawingProfilingStarted = false; 61 62 private Paint mPaint = new Paint(); 63 private Paint mPathPaint = new Paint(); 64 65 // TODO: make this common with PhoneWindow 66 static final int STATUS_BAR_HEIGHT = 25; 67 68 /** 69 * How many milliseconds we spend animating each circle of a lock pattern 70 * if the animating mode is set. The entire animation should take this 71 * constant * the length of the pattern to complete. 72 */ 73 private static final int MILLIS_PER_CIRCLE_ANIMATING = 700; 74 75 private OnPatternListener mOnPatternListener; 76 private ArrayList<Cell> mPattern = new ArrayList<Cell>(9); 77 78 /** 79 * Lookup table for the circles of the pattern we are currently drawing. 80 * This will be the cells of the complete pattern unless we are animating, 81 * in which case we use this to hold the cells we are drawing for the in 82 * progress animation. 83 */ 84 private boolean[][] mPatternDrawLookup = new boolean[3][3]; 85 86 /** 87 * the in progress point: 88 * - during interaction: where the user's finger is 89 * - during animation: the current tip of the animating line 90 */ 91 private float mInProgressX = -1; 92 private float mInProgressY = -1; 93 94 private long mAnimatingPeriodStart; 95 96 private DisplayMode mPatternDisplayMode = DisplayMode.Correct; 97 private boolean mInputEnabled = true; 98 private boolean mInStealthMode = false; 99 private boolean mEnableHapticFeedback = true; 100 private boolean mPatternInProgress = false; 101 102 private float mDiameterFactor = 0.10f; // TODO: move to attrs 103 private final int mStrokeAlpha = 128; 104 private float mHitFactor = 0.6f; 105 106 private float mSquareWidth; 107 private float mSquareHeight; 108 109 private Bitmap mBitmapBtnDefault; 110 private Bitmap mBitmapBtnTouched; 111 private Bitmap mBitmapCircleDefault; 112 private Bitmap mBitmapCircleGreen; 113 private Bitmap mBitmapCircleRed; 114 115 private Bitmap mBitmapArrowGreenUp; 116 private Bitmap mBitmapArrowRedUp; 117 118 private final Path mCurrentPath = new Path(); 119 private final Rect mInvalidate = new Rect(); 120 121 private int mBitmapWidth; 122 private int mBitmapHeight; 123 124 private int mAspect; 125 private final Matrix mArrowMatrix = new Matrix(); 126 private final Matrix mCircleMatrix = new Matrix(); 127 128 129 /** 130 * Represents a cell in the 3 X 3 matrix of the unlock pattern view. 131 */ 132 public static class Cell { 133 int row; 134 int column; 135 136 // keep # objects limited to 9 137 static Cell[][] sCells = new Cell[3][3]; 138 static { 139 for (int i = 0; i < 3; i++) { 140 for (int j = 0; j < 3; j++) { 141 sCells[i][j] = new Cell(i, j); 142 } 143 } 144 } 145 146 /** 147 * @param row The row of the cell. 148 * @param column The column of the cell. 149 */ 150 private Cell(int row, int column) { 151 checkRange(row, column); 152 this.row = row; 153 this.column = column; 154 } 155 156 public int getRow() { 157 return row; 158 } 159 160 public int getColumn() { 161 return column; 162 } 163 164 /** 165 * @param row The row of the cell. 166 * @param column The column of the cell. 167 */ 168 public static synchronized Cell of(int row, int column) { 169 checkRange(row, column); 170 return sCells[row][column]; 171 } 172 173 private static void checkRange(int row, int column) { 174 if (row < 0 || row > 2) { 175 throw new IllegalArgumentException("row must be in range 0-2"); 176 } 177 if (column < 0 || column > 2) { 178 throw new IllegalArgumentException("column must be in range 0-2"); 179 } 180 } 181 182 public String toString() { 183 return "(row=" + row + ",clmn=" + column + ")"; 184 } 185 } 186 187 /** 188 * How to display the current pattern. 189 */ 190 public enum DisplayMode { 191 192 /** 193 * The pattern drawn is correct (i.e draw it in a friendly color) 194 */ 195 Correct, 196 197 /** 198 * Animate the pattern (for demo, and help). 199 */ 200 Animate, 201 202 /** 203 * The pattern is wrong (i.e draw a foreboding color) 204 */ 205 Wrong 206 } 207 208 /** 209 * The call back interface for detecting patterns entered by the user. 210 */ 211 public static interface OnPatternListener { 212 213 /** 214 * A new pattern has begun. 215 */ 216 void onPatternStart(); 217 218 /** 219 * The pattern was cleared. 220 */ 221 void onPatternCleared(); 222 223 /** 224 * The user extended the pattern currently being drawn by one cell. 225 * @param pattern The pattern with newly added cell. 226 */ 227 void onPatternCellAdded(List<Cell> pattern); 228 229 /** 230 * A pattern was detected from the user. 231 * @param pattern The pattern. 232 */ 233 void onPatternDetected(List<Cell> pattern); 234 } 235 236 public LockPatternView(Context context) { 237 this(context, null); 238 } 239 240 public LockPatternView(Context context, AttributeSet attrs) { 241 super(context, attrs); 242 243 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LockPatternView); 244 245 final String aspect = a.getString(R.styleable.LockPatternView_aspect); 246 247 if ("square".equals(aspect)) { 248 mAspect = ASPECT_SQUARE; 249 } else if ("lock_width".equals(aspect)) { 250 mAspect = ASPECT_LOCK_WIDTH; 251 } else if ("lock_height".equals(aspect)) { 252 mAspect = ASPECT_LOCK_HEIGHT; 253 } else { 254 mAspect = ASPECT_SQUARE; 255 } 256 257 setClickable(true); 258 259 mPathPaint.setAntiAlias(true); 260 mPathPaint.setDither(true); 261 mPathPaint.setColor(Color.WHITE); // TODO this should be from the style 262 mPathPaint.setAlpha(mStrokeAlpha); 263 mPathPaint.setStyle(Paint.Style.STROKE); 264 mPathPaint.setStrokeJoin(Paint.Join.ROUND); 265 mPathPaint.setStrokeCap(Paint.Cap.ROUND); 266 267 // lot's of bitmaps! 268 mBitmapBtnDefault = getBitmapFor(R.drawable.btn_code_lock_default_holo); 269 mBitmapBtnTouched = getBitmapFor(R.drawable.btn_code_lock_touched_holo); 270 mBitmapCircleDefault = getBitmapFor(R.drawable.indicator_code_lock_point_area_default_holo); 271 mBitmapCircleGreen = getBitmapFor(R.drawable.indicator_code_lock_point_area_green_holo); 272 mBitmapCircleRed = getBitmapFor(R.drawable.indicator_code_lock_point_area_red_holo); 273 274 mBitmapArrowGreenUp = getBitmapFor(R.drawable.indicator_code_lock_drag_direction_green_up); 275 mBitmapArrowRedUp = getBitmapFor(R.drawable.indicator_code_lock_drag_direction_red_up); 276 277 // bitmaps have the size of the largest bitmap in this group 278 final Bitmap bitmaps[] = { mBitmapBtnDefault, mBitmapBtnTouched, mBitmapCircleDefault, 279 mBitmapCircleGreen, mBitmapCircleRed }; 280 281 for (Bitmap bitmap : bitmaps) { 282 mBitmapWidth = Math.max(mBitmapWidth, bitmap.getWidth()); 283 mBitmapHeight = Math.max(mBitmapHeight, bitmap.getHeight()); 284 } 285 286 } 287 288 private Bitmap getBitmapFor(int resId) { 289 return BitmapFactory.decodeResource(getContext().getResources(), resId); 290 } 291 292 /** 293 * @return Whether the view is in stealth mode. 294 */ 295 public boolean isInStealthMode() { 296 return mInStealthMode; 297 } 298 299 /** 300 * @return Whether the view has tactile feedback enabled. 301 */ 302 public boolean isTactileFeedbackEnabled() { 303 return mEnableHapticFeedback; 304 } 305 306 /** 307 * Set whether the view is in stealth mode. If true, there will be no 308 * visible feedback as the user enters the pattern. 309 * 310 * @param inStealthMode Whether in stealth mode. 311 */ 312 public void setInStealthMode(boolean inStealthMode) { 313 mInStealthMode = inStealthMode; 314 } 315 316 /** 317 * Set whether the view will use tactile feedback. If true, there will be 318 * tactile feedback as the user enters the pattern. 319 * 320 * @param tactileFeedbackEnabled Whether tactile feedback is enabled 321 */ 322 public void setTactileFeedbackEnabled(boolean tactileFeedbackEnabled) { 323 mEnableHapticFeedback = tactileFeedbackEnabled; 324 } 325 326 /** 327 * Set the call back for pattern detection. 328 * @param onPatternListener The call back. 329 */ 330 public void setOnPatternListener( 331 OnPatternListener onPatternListener) { 332 mOnPatternListener = onPatternListener; 333 } 334 335 /** 336 * Set the pattern explicitely (rather than waiting for the user to input 337 * a pattern). 338 * @param displayMode How to display the pattern. 339 * @param pattern The pattern. 340 */ 341 public void setPattern(DisplayMode displayMode, List<Cell> pattern) { 342 mPattern.clear(); 343 mPattern.addAll(pattern); 344 clearPatternDrawLookup(); 345 for (Cell cell : pattern) { 346 mPatternDrawLookup[cell.getRow()][cell.getColumn()] = true; 347 } 348 349 setDisplayMode(displayMode); 350 } 351 352 /** 353 * Set the display mode of the current pattern. This can be useful, for 354 * instance, after detecting a pattern to tell this view whether change the 355 * in progress result to correct or wrong. 356 * @param displayMode The display mode. 357 */ 358 public void setDisplayMode(DisplayMode displayMode) { 359 mPatternDisplayMode = displayMode; 360 if (displayMode == DisplayMode.Animate) { 361 if (mPattern.size() == 0) { 362 throw new IllegalStateException("you must have a pattern to " 363 + "animate if you want to set the display mode to animate"); 364 } 365 mAnimatingPeriodStart = SystemClock.elapsedRealtime(); 366 final Cell first = mPattern.get(0); 367 mInProgressX = getCenterXForColumn(first.getColumn()); 368 mInProgressY = getCenterYForRow(first.getRow()); 369 clearPatternDrawLookup(); 370 } 371 invalidate(); 372 } 373 374 private void notifyCellAdded() { 375 sendAccessEvent(R.string.lockscreen_access_pattern_cell_added); 376 if (mOnPatternListener != null) { 377 mOnPatternListener.onPatternCellAdded(mPattern); 378 } 379 } 380 381 private void notifyPatternStarted() { 382 sendAccessEvent(R.string.lockscreen_access_pattern_start); 383 if (mOnPatternListener != null) { 384 mOnPatternListener.onPatternStart(); 385 } 386 } 387 388 private void notifyPatternDetected() { 389 sendAccessEvent(R.string.lockscreen_access_pattern_detected); 390 if (mOnPatternListener != null) { 391 mOnPatternListener.onPatternDetected(mPattern); 392 } 393 } 394 395 private void notifyPatternCleared() { 396 sendAccessEvent(R.string.lockscreen_access_pattern_cleared); 397 if (mOnPatternListener != null) { 398 mOnPatternListener.onPatternCleared(); 399 } 400 } 401 402 /** 403 * Clear the pattern. 404 */ 405 public void clearPattern() { 406 resetPattern(); 407 } 408 409 /** 410 * Reset all pattern state. 411 */ 412 private void resetPattern() { 413 mPattern.clear(); 414 clearPatternDrawLookup(); 415 mPatternDisplayMode = DisplayMode.Correct; 416 invalidate(); 417 } 418 419 /** 420 * Clear the pattern lookup table. 421 */ 422 private void clearPatternDrawLookup() { 423 for (int i = 0; i < 3; i++) { 424 for (int j = 0; j < 3; j++) { 425 mPatternDrawLookup[i][j] = false; 426 } 427 } 428 } 429 430 /** 431 * Disable input (for instance when displaying a message that will 432 * timeout so user doesn't get view into messy state). 433 */ 434 public void disableInput() { 435 mInputEnabled = false; 436 } 437 438 /** 439 * Enable input. 440 */ 441 public void enableInput() { 442 mInputEnabled = true; 443 } 444 445 @Override 446 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 447 final int width = w - mPaddingLeft - mPaddingRight; 448 mSquareWidth = width / 3.0f; 449 450 final int height = h - mPaddingTop - mPaddingBottom; 451 mSquareHeight = height / 3.0f; 452 } 453 454 private int resolveMeasured(int measureSpec, int desired) 455 { 456 int result = 0; 457 int specSize = MeasureSpec.getSize(measureSpec); 458 switch (MeasureSpec.getMode(measureSpec)) { 459 case MeasureSpec.UNSPECIFIED: 460 result = desired; 461 break; 462 case MeasureSpec.AT_MOST: 463 result = Math.max(specSize, desired); 464 break; 465 case MeasureSpec.EXACTLY: 466 default: 467 result = specSize; 468 } 469 return result; 470 } 471 472 @Override 473 protected int getSuggestedMinimumWidth() { 474 // View should be large enough to contain 3 side-by-side target bitmaps 475 return 3 * mBitmapWidth; 476 } 477 478 @Override 479 protected int getSuggestedMinimumHeight() { 480 // View should be large enough to contain 3 side-by-side target bitmaps 481 return 3 * mBitmapWidth; 482 } 483 484 @Override 485 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 486 final int minimumWidth = getSuggestedMinimumWidth(); 487 final int minimumHeight = getSuggestedMinimumHeight(); 488 int viewWidth = resolveMeasured(widthMeasureSpec, minimumWidth); 489 int viewHeight = resolveMeasured(heightMeasureSpec, minimumHeight); 490 491 switch (mAspect) { 492 case ASPECT_SQUARE: 493 viewWidth = viewHeight = Math.min(viewWidth, viewHeight); 494 break; 495 case ASPECT_LOCK_WIDTH: 496 viewHeight = Math.min(viewWidth, viewHeight); 497 break; 498 case ASPECT_LOCK_HEIGHT: 499 viewWidth = Math.min(viewWidth, viewHeight); 500 break; 501 } 502 // Log.v(TAG, "LockPatternView dimensions: " + viewWidth + "x" + viewHeight); 503 setMeasuredDimension(viewWidth, viewHeight); 504 } 505 506 /** 507 * Determines whether the point x, y will add a new point to the current 508 * pattern (in addition to finding the cell, also makes heuristic choices 509 * such as filling in gaps based on current pattern). 510 * @param x The x coordinate. 511 * @param y The y coordinate. 512 */ 513 private Cell detectAndAddHit(float x, float y) { 514 final Cell cell = checkForNewHit(x, y); 515 if (cell != null) { 516 517 // check for gaps in existing pattern 518 Cell fillInGapCell = null; 519 final ArrayList<Cell> pattern = mPattern; 520 if (!pattern.isEmpty()) { 521 final Cell lastCell = pattern.get(pattern.size() - 1); 522 int dRow = cell.row - lastCell.row; 523 int dColumn = cell.column - lastCell.column; 524 525 int fillInRow = lastCell.row; 526 int fillInColumn = lastCell.column; 527 528 if (Math.abs(dRow) == 2 && Math.abs(dColumn) != 1) { 529 fillInRow = lastCell.row + ((dRow > 0) ? 1 : -1); 530 } 531 532 if (Math.abs(dColumn) == 2 && Math.abs(dRow) != 1) { 533 fillInColumn = lastCell.column + ((dColumn > 0) ? 1 : -1); 534 } 535 536 fillInGapCell = Cell.of(fillInRow, fillInColumn); 537 } 538 539 if (fillInGapCell != null && 540 !mPatternDrawLookup[fillInGapCell.row][fillInGapCell.column]) { 541 addCellToPattern(fillInGapCell); 542 } 543 addCellToPattern(cell); 544 if (mEnableHapticFeedback) { 545 performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY, 546 HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING 547 | HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING); 548 } 549 return cell; 550 } 551 return null; 552 } 553 554 private void addCellToPattern(Cell newCell) { 555 mPatternDrawLookup[newCell.getRow()][newCell.getColumn()] = true; 556 mPattern.add(newCell); 557 notifyCellAdded(); 558 } 559 560 // helper method to find which cell a point maps to 561 private Cell checkForNewHit(float x, float y) { 562 563 final int rowHit = getRowHit(y); 564 if (rowHit < 0) { 565 return null; 566 } 567 final int columnHit = getColumnHit(x); 568 if (columnHit < 0) { 569 return null; 570 } 571 572 if (mPatternDrawLookup[rowHit][columnHit]) { 573 return null; 574 } 575 return Cell.of(rowHit, columnHit); 576 } 577 578 /** 579 * Helper method to find the row that y falls into. 580 * @param y The y coordinate 581 * @return The row that y falls in, or -1 if it falls in no row. 582 */ 583 private int getRowHit(float y) { 584 585 final float squareHeight = mSquareHeight; 586 float hitSize = squareHeight * mHitFactor; 587 588 float offset = mPaddingTop + (squareHeight - hitSize) / 2f; 589 for (int i = 0; i < 3; i++) { 590 591 final float hitTop = offset + squareHeight * i; 592 if (y >= hitTop && y <= hitTop + hitSize) { 593 return i; 594 } 595 } 596 return -1; 597 } 598 599 /** 600 * Helper method to find the column x fallis into. 601 * @param x The x coordinate. 602 * @return The column that x falls in, or -1 if it falls in no column. 603 */ 604 private int getColumnHit(float x) { 605 final float squareWidth = mSquareWidth; 606 float hitSize = squareWidth * mHitFactor; 607 608 float offset = mPaddingLeft + (squareWidth - hitSize) / 2f; 609 for (int i = 0; i < 3; i++) { 610 611 final float hitLeft = offset + squareWidth * i; 612 if (x >= hitLeft && x <= hitLeft + hitSize) { 613 return i; 614 } 615 } 616 return -1; 617 } 618 619 @Override 620 public boolean onHoverEvent(MotionEvent event) { 621 if (AccessibilityManager.getInstance(mContext).isTouchExplorationEnabled()) { 622 final int action = event.getAction(); 623 switch (action) { 624 case MotionEvent.ACTION_HOVER_ENTER: 625 event.setAction(MotionEvent.ACTION_DOWN); 626 break; 627 case MotionEvent.ACTION_HOVER_MOVE: 628 event.setAction(MotionEvent.ACTION_MOVE); 629 break; 630 case MotionEvent.ACTION_HOVER_EXIT: 631 event.setAction(MotionEvent.ACTION_UP); 632 break; 633 } 634 onTouchEvent(event); 635 event.setAction(action); 636 } 637 return super.onHoverEvent(event); 638 } 639 640 @Override 641 public boolean onTouchEvent(MotionEvent event) { 642 if (!mInputEnabled || !isEnabled()) { 643 return false; 644 } 645 646 switch(event.getAction()) { 647 case MotionEvent.ACTION_DOWN: 648 handleActionDown(event); 649 return true; 650 case MotionEvent.ACTION_UP: 651 handleActionUp(event); 652 return true; 653 case MotionEvent.ACTION_MOVE: 654 handleActionMove(event); 655 return true; 656 case MotionEvent.ACTION_CANCEL: 657 if (mPatternInProgress) { 658 mPatternInProgress = false; 659 resetPattern(); 660 notifyPatternCleared(); 661 } 662 if (PROFILE_DRAWING) { 663 if (mDrawingProfilingStarted) { 664 Debug.stopMethodTracing(); 665 mDrawingProfilingStarted = false; 666 } 667 } 668 return true; 669 } 670 return false; 671 } 672 673 private void handleActionMove(MotionEvent event) { 674 // Handle all recent motion events so we don't skip any cells even when the device 675 // is busy... 676 final int historySize = event.getHistorySize(); 677 for (int i = 0; i < historySize + 1; i++) { 678 final float x = i < historySize ? event.getHistoricalX(i) : event.getX(); 679 final float y = i < historySize ? event.getHistoricalY(i) : event.getY(); 680 final int patternSizePreHitDetect = mPattern.size(); 681 Cell hitCell = detectAndAddHit(x, y); 682 final int patternSize = mPattern.size(); 683 if (hitCell != null && patternSize == 1) { 684 mPatternInProgress = true; 685 notifyPatternStarted(); 686 } 687 // note current x and y for rubber banding of in progress patterns 688 final float dx = Math.abs(x - mInProgressX); 689 final float dy = Math.abs(y - mInProgressY); 690 if (dx + dy > mSquareWidth * 0.01f) { 691 float oldX = mInProgressX; 692 float oldY = mInProgressY; 693 694 mInProgressX = x; 695 mInProgressY = y; 696 697 if (mPatternInProgress && patternSize > 0) { 698 final ArrayList<Cell> pattern = mPattern; 699 final float radius = mSquareWidth * mDiameterFactor * 0.5f; 700 701 final Cell lastCell = pattern.get(patternSize - 1); 702 703 float startX = getCenterXForColumn(lastCell.column); 704 float startY = getCenterYForRow(lastCell.row); 705 706 float left; 707 float top; 708 float right; 709 float bottom; 710 711 final Rect invalidateRect = mInvalidate; 712 713 if (startX < x) { 714 left = startX; 715 right = x; 716 } else { 717 left = x; 718 right = startX; 719 } 720 721 if (startY < y) { 722 top = startY; 723 bottom = y; 724 } else { 725 top = y; 726 bottom = startY; 727 } 728 729 // Invalidate between the pattern's last cell and the current location 730 invalidateRect.set((int) (left - radius), (int) (top - radius), 731 (int) (right + radius), (int) (bottom + radius)); 732 733 if (startX < oldX) { 734 left = startX; 735 right = oldX; 736 } else { 737 left = oldX; 738 right = startX; 739 } 740 741 if (startY < oldY) { 742 top = startY; 743 bottom = oldY; 744 } else { 745 top = oldY; 746 bottom = startY; 747 } 748 749 // Invalidate between the pattern's last cell and the previous location 750 invalidateRect.union((int) (left - radius), (int) (top - radius), 751 (int) (right + radius), (int) (bottom + radius)); 752 753 // Invalidate between the pattern's new cell and the pattern's previous cell 754 if (hitCell != null) { 755 startX = getCenterXForColumn(hitCell.column); 756 startY = getCenterYForRow(hitCell.row); 757 758 if (patternSize >= 2) { 759 // (re-using hitcell for old cell) 760 hitCell = pattern.get(patternSize - 1 - (patternSize - patternSizePreHitDetect)); 761 oldX = getCenterXForColumn(hitCell.column); 762 oldY = getCenterYForRow(hitCell.row); 763 764 if (startX < oldX) { 765 left = startX; 766 right = oldX; 767 } else { 768 left = oldX; 769 right = startX; 770 } 771 772 if (startY < oldY) { 773 top = startY; 774 bottom = oldY; 775 } else { 776 top = oldY; 777 bottom = startY; 778 } 779 } else { 780 left = right = startX; 781 top = bottom = startY; 782 } 783 784 final float widthOffset = mSquareWidth / 2f; 785 final float heightOffset = mSquareHeight / 2f; 786 787 invalidateRect.set((int) (left - widthOffset), 788 (int) (top - heightOffset), (int) (right + widthOffset), 789 (int) (bottom + heightOffset)); 790 } 791 792 invalidate(invalidateRect); 793 } else { 794 invalidate(); 795 } 796 } 797 } 798 } 799 800 private void sendAccessEvent(int resId) { 801 announceForAccessibility(mContext.getString(resId)); 802 } 803 804 private void handleActionUp(MotionEvent event) { 805 // report pattern detected 806 if (!mPattern.isEmpty()) { 807 mPatternInProgress = false; 808 notifyPatternDetected(); 809 invalidate(); 810 } 811 if (PROFILE_DRAWING) { 812 if (mDrawingProfilingStarted) { 813 Debug.stopMethodTracing(); 814 mDrawingProfilingStarted = false; 815 } 816 } 817 } 818 819 private void handleActionDown(MotionEvent event) { 820 resetPattern(); 821 final float x = event.getX(); 822 final float y = event.getY(); 823 final Cell hitCell = detectAndAddHit(x, y); 824 if (hitCell != null) { 825 mPatternInProgress = true; 826 mPatternDisplayMode = DisplayMode.Correct; 827 notifyPatternStarted(); 828 } else if (mPatternInProgress) { 829 mPatternInProgress = false; 830 notifyPatternCleared(); 831 } 832 if (hitCell != null) { 833 final float startX = getCenterXForColumn(hitCell.column); 834 final float startY = getCenterYForRow(hitCell.row); 835 836 final float widthOffset = mSquareWidth / 2f; 837 final float heightOffset = mSquareHeight / 2f; 838 839 invalidate((int) (startX - widthOffset), (int) (startY - heightOffset), 840 (int) (startX + widthOffset), (int) (startY + heightOffset)); 841 } 842 mInProgressX = x; 843 mInProgressY = y; 844 if (PROFILE_DRAWING) { 845 if (!mDrawingProfilingStarted) { 846 Debug.startMethodTracing("LockPatternDrawing"); 847 mDrawingProfilingStarted = true; 848 } 849 } 850 } 851 852 private float getCenterXForColumn(int column) { 853 return mPaddingLeft + column * mSquareWidth + mSquareWidth / 2f; 854 } 855 856 private float getCenterYForRow(int row) { 857 return mPaddingTop + row * mSquareHeight + mSquareHeight / 2f; 858 } 859 860 @Override 861 protected void onDraw(Canvas canvas) { 862 final ArrayList<Cell> pattern = mPattern; 863 final int count = pattern.size(); 864 final boolean[][] drawLookup = mPatternDrawLookup; 865 866 if (mPatternDisplayMode == DisplayMode.Animate) { 867 868 // figure out which circles to draw 869 870 // + 1 so we pause on complete pattern 871 final int oneCycle = (count + 1) * MILLIS_PER_CIRCLE_ANIMATING; 872 final int spotInCycle = (int) (SystemClock.elapsedRealtime() - 873 mAnimatingPeriodStart) % oneCycle; 874 final int numCircles = spotInCycle / MILLIS_PER_CIRCLE_ANIMATING; 875 876 clearPatternDrawLookup(); 877 for (int i = 0; i < numCircles; i++) { 878 final Cell cell = pattern.get(i); 879 drawLookup[cell.getRow()][cell.getColumn()] = true; 880 } 881 882 // figure out in progress portion of ghosting line 883 884 final boolean needToUpdateInProgressPoint = numCircles > 0 885 && numCircles < count; 886 887 if (needToUpdateInProgressPoint) { 888 final float percentageOfNextCircle = 889 ((float) (spotInCycle % MILLIS_PER_CIRCLE_ANIMATING)) / 890 MILLIS_PER_CIRCLE_ANIMATING; 891 892 final Cell currentCell = pattern.get(numCircles - 1); 893 final float centerX = getCenterXForColumn(currentCell.column); 894 final float centerY = getCenterYForRow(currentCell.row); 895 896 final Cell nextCell = pattern.get(numCircles); 897 final float dx = percentageOfNextCircle * 898 (getCenterXForColumn(nextCell.column) - centerX); 899 final float dy = percentageOfNextCircle * 900 (getCenterYForRow(nextCell.row) - centerY); 901 mInProgressX = centerX + dx; 902 mInProgressY = centerY + dy; 903 } 904 // TODO: Infinite loop here... 905 invalidate(); 906 } 907 908 final float squareWidth = mSquareWidth; 909 final float squareHeight = mSquareHeight; 910 911 float radius = (squareWidth * mDiameterFactor * 0.5f); 912 mPathPaint.setStrokeWidth(radius); 913 914 final Path currentPath = mCurrentPath; 915 currentPath.rewind(); 916 917 // draw the circles 918 final int paddingTop = mPaddingTop; 919 final int paddingLeft = mPaddingLeft; 920 921 for (int i = 0; i < 3; i++) { 922 float topY = paddingTop + i * squareHeight; 923 //float centerY = mPaddingTop + i * mSquareHeight + (mSquareHeight / 2); 924 for (int j = 0; j < 3; j++) { 925 float leftX = paddingLeft + j * squareWidth; 926 drawCircle(canvas, (int) leftX, (int) topY, drawLookup[i][j]); 927 } 928 } 929 930 // TODO: the path should be created and cached every time we hit-detect a cell 931 // only the last segment of the path should be computed here 932 // draw the path of the pattern (unless the user is in progress, and 933 // we are in stealth mode) 934 final boolean drawPath = (!mInStealthMode || mPatternDisplayMode == DisplayMode.Wrong); 935 936 // draw the arrows associated with the path (unless the user is in progress, and 937 // we are in stealth mode) 938 boolean oldFlag = (mPaint.getFlags() & Paint.FILTER_BITMAP_FLAG) != 0; 939 mPaint.setFilterBitmap(true); // draw with higher quality since we render with transforms 940 if (drawPath) { 941 for (int i = 0; i < count - 1; i++) { 942 Cell cell = pattern.get(i); 943 Cell next = pattern.get(i + 1); 944 945 // only draw the part of the pattern stored in 946 // the lookup table (this is only different in the case 947 // of animation). 948 if (!drawLookup[next.row][next.column]) { 949 break; 950 } 951 952 float leftX = paddingLeft + cell.column * squareWidth; 953 float topY = paddingTop + cell.row * squareHeight; 954 955 drawArrow(canvas, leftX, topY, cell, next); 956 } 957 } 958 959 if (drawPath) { 960 boolean anyCircles = false; 961 for (int i = 0; i < count; i++) { 962 Cell cell = pattern.get(i); 963 964 // only draw the part of the pattern stored in 965 // the lookup table (this is only different in the case 966 // of animation). 967 if (!drawLookup[cell.row][cell.column]) { 968 break; 969 } 970 anyCircles = true; 971 972 float centerX = getCenterXForColumn(cell.column); 973 float centerY = getCenterYForRow(cell.row); 974 if (i == 0) { 975 currentPath.moveTo(centerX, centerY); 976 } else { 977 currentPath.lineTo(centerX, centerY); 978 } 979 } 980 981 // add last in progress section 982 if ((mPatternInProgress || mPatternDisplayMode == DisplayMode.Animate) 983 && anyCircles) { 984 currentPath.lineTo(mInProgressX, mInProgressY); 985 } 986 canvas.drawPath(currentPath, mPathPaint); 987 } 988 989 mPaint.setFilterBitmap(oldFlag); // restore default flag 990 } 991 992 private void drawArrow(Canvas canvas, float leftX, float topY, Cell start, Cell end) { 993 boolean green = mPatternDisplayMode != DisplayMode.Wrong; 994 995 final int endRow = end.row; 996 final int startRow = start.row; 997 final int endColumn = end.column; 998 final int startColumn = start.column; 999 1000 // offsets for centering the bitmap in the cell 1001 final int offsetX = ((int) mSquareWidth - mBitmapWidth) / 2; 1002 final int offsetY = ((int) mSquareHeight - mBitmapHeight) / 2; 1003 1004 // compute transform to place arrow bitmaps at correct angle inside circle. 1005 // This assumes that the arrow image is drawn at 12:00 with it's top edge 1006 // coincident with the circle bitmap's top edge. 1007 Bitmap arrow = green ? mBitmapArrowGreenUp : mBitmapArrowRedUp; 1008 final int cellWidth = mBitmapWidth; 1009 final int cellHeight = mBitmapHeight; 1010 1011 // the up arrow bitmap is at 12:00, so find the rotation from x axis and add 90 degrees. 1012 final float theta = (float) Math.atan2( 1013 (double) (endRow - startRow), (double) (endColumn - startColumn)); 1014 final float angle = (float) Math.toDegrees(theta) + 90.0f; 1015 1016 // compose matrix 1017 float sx = Math.min(mSquareWidth / mBitmapWidth, 1.0f); 1018 float sy = Math.min(mSquareHeight / mBitmapHeight, 1.0f); 1019 mArrowMatrix.setTranslate(leftX + offsetX, topY + offsetY); // transform to cell position 1020 mArrowMatrix.preTranslate(mBitmapWidth/2, mBitmapHeight/2); 1021 mArrowMatrix.preScale(sx, sy); 1022 mArrowMatrix.preTranslate(-mBitmapWidth/2, -mBitmapHeight/2); 1023 mArrowMatrix.preRotate(angle, cellWidth / 2.0f, cellHeight / 2.0f); // rotate about cell center 1024 mArrowMatrix.preTranslate((cellWidth - arrow.getWidth()) / 2.0f, 0.0f); // translate to 12:00 pos 1025 canvas.drawBitmap(arrow, mArrowMatrix, mPaint); 1026 } 1027 1028 /** 1029 * @param canvas 1030 * @param leftX 1031 * @param topY 1032 * @param partOfPattern Whether this circle is part of the pattern. 1033 */ 1034 private void drawCircle(Canvas canvas, int leftX, int topY, boolean partOfPattern) { 1035 Bitmap outerCircle; 1036 Bitmap innerCircle; 1037 1038 if (!partOfPattern || (mInStealthMode && mPatternDisplayMode != DisplayMode.Wrong)) { 1039 // unselected circle 1040 outerCircle = mBitmapCircleDefault; 1041 innerCircle = mBitmapBtnDefault; 1042 } else if (mPatternInProgress) { 1043 // user is in middle of drawing a pattern 1044 outerCircle = mBitmapCircleGreen; 1045 innerCircle = mBitmapBtnTouched; 1046 } else if (mPatternDisplayMode == DisplayMode.Wrong) { 1047 // the pattern is wrong 1048 outerCircle = mBitmapCircleRed; 1049 innerCircle = mBitmapBtnDefault; 1050 } else if (mPatternDisplayMode == DisplayMode.Correct || 1051 mPatternDisplayMode == DisplayMode.Animate) { 1052 // the pattern is correct 1053 outerCircle = mBitmapCircleGreen; 1054 innerCircle = mBitmapBtnDefault; 1055 } else { 1056 throw new IllegalStateException("unknown display mode " + mPatternDisplayMode); 1057 } 1058 1059 final int width = mBitmapWidth; 1060 final int height = mBitmapHeight; 1061 1062 final float squareWidth = mSquareWidth; 1063 final float squareHeight = mSquareHeight; 1064 1065 int offsetX = (int) ((squareWidth - width) / 2f); 1066 int offsetY = (int) ((squareHeight - height) / 2f); 1067 1068 // Allow circles to shrink if the view is too small to hold them. 1069 float sx = Math.min(mSquareWidth / mBitmapWidth, 1.0f); 1070 float sy = Math.min(mSquareHeight / mBitmapHeight, 1.0f); 1071 1072 mCircleMatrix.setTranslate(leftX + offsetX, topY + offsetY); 1073 mCircleMatrix.preTranslate(mBitmapWidth/2, mBitmapHeight/2); 1074 mCircleMatrix.preScale(sx, sy); 1075 mCircleMatrix.preTranslate(-mBitmapWidth/2, -mBitmapHeight/2); 1076 1077 canvas.drawBitmap(outerCircle, mCircleMatrix, mPaint); 1078 canvas.drawBitmap(innerCircle, mCircleMatrix, mPaint); 1079 } 1080 1081 @Override 1082 protected Parcelable onSaveInstanceState() { 1083 Parcelable superState = super.onSaveInstanceState(); 1084 return new SavedState(superState, 1085 LockPatternUtils.patternToString(mPattern), 1086 mPatternDisplayMode.ordinal(), 1087 mInputEnabled, mInStealthMode, mEnableHapticFeedback); 1088 } 1089 1090 @Override 1091 protected void onRestoreInstanceState(Parcelable state) { 1092 final SavedState ss = (SavedState) state; 1093 super.onRestoreInstanceState(ss.getSuperState()); 1094 setPattern( 1095 DisplayMode.Correct, 1096 LockPatternUtils.stringToPattern(ss.getSerializedPattern())); 1097 mPatternDisplayMode = DisplayMode.values()[ss.getDisplayMode()]; 1098 mInputEnabled = ss.isInputEnabled(); 1099 mInStealthMode = ss.isInStealthMode(); 1100 mEnableHapticFeedback = ss.isTactileFeedbackEnabled(); 1101 } 1102 1103 /** 1104 * The parecelable for saving and restoring a lock pattern view. 1105 */ 1106 private static class SavedState extends BaseSavedState { 1107 1108 private final String mSerializedPattern; 1109 private final int mDisplayMode; 1110 private final boolean mInputEnabled; 1111 private final boolean mInStealthMode; 1112 private final boolean mTactileFeedbackEnabled; 1113 1114 /** 1115 * Constructor called from {@link LockPatternView#onSaveInstanceState()} 1116 */ 1117 private SavedState(Parcelable superState, String serializedPattern, int displayMode, 1118 boolean inputEnabled, boolean inStealthMode, boolean tactileFeedbackEnabled) { 1119 super(superState); 1120 mSerializedPattern = serializedPattern; 1121 mDisplayMode = displayMode; 1122 mInputEnabled = inputEnabled; 1123 mInStealthMode = inStealthMode; 1124 mTactileFeedbackEnabled = tactileFeedbackEnabled; 1125 } 1126 1127 /** 1128 * Constructor called from {@link #CREATOR} 1129 */ 1130 private SavedState(Parcel in) { 1131 super(in); 1132 mSerializedPattern = in.readString(); 1133 mDisplayMode = in.readInt(); 1134 mInputEnabled = (Boolean) in.readValue(null); 1135 mInStealthMode = (Boolean) in.readValue(null); 1136 mTactileFeedbackEnabled = (Boolean) in.readValue(null); 1137 } 1138 1139 public String getSerializedPattern() { 1140 return mSerializedPattern; 1141 } 1142 1143 public int getDisplayMode() { 1144 return mDisplayMode; 1145 } 1146 1147 public boolean isInputEnabled() { 1148 return mInputEnabled; 1149 } 1150 1151 public boolean isInStealthMode() { 1152 return mInStealthMode; 1153 } 1154 1155 public boolean isTactileFeedbackEnabled(){ 1156 return mTactileFeedbackEnabled; 1157 } 1158 1159 @Override 1160 public void writeToParcel(Parcel dest, int flags) { 1161 super.writeToParcel(dest, flags); 1162 dest.writeString(mSerializedPattern); 1163 dest.writeInt(mDisplayMode); 1164 dest.writeValue(mInputEnabled); 1165 dest.writeValue(mInStealthMode); 1166 dest.writeValue(mTactileFeedbackEnabled); 1167 } 1168 1169 public static final Parcelable.Creator<SavedState> CREATOR = 1170 new Creator<SavedState>() { 1171 public SavedState createFromParcel(Parcel in) { 1172 return new SavedState(in); 1173 } 1174 1175 public SavedState[] newArray(int size) { 1176 return new SavedState[size]; 1177 } 1178 }; 1179 } 1180} 1181