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