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 android.animation.Animator;
     21 import android.animation.AnimatorListenerAdapter;
     22 import android.animation.ValueAnimator;
     23 import android.content.Context;
     24 import android.content.res.TypedArray;
     25 import android.graphics.Canvas;
     26 import android.graphics.Paint;
     27 import android.graphics.Path;
     28 import android.graphics.Rect;
     29 import android.os.Debug;
     30 import android.os.Parcel;
     31 import android.os.Parcelable;
     32 import android.os.SystemClock;
     33 import android.util.AttributeSet;
     34 import android.view.HapticFeedbackConstants;
     35 import android.view.MotionEvent;
     36 import android.view.View;
     37 import android.view.accessibility.AccessibilityManager;
     38 import android.view.animation.AnimationUtils;
     39 import android.view.animation.Interpolator;
     40 
     41 import com.android.internal.R;
     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     private static final boolean PROFILE_DRAWING = false;
     60     private final CellState[][] mCellStates;
     61 
     62     private final int mDotSize;
     63     private final int mDotSizeActivated;
     64     private final int mPathWidth;
     65 
     66     private boolean mDrawingProfilingStarted = false;
     67 
     68     private Paint mPaint = new Paint();
     69     private Paint mPathPaint = new Paint();
     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     /**
     79      * This can be used to avoid updating the display for very small motions or noisy panels.
     80      * It didn't seem to have much impact on the devices tested, so currently set to 0.
     81      */
     82     private static final float DRAG_THRESHHOLD = 0.0f;
     83 
     84     private OnPatternListener mOnPatternListener;
     85     private ArrayList<Cell> mPattern = new ArrayList<Cell>(9);
     86 
     87     /**
     88      * Lookup table for the circles of the pattern we are currently drawing.
     89      * This will be the cells of the complete pattern unless we are animating,
     90      * in which case we use this to hold the cells we are drawing for the in
     91      * progress animation.
     92      */
     93     private boolean[][] mPatternDrawLookup = new boolean[3][3];
     94 
     95     /**
     96      * the in progress point:
     97      * - during interaction: where the user's finger is
     98      * - during animation: the current tip of the animating line
     99      */
    100     private float mInProgressX = -1;
    101     private float mInProgressY = -1;
    102 
    103     private long mAnimatingPeriodStart;
    104 
    105     private DisplayMode mPatternDisplayMode = DisplayMode.Correct;
    106     private boolean mInputEnabled = true;
    107     private boolean mInStealthMode = false;
    108     private boolean mEnableHapticFeedback = true;
    109     private boolean mPatternInProgress = false;
    110 
    111     private float mHitFactor = 0.6f;
    112 
    113     private float mSquareWidth;
    114     private float mSquareHeight;
    115 
    116     private final Path mCurrentPath = new Path();
    117     private final Rect mInvalidate = new Rect();
    118     private final Rect mTmpInvalidateRect = new Rect();
    119 
    120     private int mAspect;
    121     private int mRegularColor;
    122     private int mErrorColor;
    123     private int mSuccessColor;
    124 
    125     private Interpolator mFastOutSlowInInterpolator;
    126     private Interpolator mLinearOutSlowInInterpolator;
    127 
    128     /**
    129      * Represents a cell in the 3 X 3 matrix of the unlock pattern view.
    130      */
    131     public static class Cell {
    132         int row;
    133         int column;
    134 
    135         // keep # objects limited to 9
    136         static Cell[][] sCells = new Cell[3][3];
    137         static {
    138             for (int i = 0; i < 3; i++) {
    139                 for (int j = 0; j < 3; j++) {
    140                     sCells[i][j] = new Cell(i, j);
    141                 }
    142             }
    143         }
    144 
    145         /**
    146          * @param row The row of the cell.
    147          * @param column The column of the cell.
    148          */
    149         private Cell(int row, int column) {
    150             checkRange(row, column);
    151             this.row = row;
    152             this.column = column;
    153         }
    154 
    155         public int getRow() {
    156             return row;
    157         }
    158 
    159         public int getColumn() {
    160             return column;
    161         }
    162 
    163         /**
    164          * @param row The row of the cell.
    165          * @param column The column of the cell.
    166          */
    167         public static synchronized Cell of(int row, int column) {
    168             checkRange(row, column);
    169             return sCells[row][column];
    170         }
    171 
    172         private static void checkRange(int row, int column) {
    173             if (row < 0 || row > 2) {
    174                 throw new IllegalArgumentException("row must be in range 0-2");
    175             }
    176             if (column < 0 || column > 2) {
    177                 throw new IllegalArgumentException("column must be in range 0-2");
    178             }
    179         }
    180 
    181         public String toString() {
    182             return "(row=" + row + ",clmn=" + column + ")";
    183         }
    184     }
    185 
    186     public static class CellState {
    187         public float scale = 1.0f;
    188         public float translateY = 0.0f;
    189         public float alpha = 1.0f;
    190         public float size;
    191         public float lineEndX = Float.MIN_VALUE;
    192         public float lineEndY = Float.MIN_VALUE;
    193         public ValueAnimator lineAnimator;
    194      }
    195 
    196     /**
    197      * How to display the current pattern.
    198      */
    199     public enum DisplayMode {
    200 
    201         /**
    202          * The pattern drawn is correct (i.e draw it in a friendly color)
    203          */
    204         Correct,
    205 
    206         /**
    207          * Animate the pattern (for demo, and help).
    208          */
    209         Animate,
    210 
    211         /**
    212          * The pattern is wrong (i.e draw a foreboding color)
    213          */
    214         Wrong
    215     }
    216 
    217     /**
    218      * The call back interface for detecting patterns entered by the user.
    219      */
    220     public static interface OnPatternListener {
    221 
    222         /**
    223          * A new pattern has begun.
    224          */
    225         void onPatternStart();
    226 
    227         /**
    228          * The pattern was cleared.
    229          */
    230         void onPatternCleared();
    231 
    232         /**
    233          * The user extended the pattern currently being drawn by one cell.
    234          * @param pattern The pattern with newly added cell.
    235          */
    236         void onPatternCellAdded(List<Cell> pattern);
    237 
    238         /**
    239          * A pattern was detected from the user.
    240          * @param pattern The pattern.
    241          */
    242         void onPatternDetected(List<Cell> pattern);
    243     }
    244 
    245     public LockPatternView(Context context) {
    246         this(context, null);
    247     }
    248 
    249     public LockPatternView(Context context, AttributeSet attrs) {
    250         super(context, attrs);
    251 
    252         TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LockPatternView);
    253 
    254         final String aspect = a.getString(R.styleable.LockPatternView_aspect);
    255 
    256         if ("square".equals(aspect)) {
    257             mAspect = ASPECT_SQUARE;
    258         } else if ("lock_width".equals(aspect)) {
    259             mAspect = ASPECT_LOCK_WIDTH;
    260         } else if ("lock_height".equals(aspect)) {
    261             mAspect = ASPECT_LOCK_HEIGHT;
    262         } else {
    263             mAspect = ASPECT_SQUARE;
    264         }
    265 
    266         setClickable(true);
    267 
    268 
    269         mPathPaint.setAntiAlias(true);
    270         mPathPaint.setDither(true);
    271 
    272         mRegularColor = getResources().getColor(R.color.lock_pattern_view_regular_color);
    273         mErrorColor = getResources().getColor(R.color.lock_pattern_view_error_color);
    274         mSuccessColor = getResources().getColor(R.color.lock_pattern_view_success_color);
    275         mRegularColor = a.getColor(R.styleable.LockPatternView_regularColor, mRegularColor);
    276         mErrorColor = a.getColor(R.styleable.LockPatternView_errorColor, mErrorColor);
    277         mSuccessColor = a.getColor(R.styleable.LockPatternView_successColor, mSuccessColor);
    278 
    279         int pathColor = a.getColor(R.styleable.LockPatternView_pathColor, mRegularColor);
    280         mPathPaint.setColor(pathColor);
    281 
    282         mPathPaint.setStyle(Paint.Style.STROKE);
    283         mPathPaint.setStrokeJoin(Paint.Join.ROUND);
    284         mPathPaint.setStrokeCap(Paint.Cap.ROUND);
    285 
    286         mPathWidth = getResources().getDimensionPixelSize(R.dimen.lock_pattern_dot_line_width);
    287         mPathPaint.setStrokeWidth(mPathWidth);
    288 
    289         mDotSize = getResources().getDimensionPixelSize(R.dimen.lock_pattern_dot_size);
    290         mDotSizeActivated = getResources().getDimensionPixelSize(
    291                 R.dimen.lock_pattern_dot_size_activated);
    292 
    293         mPaint.setAntiAlias(true);
    294         mPaint.setDither(true);
    295 
    296         mCellStates = new CellState[3][3];
    297         for (int i = 0; i < 3; i++) {
    298             for (int j = 0; j < 3; j++) {
    299                 mCellStates[i][j] = new CellState();
    300                 mCellStates[i][j].size = mDotSize;
    301             }
    302         }
    303 
    304         mFastOutSlowInInterpolator =
    305                 AnimationUtils.loadInterpolator(context, android.R.interpolator.fast_out_slow_in);
    306         mLinearOutSlowInInterpolator =
    307                 AnimationUtils.loadInterpolator(context, android.R.interpolator.linear_out_slow_in);
    308     }
    309 
    310     public CellState[][] getCellStates() {
    311         return mCellStates;
    312     }
    313 
    314     /**
    315      * @return Whether the view is in stealth mode.
    316      */
    317     public boolean isInStealthMode() {
    318         return mInStealthMode;
    319     }
    320 
    321     /**
    322      * @return Whether the view has tactile feedback enabled.
    323      */
    324     public boolean isTactileFeedbackEnabled() {
    325         return mEnableHapticFeedback;
    326     }
    327 
    328     /**
    329      * Set whether the view is in stealth mode.  If true, there will be no
    330      * visible feedback as the user enters the pattern.
    331      *
    332      * @param inStealthMode Whether in stealth mode.
    333      */
    334     public void setInStealthMode(boolean inStealthMode) {
    335         mInStealthMode = inStealthMode;
    336     }
    337 
    338     /**
    339      * Set whether the view will use tactile feedback.  If true, there will be
    340      * tactile feedback as the user enters the pattern.
    341      *
    342      * @param tactileFeedbackEnabled Whether tactile feedback is enabled
    343      */
    344     public void setTactileFeedbackEnabled(boolean tactileFeedbackEnabled) {
    345         mEnableHapticFeedback = tactileFeedbackEnabled;
    346     }
    347 
    348     /**
    349      * Set the call back for pattern detection.
    350      * @param onPatternListener The call back.
    351      */
    352     public void setOnPatternListener(
    353             OnPatternListener onPatternListener) {
    354         mOnPatternListener = onPatternListener;
    355     }
    356 
    357     /**
    358      * Set the pattern explicitely (rather than waiting for the user to input
    359      * a pattern).
    360      * @param displayMode How to display the pattern.
    361      * @param pattern The pattern.
    362      */
    363     public void setPattern(DisplayMode displayMode, List<Cell> pattern) {
    364         mPattern.clear();
    365         mPattern.addAll(pattern);
    366         clearPatternDrawLookup();
    367         for (Cell cell : pattern) {
    368             mPatternDrawLookup[cell.getRow()][cell.getColumn()] = true;
    369         }
    370 
    371         setDisplayMode(displayMode);
    372     }
    373 
    374     /**
    375      * Set the display mode of the current pattern.  This can be useful, for
    376      * instance, after detecting a pattern to tell this view whether change the
    377      * in progress result to correct or wrong.
    378      * @param displayMode The display mode.
    379      */
    380     public void setDisplayMode(DisplayMode displayMode) {
    381         mPatternDisplayMode = displayMode;
    382         if (displayMode == DisplayMode.Animate) {
    383             if (mPattern.size() == 0) {
    384                 throw new IllegalStateException("you must have a pattern to "
    385                         + "animate if you want to set the display mode to animate");
    386             }
    387             mAnimatingPeriodStart = SystemClock.elapsedRealtime();
    388             final Cell first = mPattern.get(0);
    389             mInProgressX = getCenterXForColumn(first.getColumn());
    390             mInProgressY = getCenterYForRow(first.getRow());
    391             clearPatternDrawLookup();
    392         }
    393         invalidate();
    394     }
    395 
    396     private void notifyCellAdded() {
    397         sendAccessEvent(R.string.lockscreen_access_pattern_cell_added);
    398         if (mOnPatternListener != null) {
    399             mOnPatternListener.onPatternCellAdded(mPattern);
    400         }
    401     }
    402 
    403     private void notifyPatternStarted() {
    404         sendAccessEvent(R.string.lockscreen_access_pattern_start);
    405         if (mOnPatternListener != null) {
    406             mOnPatternListener.onPatternStart();
    407         }
    408     }
    409 
    410     private void notifyPatternDetected() {
    411         sendAccessEvent(R.string.lockscreen_access_pattern_detected);
    412         if (mOnPatternListener != null) {
    413             mOnPatternListener.onPatternDetected(mPattern);
    414         }
    415     }
    416 
    417     private void notifyPatternCleared() {
    418         sendAccessEvent(R.string.lockscreen_access_pattern_cleared);
    419         if (mOnPatternListener != null) {
    420             mOnPatternListener.onPatternCleared();
    421         }
    422     }
    423 
    424     /**
    425      * Clear the pattern.
    426      */
    427     public void clearPattern() {
    428         resetPattern();
    429     }
    430 
    431     /**
    432      * Reset all pattern state.
    433      */
    434     private void resetPattern() {
    435         mPattern.clear();
    436         clearPatternDrawLookup();
    437         mPatternDisplayMode = DisplayMode.Correct;
    438         invalidate();
    439     }
    440 
    441     /**
    442      * Clear the pattern lookup table.
    443      */
    444     private void clearPatternDrawLookup() {
    445         for (int i = 0; i < 3; i++) {
    446             for (int j = 0; j < 3; j++) {
    447                 mPatternDrawLookup[i][j] = false;
    448             }
    449         }
    450     }
    451 
    452     /**
    453      * Disable input (for instance when displaying a message that will
    454      * timeout so user doesn't get view into messy state).
    455      */
    456     public void disableInput() {
    457         mInputEnabled = false;
    458     }
    459 
    460     /**
    461      * Enable input.
    462      */
    463     public void enableInput() {
    464         mInputEnabled = true;
    465     }
    466 
    467     @Override
    468     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    469         final int width = w - mPaddingLeft - mPaddingRight;
    470         mSquareWidth = width / 3.0f;
    471 
    472         final int height = h - mPaddingTop - mPaddingBottom;
    473         mSquareHeight = height / 3.0f;
    474     }
    475 
    476     private int resolveMeasured(int measureSpec, int desired)
    477     {
    478         int result = 0;
    479         int specSize = MeasureSpec.getSize(measureSpec);
    480         switch (MeasureSpec.getMode(measureSpec)) {
    481             case MeasureSpec.UNSPECIFIED:
    482                 result = desired;
    483                 break;
    484             case MeasureSpec.AT_MOST:
    485                 result = Math.max(specSize, desired);
    486                 break;
    487             case MeasureSpec.EXACTLY:
    488             default:
    489                 result = specSize;
    490         }
    491         return result;
    492     }
    493 
    494     @Override
    495     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    496         final int minimumWidth = getSuggestedMinimumWidth();
    497         final int minimumHeight = getSuggestedMinimumHeight();
    498         int viewWidth = resolveMeasured(widthMeasureSpec, minimumWidth);
    499         int viewHeight = resolveMeasured(heightMeasureSpec, minimumHeight);
    500 
    501         switch (mAspect) {
    502             case ASPECT_SQUARE:
    503                 viewWidth = viewHeight = Math.min(viewWidth, viewHeight);
    504                 break;
    505             case ASPECT_LOCK_WIDTH:
    506                 viewHeight = Math.min(viewWidth, viewHeight);
    507                 break;
    508             case ASPECT_LOCK_HEIGHT:
    509                 viewWidth = Math.min(viewWidth, viewHeight);
    510                 break;
    511         }
    512         // Log.v(TAG, "LockPatternView dimensions: " + viewWidth + "x" + viewHeight);
    513         setMeasuredDimension(viewWidth, viewHeight);
    514     }
    515 
    516     /**
    517      * Determines whether the point x, y will add a new point to the current
    518      * pattern (in addition to finding the cell, also makes heuristic choices
    519      * such as filling in gaps based on current pattern).
    520      * @param x The x coordinate.
    521      * @param y The y coordinate.
    522      */
    523     private Cell detectAndAddHit(float x, float y) {
    524         final Cell cell = checkForNewHit(x, y);
    525         if (cell != null) {
    526 
    527             // check for gaps in existing pattern
    528             Cell fillInGapCell = null;
    529             final ArrayList<Cell> pattern = mPattern;
    530             if (!pattern.isEmpty()) {
    531                 final Cell lastCell = pattern.get(pattern.size() - 1);
    532                 int dRow = cell.row - lastCell.row;
    533                 int dColumn = cell.column - lastCell.column;
    534 
    535                 int fillInRow = lastCell.row;
    536                 int fillInColumn = lastCell.column;
    537 
    538                 if (Math.abs(dRow) == 2 && Math.abs(dColumn) != 1) {
    539                     fillInRow = lastCell.row + ((dRow > 0) ? 1 : -1);
    540                 }
    541 
    542                 if (Math.abs(dColumn) == 2 && Math.abs(dRow) != 1) {
    543                     fillInColumn = lastCell.column + ((dColumn > 0) ? 1 : -1);
    544                 }
    545 
    546                 fillInGapCell = Cell.of(fillInRow, fillInColumn);
    547             }
    548 
    549             if (fillInGapCell != null &&
    550                     !mPatternDrawLookup[fillInGapCell.row][fillInGapCell.column]) {
    551                 addCellToPattern(fillInGapCell);
    552             }
    553             addCellToPattern(cell);
    554             if (mEnableHapticFeedback) {
    555                 performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY,
    556                         HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING
    557                         | HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING);
    558             }
    559             return cell;
    560         }
    561         return null;
    562     }
    563 
    564     private void addCellToPattern(Cell newCell) {
    565         mPatternDrawLookup[newCell.getRow()][newCell.getColumn()] = true;
    566         mPattern.add(newCell);
    567         if (!mInStealthMode) {
    568             startCellActivatedAnimation(newCell);
    569         }
    570         notifyCellAdded();
    571     }
    572 
    573     private void startCellActivatedAnimation(Cell cell) {
    574         final CellState cellState = mCellStates[cell.row][cell.column];
    575         startSizeAnimation(mDotSize, mDotSizeActivated, 96, mLinearOutSlowInInterpolator,
    576                 cellState, new Runnable() {
    577             @Override
    578             public void run() {
    579                 startSizeAnimation(mDotSizeActivated, mDotSize, 192, mFastOutSlowInInterpolator,
    580                         cellState, null);
    581             }
    582         });
    583         startLineEndAnimation(cellState, mInProgressX, mInProgressY,
    584                 getCenterXForColumn(cell.column), getCenterYForRow(cell.row));
    585     }
    586 
    587     private void startLineEndAnimation(final CellState state,
    588             final float startX, final float startY, final float targetX, final float targetY) {
    589         ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1);
    590         valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    591             @Override
    592             public void onAnimationUpdate(ValueAnimator animation) {
    593                 float t = (float) animation.getAnimatedValue();
    594                 state.lineEndX = (1 - t) * startX + t * targetX;
    595                 state.lineEndY = (1 - t) * startY + t * targetY;
    596                 invalidate();
    597             }
    598         });
    599         valueAnimator.addListener(new AnimatorListenerAdapter() {
    600             @Override
    601             public void onAnimationEnd(Animator animation) {
    602                 state.lineAnimator = null;
    603             }
    604         });
    605         valueAnimator.setInterpolator(mFastOutSlowInInterpolator);
    606         valueAnimator.setDuration(100);
    607         valueAnimator.start();
    608         state.lineAnimator = valueAnimator;
    609     }
    610 
    611     private void startSizeAnimation(float start, float end, long duration, Interpolator interpolator,
    612             final CellState state, final Runnable endRunnable) {
    613         ValueAnimator valueAnimator = ValueAnimator.ofFloat(start, end);
    614         valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    615             @Override
    616             public void onAnimationUpdate(ValueAnimator animation) {
    617                 state.size = (float) animation.getAnimatedValue();
    618                 invalidate();
    619             }
    620         });
    621         if (endRunnable != null) {
    622             valueAnimator.addListener(new AnimatorListenerAdapter() {
    623                 @Override
    624                 public void onAnimationEnd(Animator animation) {
    625                     endRunnable.run();
    626                 }
    627             });
    628         }
    629         valueAnimator.setInterpolator(interpolator);
    630         valueAnimator.setDuration(duration);
    631         valueAnimator.start();
    632     }
    633 
    634     // helper method to find which cell a point maps to
    635     private Cell checkForNewHit(float x, float y) {
    636 
    637         final int rowHit = getRowHit(y);
    638         if (rowHit < 0) {
    639             return null;
    640         }
    641         final int columnHit = getColumnHit(x);
    642         if (columnHit < 0) {
    643             return null;
    644         }
    645 
    646         if (mPatternDrawLookup[rowHit][columnHit]) {
    647             return null;
    648         }
    649         return Cell.of(rowHit, columnHit);
    650     }
    651 
    652     /**
    653      * Helper method to find the row that y falls into.
    654      * @param y The y coordinate
    655      * @return The row that y falls in, or -1 if it falls in no row.
    656      */
    657     private int getRowHit(float y) {
    658 
    659         final float squareHeight = mSquareHeight;
    660         float hitSize = squareHeight * mHitFactor;
    661 
    662         float offset = mPaddingTop + (squareHeight - hitSize) / 2f;
    663         for (int i = 0; i < 3; i++) {
    664 
    665             final float hitTop = offset + squareHeight * i;
    666             if (y >= hitTop && y <= hitTop + hitSize) {
    667                 return i;
    668             }
    669         }
    670         return -1;
    671     }
    672 
    673     /**
    674      * Helper method to find the column x fallis into.
    675      * @param x The x coordinate.
    676      * @return The column that x falls in, or -1 if it falls in no column.
    677      */
    678     private int getColumnHit(float x) {
    679         final float squareWidth = mSquareWidth;
    680         float hitSize = squareWidth * mHitFactor;
    681 
    682         float offset = mPaddingLeft + (squareWidth - hitSize) / 2f;
    683         for (int i = 0; i < 3; i++) {
    684 
    685             final float hitLeft = offset + squareWidth * i;
    686             if (x >= hitLeft && x <= hitLeft + hitSize) {
    687                 return i;
    688             }
    689         }
    690         return -1;
    691     }
    692 
    693     @Override
    694     public boolean onHoverEvent(MotionEvent event) {
    695         if (AccessibilityManager.getInstance(mContext).isTouchExplorationEnabled()) {
    696             final int action = event.getAction();
    697             switch (action) {
    698                 case MotionEvent.ACTION_HOVER_ENTER:
    699                     event.setAction(MotionEvent.ACTION_DOWN);
    700                     break;
    701                 case MotionEvent.ACTION_HOVER_MOVE:
    702                     event.setAction(MotionEvent.ACTION_MOVE);
    703                     break;
    704                 case MotionEvent.ACTION_HOVER_EXIT:
    705                     event.setAction(MotionEvent.ACTION_UP);
    706                     break;
    707             }
    708             onTouchEvent(event);
    709             event.setAction(action);
    710         }
    711         return super.onHoverEvent(event);
    712     }
    713 
    714     @Override
    715     public boolean onTouchEvent(MotionEvent event) {
    716         if (!mInputEnabled || !isEnabled()) {
    717             return false;
    718         }
    719 
    720         switch(event.getAction()) {
    721             case MotionEvent.ACTION_DOWN:
    722                 handleActionDown(event);
    723                 return true;
    724             case MotionEvent.ACTION_UP:
    725                 handleActionUp(event);
    726                 return true;
    727             case MotionEvent.ACTION_MOVE:
    728                 handleActionMove(event);
    729                 return true;
    730             case MotionEvent.ACTION_CANCEL:
    731                 if (mPatternInProgress) {
    732                     mPatternInProgress = false;
    733                     resetPattern();
    734                     notifyPatternCleared();
    735                 }
    736                 if (PROFILE_DRAWING) {
    737                     if (mDrawingProfilingStarted) {
    738                         Debug.stopMethodTracing();
    739                         mDrawingProfilingStarted = false;
    740                     }
    741                 }
    742                 return true;
    743         }
    744         return false;
    745     }
    746 
    747     private void handleActionMove(MotionEvent event) {
    748         // Handle all recent motion events so we don't skip any cells even when the device
    749         // is busy...
    750         final float radius = mPathWidth;
    751         final int historySize = event.getHistorySize();
    752         mTmpInvalidateRect.setEmpty();
    753         boolean invalidateNow = false;
    754         for (int i = 0; i < historySize + 1; i++) {
    755             final float x = i < historySize ? event.getHistoricalX(i) : event.getX();
    756             final float y = i < historySize ? event.getHistoricalY(i) : event.getY();
    757             Cell hitCell = detectAndAddHit(x, y);
    758             final int patternSize = mPattern.size();
    759             if (hitCell != null && patternSize == 1) {
    760                 mPatternInProgress = true;
    761                 notifyPatternStarted();
    762             }
    763             // note current x and y for rubber banding of in progress patterns
    764             final float dx = Math.abs(x - mInProgressX);
    765             final float dy = Math.abs(y - mInProgressY);
    766             if (dx > DRAG_THRESHHOLD || dy > DRAG_THRESHHOLD) {
    767                 invalidateNow = true;
    768             }
    769 
    770             if (mPatternInProgress && patternSize > 0) {
    771                 final ArrayList<Cell> pattern = mPattern;
    772                 final Cell lastCell = pattern.get(patternSize - 1);
    773                 float lastCellCenterX = getCenterXForColumn(lastCell.column);
    774                 float lastCellCenterY = getCenterYForRow(lastCell.row);
    775 
    776                 // Adjust for drawn segment from last cell to (x,y). Radius accounts for line width.
    777                 float left = Math.min(lastCellCenterX, x) - radius;
    778                 float right = Math.max(lastCellCenterX, x) + radius;
    779                 float top = Math.min(lastCellCenterY, y) - radius;
    780                 float bottom = Math.max(lastCellCenterY, y) + radius;
    781 
    782                 // Invalidate between the pattern's new cell and the pattern's previous cell
    783                 if (hitCell != null) {
    784                     final float width = mSquareWidth * 0.5f;
    785                     final float height = mSquareHeight * 0.5f;
    786                     final float hitCellCenterX = getCenterXForColumn(hitCell.column);
    787                     final float hitCellCenterY = getCenterYForRow(hitCell.row);
    788 
    789                     left = Math.min(hitCellCenterX - width, left);
    790                     right = Math.max(hitCellCenterX + width, right);
    791                     top = Math.min(hitCellCenterY - height, top);
    792                     bottom = Math.max(hitCellCenterY + height, bottom);
    793                 }
    794 
    795                 // Invalidate between the pattern's last cell and the previous location
    796                 mTmpInvalidateRect.union(Math.round(left), Math.round(top),
    797                         Math.round(right), Math.round(bottom));
    798             }
    799         }
    800         mInProgressX = event.getX();
    801         mInProgressY = event.getY();
    802 
    803         // To save updates, we only invalidate if the user moved beyond a certain amount.
    804         if (invalidateNow) {
    805             mInvalidate.union(mTmpInvalidateRect);
    806             invalidate(mInvalidate);
    807             mInvalidate.set(mTmpInvalidateRect);
    808         }
    809     }
    810 
    811     private void sendAccessEvent(int resId) {
    812         announceForAccessibility(mContext.getString(resId));
    813     }
    814 
    815     private void handleActionUp(MotionEvent event) {
    816         // report pattern detected
    817         if (!mPattern.isEmpty()) {
    818             mPatternInProgress = false;
    819             cancelLineAnimations();
    820             notifyPatternDetected();
    821             invalidate();
    822         }
    823         if (PROFILE_DRAWING) {
    824             if (mDrawingProfilingStarted) {
    825                 Debug.stopMethodTracing();
    826                 mDrawingProfilingStarted = false;
    827             }
    828         }
    829     }
    830 
    831     private void cancelLineAnimations() {
    832         for (int i = 0; i < 3; i++) {
    833             for (int j = 0; j < 3; j++) {
    834                 CellState state = mCellStates[i][j];
    835                 if (state.lineAnimator != null) {
    836                     state.lineAnimator.cancel();
    837                     state.lineEndX = Float.MIN_VALUE;
    838                     state.lineEndY = Float.MIN_VALUE;
    839                 }
    840             }
    841         }
    842     }
    843     private void handleActionDown(MotionEvent event) {
    844         resetPattern();
    845         final float x = event.getX();
    846         final float y = event.getY();
    847         final Cell hitCell = detectAndAddHit(x, y);
    848         if (hitCell != null) {
    849             mPatternInProgress = true;
    850             mPatternDisplayMode = DisplayMode.Correct;
    851             notifyPatternStarted();
    852         } else if (mPatternInProgress) {
    853             mPatternInProgress = false;
    854             notifyPatternCleared();
    855         }
    856         if (hitCell != null) {
    857             final float startX = getCenterXForColumn(hitCell.column);
    858             final float startY = getCenterYForRow(hitCell.row);
    859 
    860             final float widthOffset = mSquareWidth / 2f;
    861             final float heightOffset = mSquareHeight / 2f;
    862 
    863             invalidate((int) (startX - widthOffset), (int) (startY - heightOffset),
    864                     (int) (startX + widthOffset), (int) (startY + heightOffset));
    865         }
    866         mInProgressX = x;
    867         mInProgressY = y;
    868         if (PROFILE_DRAWING) {
    869             if (!mDrawingProfilingStarted) {
    870                 Debug.startMethodTracing("LockPatternDrawing");
    871                 mDrawingProfilingStarted = true;
    872             }
    873         }
    874     }
    875 
    876     private float getCenterXForColumn(int column) {
    877         return mPaddingLeft + column * mSquareWidth + mSquareWidth / 2f;
    878     }
    879 
    880     private float getCenterYForRow(int row) {
    881         return mPaddingTop + row * mSquareHeight + mSquareHeight / 2f;
    882     }
    883 
    884     @Override
    885     protected void onDraw(Canvas canvas) {
    886         final ArrayList<Cell> pattern = mPattern;
    887         final int count = pattern.size();
    888         final boolean[][] drawLookup = mPatternDrawLookup;
    889 
    890         if (mPatternDisplayMode == DisplayMode.Animate) {
    891 
    892             // figure out which circles to draw
    893 
    894             // + 1 so we pause on complete pattern
    895             final int oneCycle = (count + 1) * MILLIS_PER_CIRCLE_ANIMATING;
    896             final int spotInCycle = (int) (SystemClock.elapsedRealtime() -
    897                     mAnimatingPeriodStart) % oneCycle;
    898             final int numCircles = spotInCycle / MILLIS_PER_CIRCLE_ANIMATING;
    899 
    900             clearPatternDrawLookup();
    901             for (int i = 0; i < numCircles; i++) {
    902                 final Cell cell = pattern.get(i);
    903                 drawLookup[cell.getRow()][cell.getColumn()] = true;
    904             }
    905 
    906             // figure out in progress portion of ghosting line
    907 
    908             final boolean needToUpdateInProgressPoint = numCircles > 0
    909                     && numCircles < count;
    910 
    911             if (needToUpdateInProgressPoint) {
    912                 final float percentageOfNextCircle =
    913                         ((float) (spotInCycle % MILLIS_PER_CIRCLE_ANIMATING)) /
    914                                 MILLIS_PER_CIRCLE_ANIMATING;
    915 
    916                 final Cell currentCell = pattern.get(numCircles - 1);
    917                 final float centerX = getCenterXForColumn(currentCell.column);
    918                 final float centerY = getCenterYForRow(currentCell.row);
    919 
    920                 final Cell nextCell = pattern.get(numCircles);
    921                 final float dx = percentageOfNextCircle *
    922                         (getCenterXForColumn(nextCell.column) - centerX);
    923                 final float dy = percentageOfNextCircle *
    924                         (getCenterYForRow(nextCell.row) - centerY);
    925                 mInProgressX = centerX + dx;
    926                 mInProgressY = centerY + dy;
    927             }
    928             // TODO: Infinite loop here...
    929             invalidate();
    930         }
    931 
    932         final Path currentPath = mCurrentPath;
    933         currentPath.rewind();
    934 
    935         // draw the circles
    936         for (int i = 0; i < 3; i++) {
    937             float centerY = getCenterYForRow(i);
    938             for (int j = 0; j < 3; j++) {
    939                 CellState cellState = mCellStates[i][j];
    940                 float centerX = getCenterXForColumn(j);
    941                 float size = cellState.size * cellState.scale;
    942                 float translationY = cellState.translateY;
    943                 drawCircle(canvas, (int) centerX, (int) centerY + translationY,
    944                         size, drawLookup[i][j], cellState.alpha);
    945             }
    946         }
    947 
    948         // TODO: the path should be created and cached every time we hit-detect a cell
    949         // only the last segment of the path should be computed here
    950         // draw the path of the pattern (unless we are in stealth mode)
    951         final boolean drawPath = !mInStealthMode;
    952 
    953         if (drawPath) {
    954             mPathPaint.setColor(getCurrentColor(true /* partOfPattern */));
    955 
    956             boolean anyCircles = false;
    957             float lastX = 0f;
    958             float lastY = 0f;
    959             for (int i = 0; i < count; i++) {
    960                 Cell cell = pattern.get(i);
    961 
    962                 // only draw the part of the pattern stored in
    963                 // the lookup table (this is only different in the case
    964                 // of animation).
    965                 if (!drawLookup[cell.row][cell.column]) {
    966                     break;
    967                 }
    968                 anyCircles = true;
    969 
    970                 float centerX = getCenterXForColumn(cell.column);
    971                 float centerY = getCenterYForRow(cell.row);
    972                 if (i != 0) {
    973                     CellState state = mCellStates[cell.row][cell.column];
    974                     currentPath.rewind();
    975                     currentPath.moveTo(lastX, lastY);
    976                     if (state.lineEndX != Float.MIN_VALUE && state.lineEndY != Float.MIN_VALUE) {
    977                         currentPath.lineTo(state.lineEndX, state.lineEndY);
    978                     } else {
    979                         currentPath.lineTo(centerX, centerY);
    980                     }
    981                     canvas.drawPath(currentPath, mPathPaint);
    982                 }
    983                 lastX = centerX;
    984                 lastY = centerY;
    985             }
    986 
    987             // draw last in progress section
    988             if ((mPatternInProgress || mPatternDisplayMode == DisplayMode.Animate)
    989                     && anyCircles) {
    990                 currentPath.rewind();
    991                 currentPath.moveTo(lastX, lastY);
    992                 currentPath.lineTo(mInProgressX, mInProgressY);
    993 
    994                 mPathPaint.setAlpha((int) (calculateLastSegmentAlpha(
    995                         mInProgressX, mInProgressY, lastX, lastY) * 255f));
    996                 canvas.drawPath(currentPath, mPathPaint);
    997             }
    998         }
    999     }
   1000 
   1001     private float calculateLastSegmentAlpha(float x, float y, float lastX, float lastY) {
   1002         float diffX = x - lastX;
   1003         float diffY = y - lastY;
   1004         float dist = (float) Math.sqrt(diffX*diffX + diffY*diffY);
   1005         float frac = dist/mSquareWidth;
   1006         return Math.min(1f, Math.max(0f, (frac - 0.3f) * 4f));
   1007     }
   1008 
   1009     private int getCurrentColor(boolean partOfPattern) {
   1010         if (!partOfPattern || mInStealthMode || mPatternInProgress) {
   1011             // unselected circle
   1012             return mRegularColor;
   1013         } else if (mPatternDisplayMode == DisplayMode.Wrong) {
   1014             // the pattern is wrong
   1015             return mErrorColor;
   1016         } else if (mPatternDisplayMode == DisplayMode.Correct ||
   1017                 mPatternDisplayMode == DisplayMode.Animate) {
   1018             return mSuccessColor;
   1019         } else {
   1020             throw new IllegalStateException("unknown display mode " + mPatternDisplayMode);
   1021         }
   1022     }
   1023 
   1024     /**
   1025      * @param partOfPattern Whether this circle is part of the pattern.
   1026      */
   1027     private void drawCircle(Canvas canvas, float centerX, float centerY, float size,
   1028             boolean partOfPattern, float alpha) {
   1029         mPaint.setColor(getCurrentColor(partOfPattern));
   1030         mPaint.setAlpha((int) (alpha * 255));
   1031         canvas.drawCircle(centerX, centerY, size/2, mPaint);
   1032     }
   1033 
   1034     @Override
   1035     protected Parcelable onSaveInstanceState() {
   1036         Parcelable superState = super.onSaveInstanceState();
   1037         return new SavedState(superState,
   1038                 LockPatternUtils.patternToString(mPattern),
   1039                 mPatternDisplayMode.ordinal(),
   1040                 mInputEnabled, mInStealthMode, mEnableHapticFeedback);
   1041     }
   1042 
   1043     @Override
   1044     protected void onRestoreInstanceState(Parcelable state) {
   1045         final SavedState ss = (SavedState) state;
   1046         super.onRestoreInstanceState(ss.getSuperState());
   1047         setPattern(
   1048                 DisplayMode.Correct,
   1049                 LockPatternUtils.stringToPattern(ss.getSerializedPattern()));
   1050         mPatternDisplayMode = DisplayMode.values()[ss.getDisplayMode()];
   1051         mInputEnabled = ss.isInputEnabled();
   1052         mInStealthMode = ss.isInStealthMode();
   1053         mEnableHapticFeedback = ss.isTactileFeedbackEnabled();
   1054     }
   1055 
   1056     /**
   1057      * The parecelable for saving and restoring a lock pattern view.
   1058      */
   1059     private static class SavedState extends BaseSavedState {
   1060 
   1061         private final String mSerializedPattern;
   1062         private final int mDisplayMode;
   1063         private final boolean mInputEnabled;
   1064         private final boolean mInStealthMode;
   1065         private final boolean mTactileFeedbackEnabled;
   1066 
   1067         /**
   1068          * Constructor called from {@link LockPatternView#onSaveInstanceState()}
   1069          */
   1070         private SavedState(Parcelable superState, String serializedPattern, int displayMode,
   1071                 boolean inputEnabled, boolean inStealthMode, boolean tactileFeedbackEnabled) {
   1072             super(superState);
   1073             mSerializedPattern = serializedPattern;
   1074             mDisplayMode = displayMode;
   1075             mInputEnabled = inputEnabled;
   1076             mInStealthMode = inStealthMode;
   1077             mTactileFeedbackEnabled = tactileFeedbackEnabled;
   1078         }
   1079 
   1080         /**
   1081          * Constructor called from {@link #CREATOR}
   1082          */
   1083         private SavedState(Parcel in) {
   1084             super(in);
   1085             mSerializedPattern = in.readString();
   1086             mDisplayMode = in.readInt();
   1087             mInputEnabled = (Boolean) in.readValue(null);
   1088             mInStealthMode = (Boolean) in.readValue(null);
   1089             mTactileFeedbackEnabled = (Boolean) in.readValue(null);
   1090         }
   1091 
   1092         public String getSerializedPattern() {
   1093             return mSerializedPattern;
   1094         }
   1095 
   1096         public int getDisplayMode() {
   1097             return mDisplayMode;
   1098         }
   1099 
   1100         public boolean isInputEnabled() {
   1101             return mInputEnabled;
   1102         }
   1103 
   1104         public boolean isInStealthMode() {
   1105             return mInStealthMode;
   1106         }
   1107 
   1108         public boolean isTactileFeedbackEnabled(){
   1109             return mTactileFeedbackEnabled;
   1110         }
   1111 
   1112         @Override
   1113         public void writeToParcel(Parcel dest, int flags) {
   1114             super.writeToParcel(dest, flags);
   1115             dest.writeString(mSerializedPattern);
   1116             dest.writeInt(mDisplayMode);
   1117             dest.writeValue(mInputEnabled);
   1118             dest.writeValue(mInStealthMode);
   1119             dest.writeValue(mTactileFeedbackEnabled);
   1120         }
   1121 
   1122         public static final Parcelable.Creator<SavedState> CREATOR =
   1123                 new Creator<SavedState>() {
   1124                     public SavedState createFromParcel(Parcel in) {
   1125                         return new SavedState(in);
   1126                     }
   1127 
   1128                     public SavedState[] newArray(int size) {
   1129                         return new SavedState[size];
   1130                     }
   1131                 };
   1132     }
   1133 }
   1134