LockPatternView.java revision c15819748a78963f0dbfbb06300419daf3d2f5f6
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
17package com.android.internal.widget;
18
19
20import android.animation.Animator;
21import android.animation.AnimatorListenerAdapter;
22import android.animation.ValueAnimator;
23import android.content.Context;
24import android.content.res.TypedArray;
25import android.graphics.Canvas;
26import android.graphics.Paint;
27import android.graphics.Path;
28import android.graphics.Rect;
29import android.os.Debug;
30import android.os.Parcel;
31import android.os.Parcelable;
32import android.os.SystemClock;
33import android.util.AttributeSet;
34import android.view.HapticFeedbackConstants;
35import android.view.MotionEvent;
36import android.view.View;
37import android.view.accessibility.AccessibilityManager;
38import android.view.animation.AnimationUtils;
39import android.view.animation.Interpolator;
40
41import com.android.internal.R;
42
43import java.util.ArrayList;
44import 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 */
53public 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         */
149        private Cell(int row, int column) {
150            checkRange(row, column);
151            this.row = row;
152            this.column = column;
153        }
154
155        public int getRow() {
156            return row;
157        }
158
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         */
167        public static synchronized Cell of(int row, int column) {
168            checkRange(row, column);
169            return sCells[row][column];
170        }
171
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
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         */
225        void onPatternStart();
226
227        /**
228         * The pattern was cleared.
229         */
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         */
236        void onPatternCellAdded(List<Cell> pattern);
237
238        /**
239         * A pattern was detected from the user.
240         * @param pattern The pattern.
241         */
242        void onPatternDetected(List<Cell> pattern);
243    }
244
245    public LockPatternView(Context context) {
246        this(context, null);
247    }
248
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
310    public CellState[][] getCellStates() {
311        return mCellStates;
312    }
313
314    /**
315     * @return Whether the view is in stealth mode.
316     */
317    public boolean isInStealthMode() {
318        return mInStealthMode;
319    }
320
321    /**
322     * @return Whether the view has tactile feedback enabled.
323     */
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     */
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     */
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     */
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     */
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     */
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
396    private void notifyCellAdded() {
397        sendAccessEvent(R.string.lockscreen_access_pattern_cell_added);
398        if (mOnPatternListener != null) {
399            mOnPatternListener.onPatternCellAdded(mPattern);
400        }
401    }
402
403    private void notifyPatternStarted() {
404        sendAccessEvent(R.string.lockscreen_access_pattern_start);
405        if (mOnPatternListener != null) {
406            mOnPatternListener.onPatternStart();
407        }
408    }
409
410    private void notifyPatternDetected() {
411        sendAccessEvent(R.string.lockscreen_access_pattern_detected);
412        if (mOnPatternListener != null) {
413            mOnPatternListener.onPatternDetected(mPattern);
414        }
415    }
416
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     */
427    public void clearPattern() {
428        resetPattern();
429    }
430
431    /**
432     * Reset all pattern state.
433     */
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     */
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     */
456    public void disableInput() {
457        mInputEnabled = false;
458    }
459
460    /**
461     * Enable input.
462     */
463    public void enableInput() {
464        mInputEnabled = true;
465    }
466
467    @Override
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
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
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     */
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
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
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
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
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
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     */
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     */
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
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
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
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
811    private void sendAccessEvent(int resId) {
812        announceForAccessibility(mContext.getString(resId));
813    }
814
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
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    }
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
876    private float getCenterXForColumn(int column) {
877        return mPaddingLeft + column * mSquareWidth + mSquareWidth / 2f;
878    }
879
880    private float getCenterYForRow(int row) {
881        return mPaddingTop + row * mSquareHeight + mSquareHeight / 2f;
882    }
883
884    @Override
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