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