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