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