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