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