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