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