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