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 import android.animation.Animator; 20 import android.animation.AnimatorListenerAdapter; 21 import android.animation.AnimatorSet; 22 import android.animation.ValueAnimator; 23 import android.annotation.Nullable; 24 import android.compat.annotation.UnsupportedAppUsage; 25 import android.content.Context; 26 import android.content.res.Resources; 27 import android.content.res.TypedArray; 28 import android.graphics.Canvas; 29 import android.graphics.CanvasProperty; 30 import android.graphics.Color; 31 import android.graphics.LinearGradient; 32 import android.graphics.Paint; 33 import android.graphics.Path; 34 import android.graphics.RecordingCanvas; 35 import android.graphics.Rect; 36 import android.graphics.Shader; 37 import android.graphics.drawable.Drawable; 38 import android.os.Bundle; 39 import android.os.Debug; 40 import android.os.Parcel; 41 import android.os.Parcelable; 42 import android.os.SystemClock; 43 import android.util.AttributeSet; 44 import android.util.IntArray; 45 import android.util.Log; 46 import android.util.SparseArray; 47 import android.util.TypedValue; 48 import android.view.HapticFeedbackConstants; 49 import android.view.MotionEvent; 50 import android.view.RenderNodeAnimator; 51 import android.view.View; 52 import android.view.accessibility.AccessibilityEvent; 53 import android.view.accessibility.AccessibilityManager; 54 import android.view.accessibility.AccessibilityNodeInfo; 55 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; 56 import android.view.animation.AnimationUtils; 57 import android.view.animation.Interpolator; 58 59 import com.android.internal.R; 60 import com.android.internal.graphics.ColorUtils; 61 62 import java.util.ArrayList; 63 import java.util.List; 64 65 /** 66 * Displays and detects the user's unlock attempt, which is a drag of a finger 67 * across 9 regions of the screen. 68 * 69 * Is also capable of displaying a static pattern in "in progress", "wrong" or 70 * "correct" states. 71 */ 72 public class LockPatternView extends View { 73 // Aspect to use when rendering this view 74 private static final int ASPECT_SQUARE = 0; // View will be the minimum of width/height 75 private static final int ASPECT_LOCK_WIDTH = 1; // Fixed width; height will be minimum of (w,h) 76 private static final int ASPECT_LOCK_HEIGHT = 2; // Fixed height; width will be minimum of (w,h) 77 78 private static final boolean PROFILE_DRAWING = false; 79 private static final int LINE_END_ANIMATION_DURATION_MILLIS = 50; 80 private static final int DOT_ACTIVATION_DURATION_MILLIS = 50; 81 private static final int DOT_RADIUS_INCREASE_DURATION_MILLIS = 96; 82 private static final int DOT_RADIUS_DECREASE_DURATION_MILLIS = 192; 83 private static final float MIN_DOT_HIT_FACTOR = 0.2f; 84 private final CellState[][] mCellStates; 85 86 private final int mDotSize; 87 private final int mDotSizeActivated; 88 private final float mDotHitFactor; 89 private final int mPathWidth; 90 private final int mLineFadeOutAnimationDurationMs; 91 private final int mLineFadeOutAnimationDelayMs; 92 93 private boolean mDrawingProfilingStarted = false; 94 95 @UnsupportedAppUsage 96 private final Paint mPaint = new Paint(); 97 @UnsupportedAppUsage 98 private final Paint mPathPaint = new Paint(); 99 100 /** 101 * How many milliseconds we spend animating each circle of a lock pattern 102 * if the animating mode is set. The entire animation should take this 103 * constant * the length of the pattern to complete. 104 */ 105 private static final int MILLIS_PER_CIRCLE_ANIMATING = 700; 106 107 /** 108 * This can be used to avoid updating the display for very small motions or noisy panels. 109 * It didn't seem to have much impact on the devices tested, so currently set to 0. 110 */ 111 private static final float DRAG_THRESHHOLD = 0.0f; 112 public static final int VIRTUAL_BASE_VIEW_ID = 1; 113 public static final boolean DEBUG_A11Y = false; 114 private static final String TAG = "LockPatternView"; 115 116 private OnPatternListener mOnPatternListener; 117 @UnsupportedAppUsage 118 private final ArrayList<Cell> mPattern = new ArrayList<Cell>(9); 119 120 /** 121 * Lookup table for the circles of the pattern we are currently drawing. 122 * This will be the cells of the complete pattern unless we are animating, 123 * in which case we use this to hold the cells we are drawing for the in 124 * progress animation. 125 */ 126 private final boolean[][] mPatternDrawLookup = new boolean[3][3]; 127 128 /** 129 * the in progress point: 130 * - during interaction: where the user's finger is 131 * - during animation: the current tip of the animating line 132 */ 133 private float mInProgressX = -1; 134 private float mInProgressY = -1; 135 136 private long mAnimatingPeriodStart; 137 private long[] mLineFadeStart = new long[9]; 138 139 @UnsupportedAppUsage 140 private DisplayMode mPatternDisplayMode = DisplayMode.Correct; 141 private boolean mInputEnabled = true; 142 @UnsupportedAppUsage 143 private boolean mInStealthMode = false; 144 @UnsupportedAppUsage 145 private boolean mPatternInProgress = false; 146 private boolean mFadePattern = true; 147 148 @UnsupportedAppUsage 149 private float mSquareWidth; 150 @UnsupportedAppUsage 151 private float mSquareHeight; 152 private float mDotHitRadius; 153 private final LinearGradient mFadeOutGradientShader; 154 155 private final Path mCurrentPath = new Path(); 156 private final Rect mInvalidate = new Rect(); 157 private final Rect mTmpInvalidateRect = new Rect(); 158 159 private int mAspect; 160 private int mRegularColor; 161 private int mErrorColor; 162 private int mSuccessColor; 163 private int mDotColor; 164 private int mDotActivatedColor; 165 166 private final Interpolator mFastOutSlowInInterpolator; 167 private final Interpolator mLinearOutSlowInInterpolator; 168 private final PatternExploreByTouchHelper mExploreByTouchHelper; 169 170 private Drawable mSelectedDrawable; 171 private Drawable mNotSelectedDrawable; 172 private boolean mUseLockPatternDrawable; 173 174 /** 175 * Represents a cell in the 3 X 3 matrix of the unlock pattern view. 176 */ 177 public static final class Cell { 178 @UnsupportedAppUsage 179 final int row; 180 @UnsupportedAppUsage 181 final int column; 182 183 // keep # objects limited to 9 184 private static final Cell[][] sCells = createCells(); 185 createCells()186 private static Cell[][] createCells() { 187 Cell[][] res = new Cell[3][3]; 188 for (int i = 0; i < 3; i++) { 189 for (int j = 0; j < 3; j++) { 190 res[i][j] = new Cell(i, j); 191 } 192 } 193 return res; 194 } 195 196 /** 197 * @param row The row of the cell. 198 * @param column The column of the cell. 199 */ Cell(int row, int column)200 private Cell(int row, int column) { 201 checkRange(row, column); 202 this.row = row; 203 this.column = column; 204 } 205 getRow()206 public int getRow() { 207 return row; 208 } 209 getColumn()210 public int getColumn() { 211 return column; 212 } 213 of(int row, int column)214 public static Cell of(int row, int column) { 215 checkRange(row, column); 216 return sCells[row][column]; 217 } 218 checkRange(int row, int column)219 private static void checkRange(int row, int column) { 220 if (row < 0 || row > 2) { 221 throw new IllegalArgumentException("row must be in range 0-2"); 222 } 223 if (column < 0 || column > 2) { 224 throw new IllegalArgumentException("column must be in range 0-2"); 225 } 226 } 227 228 @Override toString()229 public String toString() { 230 return "(row=" + row + ",clmn=" + column + ")"; 231 } 232 } 233 234 public static class CellState { 235 int row; 236 int col; 237 boolean hwAnimating; 238 CanvasProperty<Float> hwRadius; 239 CanvasProperty<Float> hwCenterX; 240 CanvasProperty<Float> hwCenterY; 241 CanvasProperty<Paint> hwPaint; 242 float radius; 243 float translationY; 244 float alpha = 1f; 245 float activationAnimationProgress; 246 public float lineEndX = Float.MIN_VALUE; 247 public float lineEndY = Float.MIN_VALUE; 248 @Nullable 249 Animator activationAnimator; 250 } 251 252 /** 253 * How to display the current pattern. 254 */ 255 public enum DisplayMode { 256 257 /** 258 * The pattern drawn is correct (i.e draw it in a friendly color) 259 */ 260 @UnsupportedAppUsage 261 Correct, 262 263 /** 264 * Animate the pattern (for demo, and help). 265 */ 266 @UnsupportedAppUsage 267 Animate, 268 269 /** 270 * The pattern is wrong (i.e draw a foreboding color) 271 */ 272 @UnsupportedAppUsage 273 Wrong 274 } 275 276 /** 277 * The call back interface for detecting patterns entered by the user. 278 */ 279 public static interface OnPatternListener { 280 281 /** 282 * A new pattern has begun. 283 */ onPatternStart()284 void onPatternStart(); 285 286 /** 287 * The pattern was cleared. 288 */ onPatternCleared()289 void onPatternCleared(); 290 291 /** 292 * The user extended the pattern currently being drawn by one cell. 293 * @param pattern The pattern with newly added cell. 294 */ onPatternCellAdded(List<Cell> pattern)295 void onPatternCellAdded(List<Cell> pattern); 296 297 /** 298 * A pattern was detected from the user. 299 * @param pattern The pattern. 300 */ onPatternDetected(List<Cell> pattern)301 void onPatternDetected(List<Cell> pattern); 302 } 303 LockPatternView(Context context)304 public LockPatternView(Context context) { 305 this(context, null); 306 } 307 308 @UnsupportedAppUsage LockPatternView(Context context, AttributeSet attrs)309 public LockPatternView(Context context, AttributeSet attrs) { 310 super(context, attrs); 311 312 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LockPatternView, 313 R.attr.lockPatternStyle, R.style.Widget_LockPatternView); 314 315 final String aspect = a.getString(R.styleable.LockPatternView_aspect); 316 317 if ("square".equals(aspect)) { 318 mAspect = ASPECT_SQUARE; 319 } else if ("lock_width".equals(aspect)) { 320 mAspect = ASPECT_LOCK_WIDTH; 321 } else if ("lock_height".equals(aspect)) { 322 mAspect = ASPECT_LOCK_HEIGHT; 323 } else { 324 mAspect = ASPECT_SQUARE; 325 } 326 327 setClickable(true); 328 329 330 mPathPaint.setAntiAlias(true); 331 mPathPaint.setDither(true); 332 333 mRegularColor = a.getColor(R.styleable.LockPatternView_regularColor, 0); 334 mErrorColor = a.getColor(R.styleable.LockPatternView_errorColor, 0); 335 mSuccessColor = a.getColor(R.styleable.LockPatternView_successColor, 0); 336 mDotColor = a.getColor(R.styleable.LockPatternView_dotColor, mRegularColor); 337 mDotActivatedColor = a.getColor(R.styleable.LockPatternView_dotActivatedColor, mDotColor); 338 339 int pathColor = a.getColor(R.styleable.LockPatternView_pathColor, mRegularColor); 340 mPathPaint.setColor(pathColor); 341 342 mPathPaint.setStyle(Paint.Style.STROKE); 343 mPathPaint.setStrokeJoin(Paint.Join.ROUND); 344 mPathPaint.setStrokeCap(Paint.Cap.ROUND); 345 346 mPathWidth = getResources().getDimensionPixelSize(R.dimen.lock_pattern_dot_line_width); 347 mPathPaint.setStrokeWidth(mPathWidth); 348 349 mLineFadeOutAnimationDurationMs = 350 getResources().getInteger(R.integer.lock_pattern_line_fade_out_duration); 351 mLineFadeOutAnimationDelayMs = 352 getResources().getInteger(R.integer.lock_pattern_line_fade_out_delay); 353 354 mDotSize = getResources().getDimensionPixelSize(R.dimen.lock_pattern_dot_size); 355 mDotSizeActivated = getResources().getDimensionPixelSize( 356 R.dimen.lock_pattern_dot_size_activated); 357 TypedValue outValue = new TypedValue(); 358 getResources().getValue(R.dimen.lock_pattern_dot_hit_factor, outValue, true); 359 mDotHitFactor = Math.max(Math.min(outValue.getFloat(), 1f), MIN_DOT_HIT_FACTOR); 360 361 mUseLockPatternDrawable = getResources().getBoolean(R.bool.use_lock_pattern_drawable); 362 if (mUseLockPatternDrawable) { 363 mSelectedDrawable = getResources().getDrawable(R.drawable.lockscreen_selected); 364 mNotSelectedDrawable = getResources().getDrawable(R.drawable.lockscreen_notselected); 365 } 366 367 mPaint.setAntiAlias(true); 368 mPaint.setDither(true); 369 370 mCellStates = new CellState[3][3]; 371 for (int i = 0; i < 3; i++) { 372 for (int j = 0; j < 3; j++) { 373 mCellStates[i][j] = new CellState(); 374 mCellStates[i][j].radius = mDotSize/2; 375 mCellStates[i][j].row = i; 376 mCellStates[i][j].col = j; 377 } 378 } 379 380 mFastOutSlowInInterpolator = 381 AnimationUtils.loadInterpolator(context, android.R.interpolator.fast_out_slow_in); 382 mLinearOutSlowInInterpolator = 383 AnimationUtils.loadInterpolator(context, android.R.interpolator.linear_out_slow_in); 384 mExploreByTouchHelper = new PatternExploreByTouchHelper(this); 385 setAccessibilityDelegate(mExploreByTouchHelper); 386 387 int fadeAwayGradientWidth = getResources().getDimensionPixelSize( 388 R.dimen.lock_pattern_fade_away_gradient_width); 389 // Set up gradient shader with the middle in point (0, 0). 390 mFadeOutGradientShader = new LinearGradient(/* x0= */ -fadeAwayGradientWidth / 2f, 391 /* y0= */ 0,/* x1= */ fadeAwayGradientWidth / 2f, /* y1= */ 0, 392 Color.TRANSPARENT, pathColor, Shader.TileMode.CLAMP); 393 394 a.recycle(); 395 } 396 397 @UnsupportedAppUsage getCellStates()398 public CellState[][] getCellStates() { 399 return mCellStates; 400 } 401 402 /** 403 * @return Whether the view is in stealth mode. 404 */ isInStealthMode()405 public boolean isInStealthMode() { 406 return mInStealthMode; 407 } 408 409 /** 410 * Set whether the view is in stealth mode. If true, there will be no 411 * visible feedback as the user enters the pattern. 412 * 413 * @param inStealthMode Whether in stealth mode. 414 */ 415 @UnsupportedAppUsage setInStealthMode(boolean inStealthMode)416 public void setInStealthMode(boolean inStealthMode) { 417 mInStealthMode = inStealthMode; 418 } 419 420 /** 421 * Set whether the pattern should fade as it's being drawn. If 422 * true, each segment of the pattern fades over time. 423 */ setFadePattern(boolean fadePattern)424 public void setFadePattern(boolean fadePattern) { 425 mFadePattern = fadePattern; 426 } 427 428 /** 429 * Set the call back for pattern detection. 430 * @param onPatternListener The call back. 431 */ 432 @UnsupportedAppUsage setOnPatternListener( OnPatternListener onPatternListener)433 public void setOnPatternListener( 434 OnPatternListener onPatternListener) { 435 mOnPatternListener = onPatternListener; 436 } 437 438 /** 439 * Set the pattern explicitely (rather than waiting for the user to input 440 * a pattern). 441 * @param displayMode How to display the pattern. 442 * @param pattern The pattern. 443 */ setPattern(DisplayMode displayMode, List<Cell> pattern)444 public void setPattern(DisplayMode displayMode, List<Cell> pattern) { 445 mPattern.clear(); 446 mPattern.addAll(pattern); 447 clearPatternDrawLookup(); 448 for (Cell cell : pattern) { 449 mPatternDrawLookup[cell.getRow()][cell.getColumn()] = true; 450 } 451 452 setDisplayMode(displayMode); 453 } 454 455 /** 456 * Set the display mode of the current pattern. This can be useful, for 457 * instance, after detecting a pattern to tell this view whether change the 458 * in progress result to correct or wrong. 459 * @param displayMode The display mode. 460 */ 461 @UnsupportedAppUsage setDisplayMode(DisplayMode displayMode)462 public void setDisplayMode(DisplayMode displayMode) { 463 mPatternDisplayMode = displayMode; 464 if (displayMode == DisplayMode.Animate) { 465 if (mPattern.size() == 0) { 466 throw new IllegalStateException("you must have a pattern to " 467 + "animate if you want to set the display mode to animate"); 468 } 469 mAnimatingPeriodStart = SystemClock.elapsedRealtime(); 470 final Cell first = mPattern.get(0); 471 mInProgressX = getCenterXForColumn(first.getColumn()); 472 mInProgressY = getCenterYForRow(first.getRow()); 473 clearPatternDrawLookup(); 474 } 475 invalidate(); 476 } 477 startCellStateAnimation(CellState cellState, float startAlpha, float endAlpha, float startTranslationY, float endTranslationY, float startScale, float endScale, long delay, long duration, Interpolator interpolator, Runnable finishRunnable)478 public void startCellStateAnimation(CellState cellState, float startAlpha, float endAlpha, 479 float startTranslationY, float endTranslationY, float startScale, float endScale, 480 long delay, long duration, 481 Interpolator interpolator, Runnable finishRunnable) { 482 if (isHardwareAccelerated()) { 483 startCellStateAnimationHw(cellState, startAlpha, endAlpha, startTranslationY, 484 endTranslationY, startScale, endScale, delay, duration, interpolator, 485 finishRunnable); 486 } else { 487 startCellStateAnimationSw(cellState, startAlpha, endAlpha, startTranslationY, 488 endTranslationY, startScale, endScale, delay, duration, interpolator, 489 finishRunnable); 490 } 491 } 492 startCellStateAnimationSw(final CellState cellState, final float startAlpha, final float endAlpha, final float startTranslationY, final float endTranslationY, final float startScale, final float endScale, long delay, long duration, Interpolator interpolator, final Runnable finishRunnable)493 private void startCellStateAnimationSw(final CellState cellState, 494 final float startAlpha, final float endAlpha, 495 final float startTranslationY, final float endTranslationY, 496 final float startScale, final float endScale, 497 long delay, long duration, Interpolator interpolator, final Runnable finishRunnable) { 498 cellState.alpha = startAlpha; 499 cellState.translationY = startTranslationY; 500 cellState.radius = mDotSize/2 * startScale; 501 ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f); 502 animator.setDuration(duration); 503 animator.setStartDelay(delay); 504 animator.setInterpolator(interpolator); 505 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 506 @Override 507 public void onAnimationUpdate(ValueAnimator animation) { 508 float t = (float) animation.getAnimatedValue(); 509 cellState.alpha = (1 - t) * startAlpha + t * endAlpha; 510 cellState.translationY = (1 - t) * startTranslationY + t * endTranslationY; 511 cellState.radius = mDotSize/2 * ((1 - t) * startScale + t * endScale); 512 invalidate(); 513 } 514 }); 515 animator.addListener(new AnimatorListenerAdapter() { 516 @Override 517 public void onAnimationEnd(Animator animation) { 518 if (finishRunnable != null) { 519 finishRunnable.run(); 520 } 521 } 522 }); 523 animator.start(); 524 } 525 startCellStateAnimationHw(final CellState cellState, float startAlpha, float endAlpha, float startTranslationY, float endTranslationY, float startScale, float endScale, long delay, long duration, Interpolator interpolator, final Runnable finishRunnable)526 private void startCellStateAnimationHw(final CellState cellState, 527 float startAlpha, float endAlpha, 528 float startTranslationY, float endTranslationY, 529 float startScale, float endScale, 530 long delay, long duration, Interpolator interpolator, final Runnable finishRunnable) { 531 cellState.alpha = endAlpha; 532 cellState.translationY = endTranslationY; 533 cellState.radius = mDotSize/2 * endScale; 534 cellState.hwAnimating = true; 535 cellState.hwCenterY = CanvasProperty.createFloat( 536 getCenterYForRow(cellState.row) + startTranslationY); 537 cellState.hwCenterX = CanvasProperty.createFloat(getCenterXForColumn(cellState.col)); 538 cellState.hwRadius = CanvasProperty.createFloat(mDotSize/2 * startScale); 539 mPaint.setColor(getDotColor()); 540 mPaint.setAlpha((int) (startAlpha * 255)); 541 cellState.hwPaint = CanvasProperty.createPaint(new Paint(mPaint)); 542 543 startRtFloatAnimation(cellState.hwCenterY, 544 getCenterYForRow(cellState.row) + endTranslationY, delay, duration, interpolator); 545 startRtFloatAnimation(cellState.hwRadius, mDotSize/2 * endScale, delay, duration, 546 interpolator); 547 startRtAlphaAnimation(cellState, endAlpha, delay, duration, interpolator, 548 new AnimatorListenerAdapter() { 549 @Override 550 public void onAnimationEnd(Animator animation) { 551 cellState.hwAnimating = false; 552 if (finishRunnable != null) { 553 finishRunnable.run(); 554 } 555 } 556 }); 557 558 invalidate(); 559 } 560 startRtAlphaAnimation(CellState cellState, float endAlpha, long delay, long duration, Interpolator interpolator, Animator.AnimatorListener listener)561 private void startRtAlphaAnimation(CellState cellState, float endAlpha, 562 long delay, long duration, Interpolator interpolator, 563 Animator.AnimatorListener listener) { 564 RenderNodeAnimator animator = new RenderNodeAnimator(cellState.hwPaint, 565 RenderNodeAnimator.PAINT_ALPHA, (int) (endAlpha * 255)); 566 animator.setDuration(duration); 567 animator.setStartDelay(delay); 568 animator.setInterpolator(interpolator); 569 animator.setTarget(this); 570 animator.addListener(listener); 571 animator.start(); 572 } 573 startRtFloatAnimation(CanvasProperty<Float> property, float endValue, long delay, long duration, Interpolator interpolator)574 private void startRtFloatAnimation(CanvasProperty<Float> property, float endValue, 575 long delay, long duration, Interpolator interpolator) { 576 RenderNodeAnimator animator = new RenderNodeAnimator(property, endValue); 577 animator.setDuration(duration); 578 animator.setStartDelay(delay); 579 animator.setInterpolator(interpolator); 580 animator.setTarget(this); 581 animator.start(); 582 } 583 notifyCellAdded()584 private void notifyCellAdded() { 585 // sendAccessEvent(R.string.lockscreen_access_pattern_cell_added); 586 if (mOnPatternListener != null) { 587 mOnPatternListener.onPatternCellAdded(mPattern); 588 } 589 // Disable used cells for accessibility as they get added 590 if (DEBUG_A11Y) Log.v(TAG, "ivnalidating root because cell was added."); 591 mExploreByTouchHelper.invalidateRoot(); 592 } 593 notifyPatternStarted()594 private void notifyPatternStarted() { 595 sendAccessEvent(R.string.lockscreen_access_pattern_start); 596 if (mOnPatternListener != null) { 597 mOnPatternListener.onPatternStart(); 598 } 599 } 600 601 @UnsupportedAppUsage notifyPatternDetected()602 private void notifyPatternDetected() { 603 sendAccessEvent(R.string.lockscreen_access_pattern_detected); 604 if (mOnPatternListener != null) { 605 mOnPatternListener.onPatternDetected(mPattern); 606 } 607 } 608 notifyPatternCleared()609 private void notifyPatternCleared() { 610 sendAccessEvent(R.string.lockscreen_access_pattern_cleared); 611 if (mOnPatternListener != null) { 612 mOnPatternListener.onPatternCleared(); 613 } 614 } 615 616 /** 617 * Clear the pattern. 618 */ 619 @UnsupportedAppUsage clearPattern()620 public void clearPattern() { 621 resetPattern(); 622 } 623 624 @Override dispatchHoverEvent(MotionEvent event)625 protected boolean dispatchHoverEvent(MotionEvent event) { 626 // Dispatch to onHoverEvent first so mPatternInProgress is up to date when the 627 // helper gets the event. 628 boolean handled = super.dispatchHoverEvent(event); 629 handled |= mExploreByTouchHelper.dispatchHoverEvent(event); 630 return handled; 631 } 632 633 /** 634 * Reset all pattern state. 635 */ resetPattern()636 private void resetPattern() { 637 mPattern.clear(); 638 clearPatternDrawLookup(); 639 mPatternDisplayMode = DisplayMode.Correct; 640 invalidate(); 641 } 642 643 /** 644 * If there are any cells being drawn. 645 */ isEmpty()646 public boolean isEmpty() { 647 return mPattern.isEmpty(); 648 } 649 650 /** 651 * Clear the pattern lookup table. Also reset the line fade start times for 652 * the next attempt. 653 */ clearPatternDrawLookup()654 private void clearPatternDrawLookup() { 655 for (int i = 0; i < 3; i++) { 656 for (int j = 0; j < 3; j++) { 657 mPatternDrawLookup[i][j] = false; 658 mLineFadeStart[i+j*3] = 0; 659 } 660 } 661 } 662 663 /** 664 * Disable input (for instance when displaying a message that will 665 * timeout so user doesn't get view into messy state). 666 */ 667 @UnsupportedAppUsage disableInput()668 public void disableInput() { 669 mInputEnabled = false; 670 } 671 672 /** 673 * Enable input. 674 */ 675 @UnsupportedAppUsage enableInput()676 public void enableInput() { 677 mInputEnabled = true; 678 } 679 680 @Override onSizeChanged(int w, int h, int oldw, int oldh)681 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 682 final int width = w - mPaddingLeft - mPaddingRight; 683 mSquareWidth = width / 3.0f; 684 685 if (DEBUG_A11Y) Log.v(TAG, "onSizeChanged(" + w + "," + h + ")"); 686 final int height = h - mPaddingTop - mPaddingBottom; 687 mSquareHeight = height / 3.0f; 688 mExploreByTouchHelper.invalidateRoot(); 689 mDotHitRadius = Math.min(mSquareHeight / 2, mSquareWidth / 2) * mDotHitFactor; 690 691 if (mUseLockPatternDrawable) { 692 mNotSelectedDrawable.setBounds(mPaddingLeft, mPaddingTop, width, height); 693 mSelectedDrawable.setBounds(mPaddingLeft, mPaddingTop, width, height); 694 } 695 } 696 resolveMeasured(int measureSpec, int desired)697 private int resolveMeasured(int measureSpec, int desired) 698 { 699 int result = 0; 700 int specSize = MeasureSpec.getSize(measureSpec); 701 switch (MeasureSpec.getMode(measureSpec)) { 702 case MeasureSpec.UNSPECIFIED: 703 result = desired; 704 break; 705 case MeasureSpec.AT_MOST: 706 result = Math.max(specSize, desired); 707 break; 708 case MeasureSpec.EXACTLY: 709 default: 710 result = specSize; 711 } 712 return result; 713 } 714 715 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)716 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 717 final int minimumWidth = getSuggestedMinimumWidth(); 718 final int minimumHeight = getSuggestedMinimumHeight(); 719 int viewWidth = resolveMeasured(widthMeasureSpec, minimumWidth); 720 int viewHeight = resolveMeasured(heightMeasureSpec, minimumHeight); 721 722 switch (mAspect) { 723 case ASPECT_SQUARE: 724 viewWidth = viewHeight = Math.min(viewWidth, viewHeight); 725 break; 726 case ASPECT_LOCK_WIDTH: 727 viewHeight = Math.min(viewWidth, viewHeight); 728 break; 729 case ASPECT_LOCK_HEIGHT: 730 viewWidth = Math.min(viewWidth, viewHeight); 731 break; 732 } 733 // Log.v(TAG, "LockPatternView dimensions: " + viewWidth + "x" + viewHeight); 734 setMeasuredDimension(viewWidth, viewHeight); 735 } 736 737 /** 738 * Determines whether the point x, y will add a new point to the current 739 * pattern (in addition to finding the cell, also makes heuristic choices 740 * such as filling in gaps based on current pattern). 741 * @param x The x coordinate. 742 * @param y The y coordinate. 743 */ detectAndAddHit(float x, float y)744 private Cell detectAndAddHit(float x, float y) { 745 final Cell cell = checkForNewHit(x, y); 746 if (cell != null) { 747 748 // check for gaps in existing pattern 749 Cell fillInGapCell = null; 750 final ArrayList<Cell> pattern = mPattern; 751 if (!pattern.isEmpty()) { 752 final Cell lastCell = pattern.get(pattern.size() - 1); 753 int dRow = cell.row - lastCell.row; 754 int dColumn = cell.column - lastCell.column; 755 756 int fillInRow = lastCell.row; 757 int fillInColumn = lastCell.column; 758 759 if (Math.abs(dRow) == 2 && Math.abs(dColumn) != 1) { 760 fillInRow = lastCell.row + ((dRow > 0) ? 1 : -1); 761 } 762 763 if (Math.abs(dColumn) == 2 && Math.abs(dRow) != 1) { 764 fillInColumn = lastCell.column + ((dColumn > 0) ? 1 : -1); 765 } 766 767 fillInGapCell = Cell.of(fillInRow, fillInColumn); 768 } 769 770 if (fillInGapCell != null && 771 !mPatternDrawLookup[fillInGapCell.row][fillInGapCell.column]) { 772 addCellToPattern(fillInGapCell); 773 } 774 addCellToPattern(cell); 775 performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY, 776 HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING); 777 return cell; 778 } 779 return null; 780 } 781 addCellToPattern(Cell newCell)782 private void addCellToPattern(Cell newCell) { 783 mPatternDrawLookup[newCell.getRow()][newCell.getColumn()] = true; 784 mPattern.add(newCell); 785 if (!mInStealthMode) { 786 startCellActivatedAnimation(newCell); 787 } 788 notifyCellAdded(); 789 } 790 startCellActivatedAnimation(Cell cell)791 private void startCellActivatedAnimation(Cell cell) { 792 final CellState cellState = mCellStates[cell.row][cell.column]; 793 794 if (cellState.activationAnimator != null) { 795 cellState.activationAnimator.cancel(); 796 } 797 AnimatorSet animatorSet = new AnimatorSet(); 798 AnimatorSet.Builder animatorSetBuilder = animatorSet 799 .play(createLineDisappearingAnimation()) 800 .with(createLineEndAnimation(cellState, mInProgressX, mInProgressY, 801 getCenterXForColumn(cell.column), getCenterYForRow(cell.row))); 802 if (mDotSize != mDotSizeActivated) { 803 animatorSetBuilder.with(createDotRadiusAnimation(cellState)); 804 } 805 if (mDotColor != mDotActivatedColor) { 806 animatorSetBuilder.with(createDotActivationColorAnimation(cellState)); 807 } 808 809 animatorSet.addListener(new AnimatorListenerAdapter() { 810 @Override 811 public void onAnimationEnd(Animator animation) { 812 cellState.activationAnimator = null; 813 invalidate(); 814 } 815 }); 816 cellState.activationAnimator = animatorSet; 817 animatorSet.start(); 818 } 819 createDotActivationColorAnimation(CellState cellState)820 private Animator createDotActivationColorAnimation(CellState cellState) { 821 ValueAnimator.AnimatorUpdateListener updateListener = 822 valueAnimator -> { 823 cellState.activationAnimationProgress = 824 (float) valueAnimator.getAnimatedValue(); 825 invalidate(); 826 }; 827 ValueAnimator activateAnimator = ValueAnimator.ofFloat(0f, 1f); 828 ValueAnimator deactivateAnimator = ValueAnimator.ofFloat(1f, 0f); 829 activateAnimator.addUpdateListener(updateListener); 830 deactivateAnimator.addUpdateListener(updateListener); 831 activateAnimator.setInterpolator(mFastOutSlowInInterpolator); 832 deactivateAnimator.setInterpolator(mLinearOutSlowInInterpolator); 833 834 // Align dot animation duration with line fade out animation. 835 activateAnimator.setDuration(DOT_ACTIVATION_DURATION_MILLIS); 836 deactivateAnimator.setDuration(DOT_ACTIVATION_DURATION_MILLIS); 837 AnimatorSet set = new AnimatorSet(); 838 set.play(deactivateAnimator) 839 .after(mLineFadeOutAnimationDelayMs + mLineFadeOutAnimationDurationMs 840 - DOT_ACTIVATION_DURATION_MILLIS * 2) 841 .after(activateAnimator); 842 return set; 843 } 844 845 /** 846 * On the last frame before cell activates the end point of in progress line is not aligned 847 * with dot center so we execute a short animation moving the end point to exact dot center. 848 */ createLineEndAnimation(final CellState state, final float startX, final float startY, final float targetX, final float targetY)849 private Animator createLineEndAnimation(final CellState state, 850 final float startX, final float startY, final float targetX, final float targetY) { 851 ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1); 852 valueAnimator.addUpdateListener(animation -> { 853 float t = (float) animation.getAnimatedValue(); 854 state.lineEndX = (1 - t) * startX + t * targetX; 855 state.lineEndY = (1 - t) * startY + t * targetY; 856 invalidate(); 857 }); 858 valueAnimator.setInterpolator(mFastOutSlowInInterpolator); 859 valueAnimator.setDuration(LINE_END_ANIMATION_DURATION_MILLIS); 860 return valueAnimator; 861 } 862 863 /** 864 * Starts animator to fade out a line segment. It does only invalidate because all the 865 * transitions are applied in {@code onDraw} method. 866 */ createLineDisappearingAnimation()867 private Animator createLineDisappearingAnimation() { 868 ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1); 869 valueAnimator.addUpdateListener(animation -> invalidate()); 870 valueAnimator.setStartDelay(mLineFadeOutAnimationDelayMs); 871 valueAnimator.setDuration(mLineFadeOutAnimationDurationMs); 872 return valueAnimator; 873 } 874 createDotRadiusAnimation(CellState state)875 private Animator createDotRadiusAnimation(CellState state) { 876 float defaultRadius = mDotSize / 2f; 877 float activatedRadius = mDotSizeActivated / 2f; 878 879 ValueAnimator.AnimatorUpdateListener animatorUpdateListener = 880 animation -> { 881 state.radius = (float) animation.getAnimatedValue(); 882 invalidate(); 883 }; 884 885 ValueAnimator activationAnimator = ValueAnimator.ofFloat(defaultRadius, activatedRadius); 886 activationAnimator.addUpdateListener(animatorUpdateListener); 887 activationAnimator.setInterpolator(mLinearOutSlowInInterpolator); 888 activationAnimator.setDuration(DOT_RADIUS_INCREASE_DURATION_MILLIS); 889 890 ValueAnimator deactivationAnimator = ValueAnimator.ofFloat(activatedRadius, defaultRadius); 891 deactivationAnimator.addUpdateListener(animatorUpdateListener); 892 deactivationAnimator.setInterpolator(mFastOutSlowInInterpolator); 893 deactivationAnimator.setDuration(DOT_RADIUS_DECREASE_DURATION_MILLIS); 894 895 AnimatorSet set = new AnimatorSet(); 896 set.playSequentially(activationAnimator, deactivationAnimator); 897 return set; 898 } 899 900 @Nullable checkForNewHit(float x, float y)901 private Cell checkForNewHit(float x, float y) { 902 Cell cellHit = detectCellHit(x, y); 903 if (cellHit != null && !mPatternDrawLookup[cellHit.row][cellHit.column]) { 904 return cellHit; 905 } 906 return null; 907 } 908 909 /** Helper method to find which cell a point maps to. */ 910 @Nullable detectCellHit(float x, float y)911 private Cell detectCellHit(float x, float y) { 912 final float hitRadiusSquared = mDotHitRadius * mDotHitRadius; 913 for (int row = 0; row < 3; row++) { 914 for (int column = 0; column < 3; column++) { 915 float centerY = getCenterYForRow(row); 916 float centerX = getCenterXForColumn(column); 917 if ((x - centerX) * (x - centerX) + (y - centerY) * (y - centerY) 918 < hitRadiusSquared) { 919 return Cell.of(row, column); 920 } 921 } 922 } 923 return null; 924 } 925 926 @Override onHoverEvent(MotionEvent event)927 public boolean onHoverEvent(MotionEvent event) { 928 if (AccessibilityManager.getInstance(mContext).isTouchExplorationEnabled()) { 929 final int action = event.getAction(); 930 switch (action) { 931 case MotionEvent.ACTION_HOVER_ENTER: 932 event.setAction(MotionEvent.ACTION_DOWN); 933 break; 934 case MotionEvent.ACTION_HOVER_MOVE: 935 event.setAction(MotionEvent.ACTION_MOVE); 936 break; 937 case MotionEvent.ACTION_HOVER_EXIT: 938 event.setAction(MotionEvent.ACTION_UP); 939 break; 940 } 941 onTouchEvent(event); 942 event.setAction(action); 943 } 944 return super.onHoverEvent(event); 945 } 946 947 @Override onTouchEvent(MotionEvent event)948 public boolean onTouchEvent(MotionEvent event) { 949 if (!mInputEnabled || !isEnabled()) { 950 return false; 951 } 952 953 switch(event.getAction()) { 954 case MotionEvent.ACTION_DOWN: 955 handleActionDown(event); 956 return true; 957 case MotionEvent.ACTION_UP: 958 handleActionUp(); 959 return true; 960 case MotionEvent.ACTION_MOVE: 961 handleActionMove(event); 962 return true; 963 case MotionEvent.ACTION_CANCEL: 964 if (mPatternInProgress) { 965 setPatternInProgress(false); 966 resetPattern(); 967 notifyPatternCleared(); 968 } 969 if (PROFILE_DRAWING) { 970 if (mDrawingProfilingStarted) { 971 Debug.stopMethodTracing(); 972 mDrawingProfilingStarted = false; 973 } 974 } 975 return true; 976 } 977 return false; 978 } 979 setPatternInProgress(boolean progress)980 private void setPatternInProgress(boolean progress) { 981 mPatternInProgress = progress; 982 mExploreByTouchHelper.invalidateRoot(); 983 } 984 handleActionMove(MotionEvent event)985 private void handleActionMove(MotionEvent event) { 986 // Handle all recent motion events so we don't skip any cells even when the device 987 // is busy... 988 final float radius = mPathWidth; 989 final int historySize = event.getHistorySize(); 990 mTmpInvalidateRect.setEmpty(); 991 boolean invalidateNow = false; 992 for (int i = 0; i < historySize + 1; i++) { 993 final float x = i < historySize ? event.getHistoricalX(i) : event.getX(); 994 final float y = i < historySize ? event.getHistoricalY(i) : event.getY(); 995 Cell hitCell = detectAndAddHit(x, y); 996 final int patternSize = mPattern.size(); 997 if (hitCell != null && patternSize == 1) { 998 setPatternInProgress(true); 999 notifyPatternStarted(); 1000 } 1001 // note current x and y for rubber banding of in progress patterns 1002 final float dx = Math.abs(x - mInProgressX); 1003 final float dy = Math.abs(y - mInProgressY); 1004 if (dx > DRAG_THRESHHOLD || dy > DRAG_THRESHHOLD) { 1005 invalidateNow = true; 1006 } 1007 1008 if (mPatternInProgress && patternSize > 0) { 1009 final ArrayList<Cell> pattern = mPattern; 1010 final Cell lastCell = pattern.get(patternSize - 1); 1011 float lastCellCenterX = getCenterXForColumn(lastCell.column); 1012 float lastCellCenterY = getCenterYForRow(lastCell.row); 1013 1014 // Adjust for drawn segment from last cell to (x,y). Radius accounts for line width. 1015 float left = Math.min(lastCellCenterX, x) - radius; 1016 float right = Math.max(lastCellCenterX, x) + radius; 1017 float top = Math.min(lastCellCenterY, y) - radius; 1018 float bottom = Math.max(lastCellCenterY, y) + radius; 1019 1020 // Invalidate between the pattern's new cell and the pattern's previous cell 1021 if (hitCell != null) { 1022 final float width = mSquareWidth * 0.5f; 1023 final float height = mSquareHeight * 0.5f; 1024 final float hitCellCenterX = getCenterXForColumn(hitCell.column); 1025 final float hitCellCenterY = getCenterYForRow(hitCell.row); 1026 1027 left = Math.min(hitCellCenterX - width, left); 1028 right = Math.max(hitCellCenterX + width, right); 1029 top = Math.min(hitCellCenterY - height, top); 1030 bottom = Math.max(hitCellCenterY + height, bottom); 1031 } 1032 1033 // Invalidate between the pattern's last cell and the previous location 1034 mTmpInvalidateRect.union(Math.round(left), Math.round(top), 1035 Math.round(right), Math.round(bottom)); 1036 } 1037 } 1038 mInProgressX = event.getX(); 1039 mInProgressY = event.getY(); 1040 1041 // To save updates, we only invalidate if the user moved beyond a certain amount. 1042 if (invalidateNow) { 1043 mInvalidate.union(mTmpInvalidateRect); 1044 invalidate(mInvalidate); 1045 mInvalidate.set(mTmpInvalidateRect); 1046 } 1047 } 1048 sendAccessEvent(int resId)1049 private void sendAccessEvent(int resId) { 1050 announceForAccessibility(mContext.getString(resId)); 1051 } 1052 handleActionUp()1053 private void handleActionUp() { 1054 // report pattern detected 1055 if (!mPattern.isEmpty()) { 1056 setPatternInProgress(false); 1057 cancelLineAnimations(); 1058 notifyPatternDetected(); 1059 // Also clear pattern if fading is enabled 1060 if (mFadePattern) { 1061 clearPatternDrawLookup(); 1062 mPatternDisplayMode = DisplayMode.Correct; 1063 } 1064 invalidate(); 1065 } 1066 if (PROFILE_DRAWING) { 1067 if (mDrawingProfilingStarted) { 1068 Debug.stopMethodTracing(); 1069 mDrawingProfilingStarted = false; 1070 } 1071 } 1072 } 1073 cancelLineAnimations()1074 private void cancelLineAnimations() { 1075 for (int i = 0; i < 3; i++) { 1076 for (int j = 0; j < 3; j++) { 1077 CellState state = mCellStates[i][j]; 1078 if (state.activationAnimator != null) { 1079 state.activationAnimator.cancel(); 1080 state.activationAnimator = null; 1081 state.radius = mDotSize / 2f; 1082 state.activationAnimationProgress = 0f; 1083 state.lineEndX = Float.MIN_VALUE; 1084 state.lineEndY = Float.MIN_VALUE; 1085 } 1086 } 1087 } 1088 } handleActionDown(MotionEvent event)1089 private void handleActionDown(MotionEvent event) { 1090 resetPattern(); 1091 final float x = event.getX(); 1092 final float y = event.getY(); 1093 final Cell hitCell = detectAndAddHit(x, y); 1094 if (hitCell != null) { 1095 setPatternInProgress(true); 1096 mPatternDisplayMode = DisplayMode.Correct; 1097 notifyPatternStarted(); 1098 } else if (mPatternInProgress) { 1099 setPatternInProgress(false); 1100 notifyPatternCleared(); 1101 } 1102 if (hitCell != null) { 1103 final float startX = getCenterXForColumn(hitCell.column); 1104 final float startY = getCenterYForRow(hitCell.row); 1105 1106 final float widthOffset = mSquareWidth / 2f; 1107 final float heightOffset = mSquareHeight / 2f; 1108 1109 invalidate((int) (startX - widthOffset), (int) (startY - heightOffset), 1110 (int) (startX + widthOffset), (int) (startY + heightOffset)); 1111 } 1112 mInProgressX = x; 1113 mInProgressY = y; 1114 if (PROFILE_DRAWING) { 1115 if (!mDrawingProfilingStarted) { 1116 Debug.startMethodTracing("LockPatternDrawing"); 1117 mDrawingProfilingStarted = true; 1118 } 1119 } 1120 } 1121 1122 /** 1123 * Change theme colors 1124 * @param regularColor The dot color 1125 * @param successColor Color used when pattern is correct 1126 * @param errorColor Color used when authentication fails 1127 */ setColors(int regularColor, int successColor, int errorColor)1128 public void setColors(int regularColor, int successColor, int errorColor) { 1129 mRegularColor = regularColor; 1130 mErrorColor = errorColor; 1131 mSuccessColor = successColor; 1132 mPathPaint.setColor(regularColor); 1133 invalidate(); 1134 } 1135 getCenterXForColumn(int column)1136 private float getCenterXForColumn(int column) { 1137 return mPaddingLeft + column * mSquareWidth + mSquareWidth / 2f; 1138 } 1139 getCenterYForRow(int row)1140 private float getCenterYForRow(int row) { 1141 return mPaddingTop + row * mSquareHeight + mSquareHeight / 2f; 1142 } 1143 1144 @Override onDraw(Canvas canvas)1145 protected void onDraw(Canvas canvas) { 1146 final ArrayList<Cell> pattern = mPattern; 1147 final int count = pattern.size(); 1148 final boolean[][] drawLookup = mPatternDrawLookup; 1149 1150 if (mPatternDisplayMode == DisplayMode.Animate) { 1151 1152 // figure out which circles to draw 1153 1154 // + 1 so we pause on complete pattern 1155 final int oneCycle = (count + 1) * MILLIS_PER_CIRCLE_ANIMATING; 1156 final int spotInCycle = (int) (SystemClock.elapsedRealtime() - 1157 mAnimatingPeriodStart) % oneCycle; 1158 final int numCircles = spotInCycle / MILLIS_PER_CIRCLE_ANIMATING; 1159 1160 clearPatternDrawLookup(); 1161 for (int i = 0; i < numCircles; i++) { 1162 final Cell cell = pattern.get(i); 1163 drawLookup[cell.getRow()][cell.getColumn()] = true; 1164 } 1165 1166 // figure out in progress portion of ghosting line 1167 1168 final boolean needToUpdateInProgressPoint = numCircles > 0 1169 && numCircles < count; 1170 1171 if (needToUpdateInProgressPoint) { 1172 final float percentageOfNextCircle = 1173 ((float) (spotInCycle % MILLIS_PER_CIRCLE_ANIMATING)) / 1174 MILLIS_PER_CIRCLE_ANIMATING; 1175 1176 final Cell currentCell = pattern.get(numCircles - 1); 1177 final float centerX = getCenterXForColumn(currentCell.column); 1178 final float centerY = getCenterYForRow(currentCell.row); 1179 1180 final Cell nextCell = pattern.get(numCircles); 1181 final float dx = percentageOfNextCircle * 1182 (getCenterXForColumn(nextCell.column) - centerX); 1183 final float dy = percentageOfNextCircle * 1184 (getCenterYForRow(nextCell.row) - centerY); 1185 mInProgressX = centerX + dx; 1186 mInProgressY = centerY + dy; 1187 } 1188 // TODO: Infinite loop here... 1189 invalidate(); 1190 } 1191 1192 final Path currentPath = mCurrentPath; 1193 currentPath.rewind(); 1194 1195 // TODO: the path should be created and cached every time we hit-detect a cell 1196 // only the last segment of the path should be computed here 1197 // draw the path of the pattern (unless we are in stealth mode) 1198 final boolean drawPath = !mInStealthMode; 1199 1200 if (drawPath) { 1201 mPathPaint.setColor(getCurrentColor(true /* partOfPattern */)); 1202 1203 boolean anyCircles = false; 1204 float lastX = 0f; 1205 float lastY = 0f; 1206 long elapsedRealtime = SystemClock.elapsedRealtime(); 1207 for (int i = 0; i < count; i++) { 1208 Cell cell = pattern.get(i); 1209 1210 // only draw the part of the pattern stored in 1211 // the lookup table (this is only different in the case 1212 // of animation). 1213 if (!drawLookup[cell.row][cell.column]) { 1214 break; 1215 } 1216 anyCircles = true; 1217 1218 if (mLineFadeStart[i] == 0) { 1219 mLineFadeStart[i] = SystemClock.elapsedRealtime(); 1220 } 1221 1222 float centerX = getCenterXForColumn(cell.column); 1223 float centerY = getCenterYForRow(cell.row); 1224 if (i != 0) { 1225 CellState state = mCellStates[cell.row][cell.column]; 1226 currentPath.rewind(); 1227 float endX; 1228 float endY; 1229 if (state.lineEndX != Float.MIN_VALUE && state.lineEndY != Float.MIN_VALUE) { 1230 endX = state.lineEndX; 1231 endY = state.lineEndY; 1232 } else { 1233 endX = centerX; 1234 endY = centerY; 1235 } 1236 drawLineSegment(canvas, /* startX = */ lastX, /* startY = */ lastY, endX, endY, 1237 mLineFadeStart[i], elapsedRealtime); 1238 } 1239 lastX = centerX; 1240 lastY = centerY; 1241 } 1242 1243 // draw last in progress section 1244 if ((mPatternInProgress || mPatternDisplayMode == DisplayMode.Animate) 1245 && anyCircles) { 1246 currentPath.rewind(); 1247 currentPath.moveTo(lastX, lastY); 1248 currentPath.lineTo(mInProgressX, mInProgressY); 1249 1250 mPathPaint.setAlpha((int) (calculateLastSegmentAlpha( 1251 mInProgressX, mInProgressY, lastX, lastY) * 255f)); 1252 canvas.drawPath(currentPath, mPathPaint); 1253 } 1254 } 1255 1256 // draw the circles 1257 for (int i = 0; i < 3; i++) { 1258 float centerY = getCenterYForRow(i); 1259 for (int j = 0; j < 3; j++) { 1260 CellState cellState = mCellStates[i][j]; 1261 float centerX = getCenterXForColumn(j); 1262 float translationY = cellState.translationY; 1263 1264 if (mUseLockPatternDrawable) { 1265 drawCellDrawable(canvas, i, j, cellState.radius, drawLookup[i][j]); 1266 } else { 1267 if (isHardwareAccelerated() && cellState.hwAnimating) { 1268 RecordingCanvas recordingCanvas = (RecordingCanvas) canvas; 1269 recordingCanvas.drawCircle(cellState.hwCenterX, cellState.hwCenterY, 1270 cellState.hwRadius, cellState.hwPaint); 1271 } else { 1272 drawCircle(canvas, (int) centerX, (int) centerY + translationY, 1273 cellState.radius, drawLookup[i][j], cellState.alpha, 1274 cellState.activationAnimationProgress); 1275 } 1276 } 1277 } 1278 } 1279 } 1280 1281 private void drawLineSegment(Canvas canvas, float startX, float startY, float endX, float endY, 1282 long lineFadeStart, long elapsedRealtime) { 1283 float fadeAwayProgress; 1284 if (mFadePattern) { 1285 if (elapsedRealtime - lineFadeStart 1286 >= mLineFadeOutAnimationDelayMs + mLineFadeOutAnimationDurationMs) { 1287 // Time for this segment animation is out so we don't need to draw it. 1288 return; 1289 } 1290 // Set this line segment to fade away animated. 1291 fadeAwayProgress = Math.max( 1292 ((float) (elapsedRealtime - lineFadeStart - mLineFadeOutAnimationDelayMs)) 1293 / mLineFadeOutAnimationDurationMs, 0f); 1294 drawFadingAwayLineSegment(canvas, startX, startY, endX, endY, fadeAwayProgress); 1295 } else { 1296 mPathPaint.setAlpha(255); 1297 canvas.drawLine(startX, startY, endX, endY, mPathPaint); 1298 } 1299 } 1300 1301 private void drawFadingAwayLineSegment(Canvas canvas, float startX, float startY, float endX, 1302 float endY, float fadeAwayProgress) { 1303 mPathPaint.setAlpha((int) (255 * (1 - fadeAwayProgress))); 1304 1305 // To draw gradient segment we use mFadeOutGradientShader which has immutable coordinates 1306 // thus we will need to translate and rotate the canvas. 1307 mPathPaint.setShader(mFadeOutGradientShader); 1308 canvas.save(); 1309 1310 // First translate canvas to gradient middle point. 1311 float gradientMidX = endX * fadeAwayProgress + startX * (1 - fadeAwayProgress); 1312 float gradientMidY = endY * fadeAwayProgress + startY * (1 - fadeAwayProgress); 1313 canvas.translate(gradientMidX, gradientMidY); 1314 1315 // Then rotate it to the direction of the segment. 1316 double segmentAngleRad = Math.atan((endY - startY) / (endX - startX)); 1317 float segmentAngleDegrees = (float) Math.toDegrees(segmentAngleRad); 1318 if (endX - startX < 0) { 1319 // Arc tangent gives us angle degrees [-90; 90] thus to cover [90; 270] degrees we 1320 // need this hack. 1321 segmentAngleDegrees += 180f; 1322 } 1323 canvas.rotate(segmentAngleDegrees); 1324 1325 // Pythagoras theorem. 1326 float segmentLength = (float) Math.hypot(endX - startX, endY - startY); 1327 1328 // Draw the segment in coordinates aligned with shader coordinates. 1329 canvas.drawLine(/* startX= */ -segmentLength * fadeAwayProgress, /* startY= */ 1330 0,/* stopX= */ segmentLength * (1 - fadeAwayProgress), /* stopY= */ 0, mPathPaint); 1331 1332 canvas.restore(); 1333 mPathPaint.setShader(null); 1334 } 1335 1336 private float calculateLastSegmentAlpha(float x, float y, float lastX, float lastY) { 1337 float diffX = x - lastX; 1338 float diffY = y - lastY; 1339 float dist = (float) Math.sqrt(diffX*diffX + diffY*diffY); 1340 float frac = dist/mSquareWidth; 1341 return Math.min(1f, Math.max(0f, (frac - 0.3f) * 4f)); 1342 } 1343 1344 private int getDotColor() { 1345 if (mInStealthMode) { 1346 // Always use the default color in this case 1347 return mDotColor; 1348 } else if (mPatternDisplayMode == DisplayMode.Wrong) { 1349 // the pattern is wrong 1350 return mErrorColor; 1351 } 1352 return mDotColor; 1353 } 1354 1355 private int getCurrentColor(boolean partOfPattern) { 1356 if (!partOfPattern || mInStealthMode || mPatternInProgress) { 1357 // unselected circle 1358 return mRegularColor; 1359 } else if (mPatternDisplayMode == DisplayMode.Wrong) { 1360 // the pattern is wrong 1361 return mErrorColor; 1362 } else if (mPatternDisplayMode == DisplayMode.Correct || 1363 mPatternDisplayMode == DisplayMode.Animate) { 1364 return mSuccessColor; 1365 } else { 1366 throw new IllegalStateException("unknown display mode " + mPatternDisplayMode); 1367 } 1368 } 1369 1370 /** 1371 * @param partOfPattern Whether this circle is part of the pattern. 1372 */ 1373 private void drawCircle(Canvas canvas, float centerX, float centerY, float radius, 1374 boolean partOfPattern, float alpha, float activationAnimationProgress) { 1375 if (mFadePattern && !mInStealthMode) { 1376 int resultColor = ColorUtils.blendARGB(mDotColor, mDotActivatedColor, 1377 /* ratio= */ activationAnimationProgress); 1378 mPaint.setColor(resultColor); 1379 } else { 1380 mPaint.setColor(getDotColor()); 1381 } 1382 mPaint.setAlpha((int) (alpha * 255)); 1383 canvas.drawCircle(centerX, centerY, radius, mPaint); 1384 } 1385 1386 /** 1387 * @param partOfPattern Whether this circle is part of the pattern. 1388 */ 1389 private void drawCellDrawable(Canvas canvas, int i, int j, float radius, 1390 boolean partOfPattern) { 1391 Rect dst = new Rect( 1392 (int) (mPaddingLeft + j * mSquareWidth), 1393 (int) (mPaddingTop + i * mSquareHeight), 1394 (int) (mPaddingLeft + (j + 1) * mSquareWidth), 1395 (int) (mPaddingTop + (i + 1) * mSquareHeight)); 1396 float scale = radius / (mDotSize / 2); 1397 1398 // Only draw on this square with the appropriate scale. 1399 canvas.save(); 1400 canvas.clipRect(dst); 1401 canvas.scale(scale, scale, dst.centerX(), dst.centerY()); 1402 if (!partOfPattern || scale > 1) { 1403 mNotSelectedDrawable.draw(canvas); 1404 } else { 1405 mSelectedDrawable.draw(canvas); 1406 } 1407 canvas.restore(); 1408 } 1409 1410 @Override 1411 protected Parcelable onSaveInstanceState() { 1412 Parcelable superState = super.onSaveInstanceState(); 1413 byte[] patternBytes = LockPatternUtils.patternToByteArray(mPattern); 1414 String patternString = patternBytes != null ? new String(patternBytes) : null; 1415 return new SavedState(superState, 1416 patternString, 1417 mPatternDisplayMode.ordinal(), 1418 mInputEnabled, mInStealthMode); 1419 } 1420 1421 @Override 1422 protected void onRestoreInstanceState(Parcelable state) { 1423 final SavedState ss = (SavedState) state; 1424 super.onRestoreInstanceState(ss.getSuperState()); 1425 setPattern( 1426 DisplayMode.Correct, 1427 LockPatternUtils.byteArrayToPattern(ss.getSerializedPattern().getBytes())); 1428 mPatternDisplayMode = DisplayMode.values()[ss.getDisplayMode()]; 1429 mInputEnabled = ss.isInputEnabled(); 1430 mInStealthMode = ss.isInStealthMode(); 1431 } 1432 1433 /** 1434 * The parecelable for saving and restoring a lock pattern view. 1435 */ 1436 private static class SavedState extends BaseSavedState { 1437 1438 private final String mSerializedPattern; 1439 private final int mDisplayMode; 1440 private final boolean mInputEnabled; 1441 private final boolean mInStealthMode; 1442 1443 /** 1444 * Constructor called from {@link LockPatternView#onSaveInstanceState()} 1445 */ 1446 @UnsupportedAppUsage 1447 private SavedState(Parcelable superState, String serializedPattern, int displayMode, 1448 boolean inputEnabled, boolean inStealthMode) { 1449 super(superState); 1450 mSerializedPattern = serializedPattern; 1451 mDisplayMode = displayMode; 1452 mInputEnabled = inputEnabled; 1453 mInStealthMode = inStealthMode; 1454 } 1455 1456 /** 1457 * Constructor called from {@link #CREATOR} 1458 */ 1459 @UnsupportedAppUsage 1460 private SavedState(Parcel in) { 1461 super(in); 1462 mSerializedPattern = in.readString(); 1463 mDisplayMode = in.readInt(); 1464 mInputEnabled = (Boolean) in.readValue(null); 1465 mInStealthMode = (Boolean) in.readValue(null); 1466 } 1467 1468 public String getSerializedPattern() { 1469 return mSerializedPattern; 1470 } 1471 1472 public int getDisplayMode() { 1473 return mDisplayMode; 1474 } 1475 1476 public boolean isInputEnabled() { 1477 return mInputEnabled; 1478 } 1479 1480 public boolean isInStealthMode() { 1481 return mInStealthMode; 1482 } 1483 1484 @Override 1485 public void writeToParcel(Parcel dest, int flags) { 1486 super.writeToParcel(dest, flags); 1487 dest.writeString(mSerializedPattern); 1488 dest.writeInt(mDisplayMode); 1489 dest.writeValue(mInputEnabled); 1490 dest.writeValue(mInStealthMode); 1491 } 1492 1493 @SuppressWarnings({ "unused", "hiding" }) // Found using reflection 1494 public static final Parcelable.Creator<SavedState> CREATOR = 1495 new Creator<SavedState>() { 1496 @Override 1497 public SavedState createFromParcel(Parcel in) { 1498 return new SavedState(in); 1499 } 1500 1501 @Override 1502 public SavedState[] newArray(int size) { 1503 return new SavedState[size]; 1504 } 1505 }; 1506 } 1507 1508 private final class PatternExploreByTouchHelper extends ExploreByTouchHelper { 1509 private Rect mTempRect = new Rect(); 1510 private final SparseArray<VirtualViewContainer> mItems = new SparseArray<>(); 1511 1512 class VirtualViewContainer { 1513 public VirtualViewContainer(CharSequence description) { 1514 this.description = description; 1515 } 1516 CharSequence description; 1517 }; 1518 1519 public PatternExploreByTouchHelper(View forView) { 1520 super(forView); 1521 for (int i = VIRTUAL_BASE_VIEW_ID; i < VIRTUAL_BASE_VIEW_ID + 9; i++) { 1522 mItems.put(i, new VirtualViewContainer(getTextForVirtualView(i))); 1523 } 1524 } 1525 1526 @Override 1527 protected int getVirtualViewAt(float x, float y) { 1528 // This must use the same hit logic for the screen to ensure consistency whether 1529 // accessibility is on or off. 1530 return getVirtualViewIdForHit(x, y); 1531 } 1532 1533 @Override 1534 protected void getVisibleVirtualViews(IntArray virtualViewIds) { 1535 if (DEBUG_A11Y) Log.v(TAG, "getVisibleVirtualViews(len=" + virtualViewIds.size() + ")"); 1536 if (!mPatternInProgress) { 1537 return; 1538 } 1539 for (int i = VIRTUAL_BASE_VIEW_ID; i < VIRTUAL_BASE_VIEW_ID + 9; i++) { 1540 // Add all views. As views are added to the pattern, we remove them 1541 // from notification by making them non-clickable below. 1542 virtualViewIds.add(i); 1543 } 1544 } 1545 1546 @Override 1547 protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) { 1548 if (DEBUG_A11Y) Log.v(TAG, "onPopulateEventForVirtualView(" + virtualViewId + ")"); 1549 // Announce this view 1550 VirtualViewContainer container = mItems.get(virtualViewId); 1551 if (container != null) { 1552 event.getText().add(container.description); 1553 } 1554 } 1555 1556 @Override 1557 public void onPopulateAccessibilityEvent(View host, AccessibilityEvent event) { 1558 super.onPopulateAccessibilityEvent(host, event); 1559 if (!mPatternInProgress) { 1560 CharSequence contentDescription = getContext().getText( 1561 com.android.internal.R.string.lockscreen_access_pattern_area); 1562 event.setContentDescription(contentDescription); 1563 } 1564 } 1565 1566 @Override 1567 protected void onPopulateNodeForVirtualView(int virtualViewId, AccessibilityNodeInfo node) { 1568 if (DEBUG_A11Y) Log.v(TAG, "onPopulateNodeForVirtualView(view=" + virtualViewId + ")"); 1569 1570 // Node and event text and content descriptions are usually 1571 // identical, so we'll use the exact same string as before. 1572 node.setText(getTextForVirtualView(virtualViewId)); 1573 node.setContentDescription(getTextForVirtualView(virtualViewId)); 1574 1575 if (mPatternInProgress) { 1576 node.setFocusable(true); 1577 1578 if (isClickable(virtualViewId)) { 1579 // Mark this node of interest by making it clickable. 1580 node.addAction(AccessibilityAction.ACTION_CLICK); 1581 node.setClickable(isClickable(virtualViewId)); 1582 } 1583 } 1584 1585 // Compute bounds for this object 1586 final Rect bounds = getBoundsForVirtualView(virtualViewId); 1587 if (DEBUG_A11Y) Log.v(TAG, "bounds:" + bounds.toString()); 1588 node.setBoundsInParent(bounds); 1589 } 1590 1591 private boolean isClickable(int virtualViewId) { 1592 // Dots are clickable if they're not part of the current pattern. 1593 if (virtualViewId != ExploreByTouchHelper.INVALID_ID) { 1594 int row = (virtualViewId - VIRTUAL_BASE_VIEW_ID) / 3; 1595 int col = (virtualViewId - VIRTUAL_BASE_VIEW_ID) % 3; 1596 if (row < 3) { 1597 return !mPatternDrawLookup[row][col]; 1598 } 1599 } 1600 return false; 1601 } 1602 1603 @Override 1604 protected boolean onPerformActionForVirtualView(int virtualViewId, int action, 1605 Bundle arguments) { 1606 if (DEBUG_A11Y) Log.v(TAG, "onPerformActionForVirtualView(id=" + virtualViewId 1607 + ", action=" + action); 1608 switch (action) { 1609 case AccessibilityNodeInfo.ACTION_CLICK: 1610 // Click handling should be consistent with 1611 // onTouchEvent(). This ensures that the view works the 1612 // same whether accessibility is turned on or off. 1613 return onItemClicked(virtualViewId); 1614 default: 1615 if (DEBUG_A11Y) Log.v(TAG, "*** action not handled in " 1616 + "onPerformActionForVirtualView(viewId=" 1617 + virtualViewId + "action=" + action + ")"); 1618 } 1619 return false; 1620 } 1621 1622 boolean onItemClicked(int index) { 1623 if (DEBUG_A11Y) Log.v(TAG, "onItemClicked(" + index + ")"); 1624 1625 // Since the item's checked state is exposed to accessibility 1626 // services through its AccessibilityNodeInfo, we need to invalidate 1627 // the item's virtual view. At some point in the future, the 1628 // framework will obtain an updated version of the virtual view. 1629 invalidateVirtualView(index); 1630 1631 // We need to let the framework know what type of event 1632 // happened. Accessibility services may use this event to provide 1633 // appropriate feedback to the user. 1634 sendEventForVirtualView(index, AccessibilityEvent.TYPE_VIEW_CLICKED); 1635 1636 return true; 1637 } 1638 1639 private Rect getBoundsForVirtualView(int virtualViewId) { 1640 int ordinal = virtualViewId - VIRTUAL_BASE_VIEW_ID; 1641 final Rect bounds = mTempRect; 1642 final int row = ordinal / 3; 1643 final int col = ordinal % 3; 1644 float centerX = getCenterXForColumn(col); 1645 float centerY = getCenterYForRow(row); 1646 float cellHitRadius = mDotHitRadius; 1647 bounds.left = (int) (centerX - cellHitRadius); 1648 bounds.right = (int) (centerX + cellHitRadius); 1649 bounds.top = (int) (centerY - cellHitRadius); 1650 bounds.bottom = (int) (centerY + cellHitRadius); 1651 return bounds; 1652 } 1653 1654 private CharSequence getTextForVirtualView(int virtualViewId) { 1655 final Resources res = getResources(); 1656 return res.getString(R.string.lockscreen_access_pattern_cell_added_verbose, 1657 virtualViewId); 1658 } 1659 1660 /** 1661 * Helper method to find which cell a point maps to 1662 * 1663 * if there's no hit. 1664 * @param x touch position x 1665 * @param y touch position y 1666 * @return VIRTUAL_BASE_VIEW_ID+id or 0 if no view was hit 1667 */ 1668 private int getVirtualViewIdForHit(float x, float y) { 1669 Cell cellHit = detectCellHit(x, y); 1670 if (cellHit == null) { 1671 return ExploreByTouchHelper.INVALID_ID; 1672 } 1673 boolean dotAvailable = mPatternDrawLookup[cellHit.row][cellHit.column]; 1674 int dotId = (cellHit.row * 3 + cellHit.column) + VIRTUAL_BASE_VIEW_ID; 1675 int view = dotAvailable ? dotId : ExploreByTouchHelper.INVALID_ID; 1676 if (DEBUG_A11Y) Log.v(TAG, "getVirtualViewIdForHit(" + x + "," + y + ") => " 1677 + view + "avail =" + dotAvailable); 1678 return view; 1679 } 1680 } 1681 } 1682