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