• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2007 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.internal.widget;
18 
19 
20 import android.animation.Animator;
21 import android.animation.AnimatorListenerAdapter;
22 import android.animation.ValueAnimator;
23 import android.content.Context;
24 import android.content.res.TypedArray;
25 import android.graphics.Canvas;
26 import android.graphics.Paint;
27 import android.graphics.Path;
28 import android.graphics.Rect;
29 import android.os.Debug;
30 import android.os.Parcel;
31 import android.os.Parcelable;
32 import android.os.SystemClock;
33 import android.util.AttributeSet;
34 import android.view.HapticFeedbackConstants;
35 import android.view.MotionEvent;
36 import android.view.View;
37 import android.view.accessibility.AccessibilityManager;
38 import android.view.animation.AnimationUtils;
39 import android.view.animation.Interpolator;
40 
41 import com.android.internal.R;
42 
43 import java.util.ArrayList;
44 import java.util.List;
45 
46 /**
47  * Displays and detects the user's unlock attempt, which is a drag of a finger
48  * across 9 regions of the screen.
49  *
50  * Is also capable of displaying a static pattern in "in progress", "wrong" or
51  * "correct" states.
52  */
53 public class LockPatternView extends View {
54     // Aspect to use when rendering this view
55     private static final int ASPECT_SQUARE = 0; // View will be the minimum of width/height
56     private static final int ASPECT_LOCK_WIDTH = 1; // Fixed width; height will be minimum of (w,h)
57     private static final int ASPECT_LOCK_HEIGHT = 2; // Fixed height; width will be minimum of (w,h)
58 
59     private static final boolean PROFILE_DRAWING = false;
60     private final CellState[][] mCellStates;
61 
62     private final int mDotSize;
63     private final int mDotSizeActivated;
64     private final int mPathWidth;
65 
66     private boolean mDrawingProfilingStarted = false;
67 
68     private Paint mPaint = new Paint();
69     private Paint mPathPaint = new Paint();
70 
71     /**
72      * How many milliseconds we spend animating each circle of a lock pattern
73      * if the animating mode is set.  The entire animation should take this
74      * constant * the length of the pattern to complete.
75      */
76     private static final int MILLIS_PER_CIRCLE_ANIMATING = 700;
77 
78     /**
79      * This can be used to avoid updating the display for very small motions or noisy panels.
80      * It didn't seem to have much impact on the devices tested, so currently set to 0.
81      */
82     private static final float DRAG_THRESHHOLD = 0.0f;
83 
84     private OnPatternListener mOnPatternListener;
85     private ArrayList<Cell> mPattern = new ArrayList<Cell>(9);
86 
87     /**
88      * Lookup table for the circles of the pattern we are currently drawing.
89      * This will be the cells of the complete pattern unless we are animating,
90      * in which case we use this to hold the cells we are drawing for the in
91      * progress animation.
92      */
93     private boolean[][] mPatternDrawLookup = new boolean[3][3];
94 
95     /**
96      * the in progress point:
97      * - during interaction: where the user's finger is
98      * - during animation: the current tip of the animating line
99      */
100     private float mInProgressX = -1;
101     private float mInProgressY = -1;
102 
103     private long mAnimatingPeriodStart;
104 
105     private DisplayMode mPatternDisplayMode = DisplayMode.Correct;
106     private boolean mInputEnabled = true;
107     private boolean mInStealthMode = false;
108     private boolean mEnableHapticFeedback = true;
109     private boolean mPatternInProgress = false;
110 
111     private float mHitFactor = 0.6f;
112 
113     private float mSquareWidth;
114     private float mSquareHeight;
115 
116     private final Path mCurrentPath = new Path();
117     private final Rect mInvalidate = new Rect();
118     private final Rect mTmpInvalidateRect = new Rect();
119 
120     private int mAspect;
121     private int mRegularColor;
122     private int mErrorColor;
123     private int mSuccessColor;
124 
125     private Interpolator mFastOutSlowInInterpolator;
126     private Interpolator mLinearOutSlowInInterpolator;
127 
128     /**
129      * Represents a cell in the 3 X 3 matrix of the unlock pattern view.
130      */
131     public static class Cell {
132         int row;
133         int column;
134 
135         // keep # objects limited to 9
136         static Cell[][] sCells = new Cell[3][3];
137         static {
138             for (int i = 0; i < 3; i++) {
139                 for (int j = 0; j < 3; j++) {
140                     sCells[i][j] = new Cell(i, j);
141                 }
142             }
143         }
144 
145         /**
146          * @param row The row of the cell.
147          * @param column The column of the cell.
148          */
Cell(int row, int column)149         private Cell(int row, int column) {
150             checkRange(row, column);
151             this.row = row;
152             this.column = column;
153         }
154 
getRow()155         public int getRow() {
156             return row;
157         }
158 
getColumn()159         public int getColumn() {
160             return column;
161         }
162 
163         /**
164          * @param row The row of the cell.
165          * @param column The column of the cell.
166          */
of(int row, int column)167         public static synchronized Cell of(int row, int column) {
168             checkRange(row, column);
169             return sCells[row][column];
170         }
171 
checkRange(int row, int column)172         private static void checkRange(int row, int column) {
173             if (row < 0 || row > 2) {
174                 throw new IllegalArgumentException("row must be in range 0-2");
175             }
176             if (column < 0 || column > 2) {
177                 throw new IllegalArgumentException("column must be in range 0-2");
178             }
179         }
180 
toString()181         public String toString() {
182             return "(row=" + row + ",clmn=" + column + ")";
183         }
184     }
185 
186     public static class CellState {
187         public float scale = 1.0f;
188         public float translateY = 0.0f;
189         public float alpha = 1.0f;
190         public float size;
191         public float lineEndX = Float.MIN_VALUE;
192         public float lineEndY = Float.MIN_VALUE;
193         public ValueAnimator lineAnimator;
194      }
195 
196     /**
197      * How to display the current pattern.
198      */
199     public enum DisplayMode {
200 
201         /**
202          * The pattern drawn is correct (i.e draw it in a friendly color)
203          */
204         Correct,
205 
206         /**
207          * Animate the pattern (for demo, and help).
208          */
209         Animate,
210 
211         /**
212          * The pattern is wrong (i.e draw a foreboding color)
213          */
214         Wrong
215     }
216 
217     /**
218      * The call back interface for detecting patterns entered by the user.
219      */
220     public static interface OnPatternListener {
221 
222         /**
223          * A new pattern has begun.
224          */
onPatternStart()225         void onPatternStart();
226 
227         /**
228          * The pattern was cleared.
229          */
onPatternCleared()230         void onPatternCleared();
231 
232         /**
233          * The user extended the pattern currently being drawn by one cell.
234          * @param pattern The pattern with newly added cell.
235          */
onPatternCellAdded(List<Cell> pattern)236         void onPatternCellAdded(List<Cell> pattern);
237 
238         /**
239          * A pattern was detected from the user.
240          * @param pattern The pattern.
241          */
onPatternDetected(List<Cell> pattern)242         void onPatternDetected(List<Cell> pattern);
243     }
244 
LockPatternView(Context context)245     public LockPatternView(Context context) {
246         this(context, null);
247     }
248 
LockPatternView(Context context, AttributeSet attrs)249     public LockPatternView(Context context, AttributeSet attrs) {
250         super(context, attrs);
251 
252         TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LockPatternView);
253 
254         final String aspect = a.getString(R.styleable.LockPatternView_aspect);
255 
256         if ("square".equals(aspect)) {
257             mAspect = ASPECT_SQUARE;
258         } else if ("lock_width".equals(aspect)) {
259             mAspect = ASPECT_LOCK_WIDTH;
260         } else if ("lock_height".equals(aspect)) {
261             mAspect = ASPECT_LOCK_HEIGHT;
262         } else {
263             mAspect = ASPECT_SQUARE;
264         }
265 
266         setClickable(true);
267 
268 
269         mPathPaint.setAntiAlias(true);
270         mPathPaint.setDither(true);
271 
272         mRegularColor = getResources().getColor(R.color.lock_pattern_view_regular_color);
273         mErrorColor = getResources().getColor(R.color.lock_pattern_view_error_color);
274         mSuccessColor = getResources().getColor(R.color.lock_pattern_view_success_color);
275         mRegularColor = a.getColor(R.styleable.LockPatternView_regularColor, mRegularColor);
276         mErrorColor = a.getColor(R.styleable.LockPatternView_errorColor, mErrorColor);
277         mSuccessColor = a.getColor(R.styleable.LockPatternView_successColor, mSuccessColor);
278 
279         int pathColor = a.getColor(R.styleable.LockPatternView_pathColor, mRegularColor);
280         mPathPaint.setColor(pathColor);
281 
282         mPathPaint.setStyle(Paint.Style.STROKE);
283         mPathPaint.setStrokeJoin(Paint.Join.ROUND);
284         mPathPaint.setStrokeCap(Paint.Cap.ROUND);
285 
286         mPathWidth = getResources().getDimensionPixelSize(R.dimen.lock_pattern_dot_line_width);
287         mPathPaint.setStrokeWidth(mPathWidth);
288 
289         mDotSize = getResources().getDimensionPixelSize(R.dimen.lock_pattern_dot_size);
290         mDotSizeActivated = getResources().getDimensionPixelSize(
291                 R.dimen.lock_pattern_dot_size_activated);
292 
293         mPaint.setAntiAlias(true);
294         mPaint.setDither(true);
295 
296         mCellStates = new CellState[3][3];
297         for (int i = 0; i < 3; i++) {
298             for (int j = 0; j < 3; j++) {
299                 mCellStates[i][j] = new CellState();
300                 mCellStates[i][j].size = mDotSize;
301             }
302         }
303 
304         mFastOutSlowInInterpolator =
305                 AnimationUtils.loadInterpolator(context, android.R.interpolator.fast_out_slow_in);
306         mLinearOutSlowInInterpolator =
307                 AnimationUtils.loadInterpolator(context, android.R.interpolator.linear_out_slow_in);
308     }
309 
getCellStates()310     public CellState[][] getCellStates() {
311         return mCellStates;
312     }
313 
314     /**
315      * @return Whether the view is in stealth mode.
316      */
isInStealthMode()317     public boolean isInStealthMode() {
318         return mInStealthMode;
319     }
320 
321     /**
322      * @return Whether the view has tactile feedback enabled.
323      */
isTactileFeedbackEnabled()324     public boolean isTactileFeedbackEnabled() {
325         return mEnableHapticFeedback;
326     }
327 
328     /**
329      * Set whether the view is in stealth mode.  If true, there will be no
330      * visible feedback as the user enters the pattern.
331      *
332      * @param inStealthMode Whether in stealth mode.
333      */
setInStealthMode(boolean inStealthMode)334     public void setInStealthMode(boolean inStealthMode) {
335         mInStealthMode = inStealthMode;
336     }
337 
338     /**
339      * Set whether the view will use tactile feedback.  If true, there will be
340      * tactile feedback as the user enters the pattern.
341      *
342      * @param tactileFeedbackEnabled Whether tactile feedback is enabled
343      */
setTactileFeedbackEnabled(boolean tactileFeedbackEnabled)344     public void setTactileFeedbackEnabled(boolean tactileFeedbackEnabled) {
345         mEnableHapticFeedback = tactileFeedbackEnabled;
346     }
347 
348     /**
349      * Set the call back for pattern detection.
350      * @param onPatternListener The call back.
351      */
setOnPatternListener( OnPatternListener onPatternListener)352     public void setOnPatternListener(
353             OnPatternListener onPatternListener) {
354         mOnPatternListener = onPatternListener;
355     }
356 
357     /**
358      * Set the pattern explicitely (rather than waiting for the user to input
359      * a pattern).
360      * @param displayMode How to display the pattern.
361      * @param pattern The pattern.
362      */
setPattern(DisplayMode displayMode, List<Cell> pattern)363     public void setPattern(DisplayMode displayMode, List<Cell> pattern) {
364         mPattern.clear();
365         mPattern.addAll(pattern);
366         clearPatternDrawLookup();
367         for (Cell cell : pattern) {
368             mPatternDrawLookup[cell.getRow()][cell.getColumn()] = true;
369         }
370 
371         setDisplayMode(displayMode);
372     }
373 
374     /**
375      * Set the display mode of the current pattern.  This can be useful, for
376      * instance, after detecting a pattern to tell this view whether change the
377      * in progress result to correct or wrong.
378      * @param displayMode The display mode.
379      */
setDisplayMode(DisplayMode displayMode)380     public void setDisplayMode(DisplayMode displayMode) {
381         mPatternDisplayMode = displayMode;
382         if (displayMode == DisplayMode.Animate) {
383             if (mPattern.size() == 0) {
384                 throw new IllegalStateException("you must have a pattern to "
385                         + "animate if you want to set the display mode to animate");
386             }
387             mAnimatingPeriodStart = SystemClock.elapsedRealtime();
388             final Cell first = mPattern.get(0);
389             mInProgressX = getCenterXForColumn(first.getColumn());
390             mInProgressY = getCenterYForRow(first.getRow());
391             clearPatternDrawLookup();
392         }
393         invalidate();
394     }
395 
notifyCellAdded()396     private void notifyCellAdded() {
397         sendAccessEvent(R.string.lockscreen_access_pattern_cell_added);
398         if (mOnPatternListener != null) {
399             mOnPatternListener.onPatternCellAdded(mPattern);
400         }
401     }
402 
notifyPatternStarted()403     private void notifyPatternStarted() {
404         sendAccessEvent(R.string.lockscreen_access_pattern_start);
405         if (mOnPatternListener != null) {
406             mOnPatternListener.onPatternStart();
407         }
408     }
409 
notifyPatternDetected()410     private void notifyPatternDetected() {
411         sendAccessEvent(R.string.lockscreen_access_pattern_detected);
412         if (mOnPatternListener != null) {
413             mOnPatternListener.onPatternDetected(mPattern);
414         }
415     }
416 
notifyPatternCleared()417     private void notifyPatternCleared() {
418         sendAccessEvent(R.string.lockscreen_access_pattern_cleared);
419         if (mOnPatternListener != null) {
420             mOnPatternListener.onPatternCleared();
421         }
422     }
423 
424     /**
425      * Clear the pattern.
426      */
clearPattern()427     public void clearPattern() {
428         resetPattern();
429     }
430 
431     /**
432      * Reset all pattern state.
433      */
resetPattern()434     private void resetPattern() {
435         mPattern.clear();
436         clearPatternDrawLookup();
437         mPatternDisplayMode = DisplayMode.Correct;
438         invalidate();
439     }
440 
441     /**
442      * Clear the pattern lookup table.
443      */
clearPatternDrawLookup()444     private void clearPatternDrawLookup() {
445         for (int i = 0; i < 3; i++) {
446             for (int j = 0; j < 3; j++) {
447                 mPatternDrawLookup[i][j] = false;
448             }
449         }
450     }
451 
452     /**
453      * Disable input (for instance when displaying a message that will
454      * timeout so user doesn't get view into messy state).
455      */
disableInput()456     public void disableInput() {
457         mInputEnabled = false;
458     }
459 
460     /**
461      * Enable input.
462      */
enableInput()463     public void enableInput() {
464         mInputEnabled = true;
465     }
466 
467     @Override
onSizeChanged(int w, int h, int oldw, int oldh)468     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
469         final int width = w - mPaddingLeft - mPaddingRight;
470         mSquareWidth = width / 3.0f;
471 
472         final int height = h - mPaddingTop - mPaddingBottom;
473         mSquareHeight = height / 3.0f;
474     }
475 
resolveMeasured(int measureSpec, int desired)476     private int resolveMeasured(int measureSpec, int desired)
477     {
478         int result = 0;
479         int specSize = MeasureSpec.getSize(measureSpec);
480         switch (MeasureSpec.getMode(measureSpec)) {
481             case MeasureSpec.UNSPECIFIED:
482                 result = desired;
483                 break;
484             case MeasureSpec.AT_MOST:
485                 result = Math.max(specSize, desired);
486                 break;
487             case MeasureSpec.EXACTLY:
488             default:
489                 result = specSize;
490         }
491         return result;
492     }
493 
494     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)495     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
496         final int minimumWidth = getSuggestedMinimumWidth();
497         final int minimumHeight = getSuggestedMinimumHeight();
498         int viewWidth = resolveMeasured(widthMeasureSpec, minimumWidth);
499         int viewHeight = resolveMeasured(heightMeasureSpec, minimumHeight);
500 
501         switch (mAspect) {
502             case ASPECT_SQUARE:
503                 viewWidth = viewHeight = Math.min(viewWidth, viewHeight);
504                 break;
505             case ASPECT_LOCK_WIDTH:
506                 viewHeight = Math.min(viewWidth, viewHeight);
507                 break;
508             case ASPECT_LOCK_HEIGHT:
509                 viewWidth = Math.min(viewWidth, viewHeight);
510                 break;
511         }
512         // Log.v(TAG, "LockPatternView dimensions: " + viewWidth + "x" + viewHeight);
513         setMeasuredDimension(viewWidth, viewHeight);
514     }
515 
516     /**
517      * Determines whether the point x, y will add a new point to the current
518      * pattern (in addition to finding the cell, also makes heuristic choices
519      * such as filling in gaps based on current pattern).
520      * @param x The x coordinate.
521      * @param y The y coordinate.
522      */
detectAndAddHit(float x, float y)523     private Cell detectAndAddHit(float x, float y) {
524         final Cell cell = checkForNewHit(x, y);
525         if (cell != null) {
526 
527             // check for gaps in existing pattern
528             Cell fillInGapCell = null;
529             final ArrayList<Cell> pattern = mPattern;
530             if (!pattern.isEmpty()) {
531                 final Cell lastCell = pattern.get(pattern.size() - 1);
532                 int dRow = cell.row - lastCell.row;
533                 int dColumn = cell.column - lastCell.column;
534 
535                 int fillInRow = lastCell.row;
536                 int fillInColumn = lastCell.column;
537 
538                 if (Math.abs(dRow) == 2 && Math.abs(dColumn) != 1) {
539                     fillInRow = lastCell.row + ((dRow > 0) ? 1 : -1);
540                 }
541 
542                 if (Math.abs(dColumn) == 2 && Math.abs(dRow) != 1) {
543                     fillInColumn = lastCell.column + ((dColumn > 0) ? 1 : -1);
544                 }
545 
546                 fillInGapCell = Cell.of(fillInRow, fillInColumn);
547             }
548 
549             if (fillInGapCell != null &&
550                     !mPatternDrawLookup[fillInGapCell.row][fillInGapCell.column]) {
551                 addCellToPattern(fillInGapCell);
552             }
553             addCellToPattern(cell);
554             if (mEnableHapticFeedback) {
555                 performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY,
556                         HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING
557                         | HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING);
558             }
559             return cell;
560         }
561         return null;
562     }
563 
addCellToPattern(Cell newCell)564     private void addCellToPattern(Cell newCell) {
565         mPatternDrawLookup[newCell.getRow()][newCell.getColumn()] = true;
566         mPattern.add(newCell);
567         if (!mInStealthMode) {
568             startCellActivatedAnimation(newCell);
569         }
570         notifyCellAdded();
571     }
572 
startCellActivatedAnimation(Cell cell)573     private void startCellActivatedAnimation(Cell cell) {
574         final CellState cellState = mCellStates[cell.row][cell.column];
575         startSizeAnimation(mDotSize, mDotSizeActivated, 96, mLinearOutSlowInInterpolator,
576                 cellState, new Runnable() {
577             @Override
578             public void run() {
579                 startSizeAnimation(mDotSizeActivated, mDotSize, 192, mFastOutSlowInInterpolator,
580                         cellState, null);
581             }
582         });
583         startLineEndAnimation(cellState, mInProgressX, mInProgressY,
584                 getCenterXForColumn(cell.column), getCenterYForRow(cell.row));
585     }
586 
startLineEndAnimation(final CellState state, final float startX, final float startY, final float targetX, final float targetY)587     private void startLineEndAnimation(final CellState state,
588             final float startX, final float startY, final float targetX, final float targetY) {
589         ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1);
590         valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
591             @Override
592             public void onAnimationUpdate(ValueAnimator animation) {
593                 float t = (float) animation.getAnimatedValue();
594                 state.lineEndX = (1 - t) * startX + t * targetX;
595                 state.lineEndY = (1 - t) * startY + t * targetY;
596                 invalidate();
597             }
598         });
599         valueAnimator.addListener(new AnimatorListenerAdapter() {
600             @Override
601             public void onAnimationEnd(Animator animation) {
602                 state.lineAnimator = null;
603             }
604         });
605         valueAnimator.setInterpolator(mFastOutSlowInInterpolator);
606         valueAnimator.setDuration(100);
607         valueAnimator.start();
608         state.lineAnimator = valueAnimator;
609     }
610 
startSizeAnimation(float start, float end, long duration, Interpolator interpolator, final CellState state, final Runnable endRunnable)611     private void startSizeAnimation(float start, float end, long duration, Interpolator interpolator,
612             final CellState state, final Runnable endRunnable) {
613         ValueAnimator valueAnimator = ValueAnimator.ofFloat(start, end);
614         valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
615             @Override
616             public void onAnimationUpdate(ValueAnimator animation) {
617                 state.size = (float) animation.getAnimatedValue();
618                 invalidate();
619             }
620         });
621         if (endRunnable != null) {
622             valueAnimator.addListener(new AnimatorListenerAdapter() {
623                 @Override
624                 public void onAnimationEnd(Animator animation) {
625                     endRunnable.run();
626                 }
627             });
628         }
629         valueAnimator.setInterpolator(interpolator);
630         valueAnimator.setDuration(duration);
631         valueAnimator.start();
632     }
633 
634     // helper method to find which cell a point maps to
checkForNewHit(float x, float y)635     private Cell checkForNewHit(float x, float y) {
636 
637         final int rowHit = getRowHit(y);
638         if (rowHit < 0) {
639             return null;
640         }
641         final int columnHit = getColumnHit(x);
642         if (columnHit < 0) {
643             return null;
644         }
645 
646         if (mPatternDrawLookup[rowHit][columnHit]) {
647             return null;
648         }
649         return Cell.of(rowHit, columnHit);
650     }
651 
652     /**
653      * Helper method to find the row that y falls into.
654      * @param y The y coordinate
655      * @return The row that y falls in, or -1 if it falls in no row.
656      */
getRowHit(float y)657     private int getRowHit(float y) {
658 
659         final float squareHeight = mSquareHeight;
660         float hitSize = squareHeight * mHitFactor;
661 
662         float offset = mPaddingTop + (squareHeight - hitSize) / 2f;
663         for (int i = 0; i < 3; i++) {
664 
665             final float hitTop = offset + squareHeight * i;
666             if (y >= hitTop && y <= hitTop + hitSize) {
667                 return i;
668             }
669         }
670         return -1;
671     }
672 
673     /**
674      * Helper method to find the column x fallis into.
675      * @param x The x coordinate.
676      * @return The column that x falls in, or -1 if it falls in no column.
677      */
getColumnHit(float x)678     private int getColumnHit(float x) {
679         final float squareWidth = mSquareWidth;
680         float hitSize = squareWidth * mHitFactor;
681 
682         float offset = mPaddingLeft + (squareWidth - hitSize) / 2f;
683         for (int i = 0; i < 3; i++) {
684 
685             final float hitLeft = offset + squareWidth * i;
686             if (x >= hitLeft && x <= hitLeft + hitSize) {
687                 return i;
688             }
689         }
690         return -1;
691     }
692 
693     @Override
onHoverEvent(MotionEvent event)694     public boolean onHoverEvent(MotionEvent event) {
695         if (AccessibilityManager.getInstance(mContext).isTouchExplorationEnabled()) {
696             final int action = event.getAction();
697             switch (action) {
698                 case MotionEvent.ACTION_HOVER_ENTER:
699                     event.setAction(MotionEvent.ACTION_DOWN);
700                     break;
701                 case MotionEvent.ACTION_HOVER_MOVE:
702                     event.setAction(MotionEvent.ACTION_MOVE);
703                     break;
704                 case MotionEvent.ACTION_HOVER_EXIT:
705                     event.setAction(MotionEvent.ACTION_UP);
706                     break;
707             }
708             onTouchEvent(event);
709             event.setAction(action);
710         }
711         return super.onHoverEvent(event);
712     }
713 
714     @Override
onTouchEvent(MotionEvent event)715     public boolean onTouchEvent(MotionEvent event) {
716         if (!mInputEnabled || !isEnabled()) {
717             return false;
718         }
719 
720         switch(event.getAction()) {
721             case MotionEvent.ACTION_DOWN:
722                 handleActionDown(event);
723                 return true;
724             case MotionEvent.ACTION_UP:
725                 handleActionUp(event);
726                 return true;
727             case MotionEvent.ACTION_MOVE:
728                 handleActionMove(event);
729                 return true;
730             case MotionEvent.ACTION_CANCEL:
731                 if (mPatternInProgress) {
732                     mPatternInProgress = false;
733                     resetPattern();
734                     notifyPatternCleared();
735                 }
736                 if (PROFILE_DRAWING) {
737                     if (mDrawingProfilingStarted) {
738                         Debug.stopMethodTracing();
739                         mDrawingProfilingStarted = false;
740                     }
741                 }
742                 return true;
743         }
744         return false;
745     }
746 
handleActionMove(MotionEvent event)747     private void handleActionMove(MotionEvent event) {
748         // Handle all recent motion events so we don't skip any cells even when the device
749         // is busy...
750         final float radius = mPathWidth;
751         final int historySize = event.getHistorySize();
752         mTmpInvalidateRect.setEmpty();
753         boolean invalidateNow = false;
754         for (int i = 0; i < historySize + 1; i++) {
755             final float x = i < historySize ? event.getHistoricalX(i) : event.getX();
756             final float y = i < historySize ? event.getHistoricalY(i) : event.getY();
757             Cell hitCell = detectAndAddHit(x, y);
758             final int patternSize = mPattern.size();
759             if (hitCell != null && patternSize == 1) {
760                 mPatternInProgress = true;
761                 notifyPatternStarted();
762             }
763             // note current x and y for rubber banding of in progress patterns
764             final float dx = Math.abs(x - mInProgressX);
765             final float dy = Math.abs(y - mInProgressY);
766             if (dx > DRAG_THRESHHOLD || dy > DRAG_THRESHHOLD) {
767                 invalidateNow = true;
768             }
769 
770             if (mPatternInProgress && patternSize > 0) {
771                 final ArrayList<Cell> pattern = mPattern;
772                 final Cell lastCell = pattern.get(patternSize - 1);
773                 float lastCellCenterX = getCenterXForColumn(lastCell.column);
774                 float lastCellCenterY = getCenterYForRow(lastCell.row);
775 
776                 // Adjust for drawn segment from last cell to (x,y). Radius accounts for line width.
777                 float left = Math.min(lastCellCenterX, x) - radius;
778                 float right = Math.max(lastCellCenterX, x) + radius;
779                 float top = Math.min(lastCellCenterY, y) - radius;
780                 float bottom = Math.max(lastCellCenterY, y) + radius;
781 
782                 // Invalidate between the pattern's new cell and the pattern's previous cell
783                 if (hitCell != null) {
784                     final float width = mSquareWidth * 0.5f;
785                     final float height = mSquareHeight * 0.5f;
786                     final float hitCellCenterX = getCenterXForColumn(hitCell.column);
787                     final float hitCellCenterY = getCenterYForRow(hitCell.row);
788 
789                     left = Math.min(hitCellCenterX - width, left);
790                     right = Math.max(hitCellCenterX + width, right);
791                     top = Math.min(hitCellCenterY - height, top);
792                     bottom = Math.max(hitCellCenterY + height, bottom);
793                 }
794 
795                 // Invalidate between the pattern's last cell and the previous location
796                 mTmpInvalidateRect.union(Math.round(left), Math.round(top),
797                         Math.round(right), Math.round(bottom));
798             }
799         }
800         mInProgressX = event.getX();
801         mInProgressY = event.getY();
802 
803         // To save updates, we only invalidate if the user moved beyond a certain amount.
804         if (invalidateNow) {
805             mInvalidate.union(mTmpInvalidateRect);
806             invalidate(mInvalidate);
807             mInvalidate.set(mTmpInvalidateRect);
808         }
809     }
810 
sendAccessEvent(int resId)811     private void sendAccessEvent(int resId) {
812         announceForAccessibility(mContext.getString(resId));
813     }
814 
handleActionUp(MotionEvent event)815     private void handleActionUp(MotionEvent event) {
816         // report pattern detected
817         if (!mPattern.isEmpty()) {
818             mPatternInProgress = false;
819             cancelLineAnimations();
820             notifyPatternDetected();
821             invalidate();
822         }
823         if (PROFILE_DRAWING) {
824             if (mDrawingProfilingStarted) {
825                 Debug.stopMethodTracing();
826                 mDrawingProfilingStarted = false;
827             }
828         }
829     }
830 
cancelLineAnimations()831     private void cancelLineAnimations() {
832         for (int i = 0; i < 3; i++) {
833             for (int j = 0; j < 3; j++) {
834                 CellState state = mCellStates[i][j];
835                 if (state.lineAnimator != null) {
836                     state.lineAnimator.cancel();
837                     state.lineEndX = Float.MIN_VALUE;
838                     state.lineEndY = Float.MIN_VALUE;
839                 }
840             }
841         }
842     }
handleActionDown(MotionEvent event)843     private void handleActionDown(MotionEvent event) {
844         resetPattern();
845         final float x = event.getX();
846         final float y = event.getY();
847         final Cell hitCell = detectAndAddHit(x, y);
848         if (hitCell != null) {
849             mPatternInProgress = true;
850             mPatternDisplayMode = DisplayMode.Correct;
851             notifyPatternStarted();
852         } else if (mPatternInProgress) {
853             mPatternInProgress = false;
854             notifyPatternCleared();
855         }
856         if (hitCell != null) {
857             final float startX = getCenterXForColumn(hitCell.column);
858             final float startY = getCenterYForRow(hitCell.row);
859 
860             final float widthOffset = mSquareWidth / 2f;
861             final float heightOffset = mSquareHeight / 2f;
862 
863             invalidate((int) (startX - widthOffset), (int) (startY - heightOffset),
864                     (int) (startX + widthOffset), (int) (startY + heightOffset));
865         }
866         mInProgressX = x;
867         mInProgressY = y;
868         if (PROFILE_DRAWING) {
869             if (!mDrawingProfilingStarted) {
870                 Debug.startMethodTracing("LockPatternDrawing");
871                 mDrawingProfilingStarted = true;
872             }
873         }
874     }
875 
getCenterXForColumn(int column)876     private float getCenterXForColumn(int column) {
877         return mPaddingLeft + column * mSquareWidth + mSquareWidth / 2f;
878     }
879 
getCenterYForRow(int row)880     private float getCenterYForRow(int row) {
881         return mPaddingTop + row * mSquareHeight + mSquareHeight / 2f;
882     }
883 
884     @Override
onDraw(Canvas canvas)885     protected void onDraw(Canvas canvas) {
886         final ArrayList<Cell> pattern = mPattern;
887         final int count = pattern.size();
888         final boolean[][] drawLookup = mPatternDrawLookup;
889 
890         if (mPatternDisplayMode == DisplayMode.Animate) {
891 
892             // figure out which circles to draw
893 
894             // + 1 so we pause on complete pattern
895             final int oneCycle = (count + 1) * MILLIS_PER_CIRCLE_ANIMATING;
896             final int spotInCycle = (int) (SystemClock.elapsedRealtime() -
897                     mAnimatingPeriodStart) % oneCycle;
898             final int numCircles = spotInCycle / MILLIS_PER_CIRCLE_ANIMATING;
899 
900             clearPatternDrawLookup();
901             for (int i = 0; i < numCircles; i++) {
902                 final Cell cell = pattern.get(i);
903                 drawLookup[cell.getRow()][cell.getColumn()] = true;
904             }
905 
906             // figure out in progress portion of ghosting line
907 
908             final boolean needToUpdateInProgressPoint = numCircles > 0
909                     && numCircles < count;
910 
911             if (needToUpdateInProgressPoint) {
912                 final float percentageOfNextCircle =
913                         ((float) (spotInCycle % MILLIS_PER_CIRCLE_ANIMATING)) /
914                                 MILLIS_PER_CIRCLE_ANIMATING;
915 
916                 final Cell currentCell = pattern.get(numCircles - 1);
917                 final float centerX = getCenterXForColumn(currentCell.column);
918                 final float centerY = getCenterYForRow(currentCell.row);
919 
920                 final Cell nextCell = pattern.get(numCircles);
921                 final float dx = percentageOfNextCircle *
922                         (getCenterXForColumn(nextCell.column) - centerX);
923                 final float dy = percentageOfNextCircle *
924                         (getCenterYForRow(nextCell.row) - centerY);
925                 mInProgressX = centerX + dx;
926                 mInProgressY = centerY + dy;
927             }
928             // TODO: Infinite loop here...
929             invalidate();
930         }
931 
932         final Path currentPath = mCurrentPath;
933         currentPath.rewind();
934 
935         // draw the circles
936         for (int i = 0; i < 3; i++) {
937             float centerY = getCenterYForRow(i);
938             for (int j = 0; j < 3; j++) {
939                 CellState cellState = mCellStates[i][j];
940                 float centerX = getCenterXForColumn(j);
941                 float size = cellState.size * cellState.scale;
942                 float translationY = cellState.translateY;
943                 drawCircle(canvas, (int) centerX, (int) centerY + translationY,
944                         size, drawLookup[i][j], cellState.alpha);
945             }
946         }
947 
948         // TODO: the path should be created and cached every time we hit-detect a cell
949         // only the last segment of the path should be computed here
950         // draw the path of the pattern (unless we are in stealth mode)
951         final boolean drawPath = !mInStealthMode;
952 
953         if (drawPath) {
954             mPathPaint.setColor(getCurrentColor(true /* partOfPattern */));
955 
956             boolean anyCircles = false;
957             float lastX = 0f;
958             float lastY = 0f;
959             for (int i = 0; i < count; i++) {
960                 Cell cell = pattern.get(i);
961 
962                 // only draw the part of the pattern stored in
963                 // the lookup table (this is only different in the case
964                 // of animation).
965                 if (!drawLookup[cell.row][cell.column]) {
966                     break;
967                 }
968                 anyCircles = true;
969 
970                 float centerX = getCenterXForColumn(cell.column);
971                 float centerY = getCenterYForRow(cell.row);
972                 if (i != 0) {
973                     CellState state = mCellStates[cell.row][cell.column];
974                     currentPath.rewind();
975                     currentPath.moveTo(lastX, lastY);
976                     if (state.lineEndX != Float.MIN_VALUE && state.lineEndY != Float.MIN_VALUE) {
977                         currentPath.lineTo(state.lineEndX, state.lineEndY);
978                     } else {
979                         currentPath.lineTo(centerX, centerY);
980                     }
981                     canvas.drawPath(currentPath, mPathPaint);
982                 }
983                 lastX = centerX;
984                 lastY = centerY;
985             }
986 
987             // draw last in progress section
988             if ((mPatternInProgress || mPatternDisplayMode == DisplayMode.Animate)
989                     && anyCircles) {
990                 currentPath.rewind();
991                 currentPath.moveTo(lastX, lastY);
992                 currentPath.lineTo(mInProgressX, mInProgressY);
993 
994                 mPathPaint.setAlpha((int) (calculateLastSegmentAlpha(
995                         mInProgressX, mInProgressY, lastX, lastY) * 255f));
996                 canvas.drawPath(currentPath, mPathPaint);
997             }
998         }
999     }
1000 
1001     private float calculateLastSegmentAlpha(float x, float y, float lastX, float lastY) {
1002         float diffX = x - lastX;
1003         float diffY = y - lastY;
1004         float dist = (float) Math.sqrt(diffX*diffX + diffY*diffY);
1005         float frac = dist/mSquareWidth;
1006         return Math.min(1f, Math.max(0f, (frac - 0.3f) * 4f));
1007     }
1008 
1009     private int getCurrentColor(boolean partOfPattern) {
1010         if (!partOfPattern || mInStealthMode || mPatternInProgress) {
1011             // unselected circle
1012             return mRegularColor;
1013         } else if (mPatternDisplayMode == DisplayMode.Wrong) {
1014             // the pattern is wrong
1015             return mErrorColor;
1016         } else if (mPatternDisplayMode == DisplayMode.Correct ||
1017                 mPatternDisplayMode == DisplayMode.Animate) {
1018             return mSuccessColor;
1019         } else {
1020             throw new IllegalStateException("unknown display mode " + mPatternDisplayMode);
1021         }
1022     }
1023 
1024     /**
1025      * @param partOfPattern Whether this circle is part of the pattern.
1026      */
1027     private void drawCircle(Canvas canvas, float centerX, float centerY, float size,
1028             boolean partOfPattern, float alpha) {
1029         mPaint.setColor(getCurrentColor(partOfPattern));
1030         mPaint.setAlpha((int) (alpha * 255));
1031         canvas.drawCircle(centerX, centerY, size/2, mPaint);
1032     }
1033 
1034     @Override
1035     protected Parcelable onSaveInstanceState() {
1036         Parcelable superState = super.onSaveInstanceState();
1037         return new SavedState(superState,
1038                 LockPatternUtils.patternToString(mPattern),
1039                 mPatternDisplayMode.ordinal(),
1040                 mInputEnabled, mInStealthMode, mEnableHapticFeedback);
1041     }
1042 
1043     @Override
1044     protected void onRestoreInstanceState(Parcelable state) {
1045         final SavedState ss = (SavedState) state;
1046         super.onRestoreInstanceState(ss.getSuperState());
1047         setPattern(
1048                 DisplayMode.Correct,
1049                 LockPatternUtils.stringToPattern(ss.getSerializedPattern()));
1050         mPatternDisplayMode = DisplayMode.values()[ss.getDisplayMode()];
1051         mInputEnabled = ss.isInputEnabled();
1052         mInStealthMode = ss.isInStealthMode();
1053         mEnableHapticFeedback = ss.isTactileFeedbackEnabled();
1054     }
1055 
1056     /**
1057      * The parecelable for saving and restoring a lock pattern view.
1058      */
1059     private static class SavedState extends BaseSavedState {
1060 
1061         private final String mSerializedPattern;
1062         private final int mDisplayMode;
1063         private final boolean mInputEnabled;
1064         private final boolean mInStealthMode;
1065         private final boolean mTactileFeedbackEnabled;
1066 
1067         /**
1068          * Constructor called from {@link LockPatternView#onSaveInstanceState()}
1069          */
1070         private SavedState(Parcelable superState, String serializedPattern, int displayMode,
1071                 boolean inputEnabled, boolean inStealthMode, boolean tactileFeedbackEnabled) {
1072             super(superState);
1073             mSerializedPattern = serializedPattern;
1074             mDisplayMode = displayMode;
1075             mInputEnabled = inputEnabled;
1076             mInStealthMode = inStealthMode;
1077             mTactileFeedbackEnabled = tactileFeedbackEnabled;
1078         }
1079 
1080         /**
1081          * Constructor called from {@link #CREATOR}
1082          */
1083         private SavedState(Parcel in) {
1084             super(in);
1085             mSerializedPattern = in.readString();
1086             mDisplayMode = in.readInt();
1087             mInputEnabled = (Boolean) in.readValue(null);
1088             mInStealthMode = (Boolean) in.readValue(null);
1089             mTactileFeedbackEnabled = (Boolean) in.readValue(null);
1090         }
1091 
1092         public String getSerializedPattern() {
1093             return mSerializedPattern;
1094         }
1095 
1096         public int getDisplayMode() {
1097             return mDisplayMode;
1098         }
1099 
1100         public boolean isInputEnabled() {
1101             return mInputEnabled;
1102         }
1103 
1104         public boolean isInStealthMode() {
1105             return mInStealthMode;
1106         }
1107 
1108         public boolean isTactileFeedbackEnabled(){
1109             return mTactileFeedbackEnabled;
1110         }
1111 
1112         @Override
1113         public void writeToParcel(Parcel dest, int flags) {
1114             super.writeToParcel(dest, flags);
1115             dest.writeString(mSerializedPattern);
1116             dest.writeInt(mDisplayMode);
1117             dest.writeValue(mInputEnabled);
1118             dest.writeValue(mInStealthMode);
1119             dest.writeValue(mTactileFeedbackEnabled);
1120         }
1121 
1122         public static final Parcelable.Creator<SavedState> CREATOR =
1123                 new Creator<SavedState>() {
1124                     public SavedState createFromParcel(Parcel in) {
1125                         return new SavedState(in);
1126                     }
1127 
1128                     public SavedState[] newArray(int size) {
1129                         return new SavedState[size];
1130                     }
1131                 };
1132     }
1133 }
1134