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 */ Cell(int row, int column)149 private Cell(int row, int column) { 150 checkRange(row, column); 151 this.row = row; 152 this.column = column; 153 } 154 getRow()155 public int getRow() { 156 return row; 157 } 158 getColumn()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 */ of(int row, int column)167 public static synchronized Cell of(int row, int column) { 168 checkRange(row, column); 169 return sCells[row][column]; 170 } 171 checkRange(int row, int column)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 toString()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 */ onPatternStart()225 void onPatternStart(); 226 227 /** 228 * The pattern was cleared. 229 */ onPatternCleared()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 */ onPatternCellAdded(List<Cell> pattern)236 void onPatternCellAdded(List<Cell> pattern); 237 238 /** 239 * A pattern was detected from the user. 240 * @param pattern The pattern. 241 */ onPatternDetected(List<Cell> pattern)242 void onPatternDetected(List<Cell> pattern); 243 } 244 LockPatternView(Context context)245 public LockPatternView(Context context) { 246 this(context, null); 247 } 248 LockPatternView(Context context, AttributeSet attrs)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 getCellStates()310 public CellState[][] getCellStates() { 311 return mCellStates; 312 } 313 314 /** 315 * @return Whether the view is in stealth mode. 316 */ isInStealthMode()317 public boolean isInStealthMode() { 318 return mInStealthMode; 319 } 320 321 /** 322 * @return Whether the view has tactile feedback enabled. 323 */ isTactileFeedbackEnabled()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 */ setInStealthMode(boolean inStealthMode)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 */ setTactileFeedbackEnabled(boolean tactileFeedbackEnabled)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 */ setOnPatternListener( OnPatternListener onPatternListener)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 */ setPattern(DisplayMode displayMode, List<Cell> pattern)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 */ setDisplayMode(DisplayMode displayMode)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 notifyCellAdded()396 private void notifyCellAdded() { 397 sendAccessEvent(R.string.lockscreen_access_pattern_cell_added); 398 if (mOnPatternListener != null) { 399 mOnPatternListener.onPatternCellAdded(mPattern); 400 } 401 } 402 notifyPatternStarted()403 private void notifyPatternStarted() { 404 sendAccessEvent(R.string.lockscreen_access_pattern_start); 405 if (mOnPatternListener != null) { 406 mOnPatternListener.onPatternStart(); 407 } 408 } 409 notifyPatternDetected()410 private void notifyPatternDetected() { 411 sendAccessEvent(R.string.lockscreen_access_pattern_detected); 412 if (mOnPatternListener != null) { 413 mOnPatternListener.onPatternDetected(mPattern); 414 } 415 } 416 notifyPatternCleared()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 */ clearPattern()427 public void clearPattern() { 428 resetPattern(); 429 } 430 431 /** 432 * Reset all pattern state. 433 */ resetPattern()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 */ clearPatternDrawLookup()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 */ disableInput()456 public void disableInput() { 457 mInputEnabled = false; 458 } 459 460 /** 461 * Enable input. 462 */ enableInput()463 public void enableInput() { 464 mInputEnabled = true; 465 } 466 467 @Override onSizeChanged(int w, int h, int oldw, int oldh)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 resolveMeasured(int measureSpec, int desired)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 onMeasure(int widthMeasureSpec, int heightMeasureSpec)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 */ detectAndAddHit(float x, float y)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 addCellToPattern(Cell newCell)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 startCellActivatedAnimation(Cell cell)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 startLineEndAnimation(final CellState state, final float startX, final float startY, final float targetX, final float targetY)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 startSizeAnimation(float start, float end, long duration, Interpolator interpolator, final CellState state, final Runnable endRunnable)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 checkForNewHit(float x, float y)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 */ getRowHit(float y)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 */ getColumnHit(float x)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 onHoverEvent(MotionEvent event)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 onTouchEvent(MotionEvent event)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 handleActionMove(MotionEvent event)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 sendAccessEvent(int resId)811 private void sendAccessEvent(int resId) { 812 announceForAccessibility(mContext.getString(resId)); 813 } 814 handleActionUp(MotionEvent event)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 cancelLineAnimations()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 } handleActionDown(MotionEvent event)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 getCenterXForColumn(int column)876 private float getCenterXForColumn(int column) { 877 return mPaddingLeft + column * mSquareWidth + mSquareWidth / 2f; 878 } 879 getCenterYForRow(int row)880 private float getCenterYForRow(int row) { 881 return mPaddingTop + row * mSquareHeight + mSquareHeight / 2f; 882 } 883 884 @Override onDraw(Canvas canvas)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