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