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