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