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