欢迎使用优快云-markdown编辑器

本文介绍了一个手势解锁图案视图的实现方案,包括自定义View的绘制逻辑、触摸事件处理流程、图案检测回调接口等核心功能。该View支持动画展示、错误反馈等多种显示模式。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

/**
* 手势的View
lmj
*/
public class LockPatternView extends View {
// Aspect to use when rendering this view
private static final int ASPECT_SQUARE = 0; // View will be the minimum of
// width/height
private static final int ASPECT_LOCK_WIDTH = 1; // Fixed width; height will
// be minimum of (w,h)
private static final int ASPECT_LOCK_HEIGHT = 2; // Fixed height; width will
// be minimum of (w,h)

public static final int MIN_LOCK_PATTERN_SIZE = 4;

private static final boolean PROFILE_DRAWING = false;
private boolean mDrawingProfilingStarted = false;

private Paint mPaint = new Paint();
private Paint mPathPaint = new Paint();

/**
 * How many milliseconds we spend animating each circle of a lock pattern if
 * the animating mode is set. The entire animation should take this constant
 * * the length of the pattern to complete.
 */
private static final int MILLIS_PER_CIRCLE_ANIMATING = 700;

/**
 * This can be used to avoid updating the display for very small motions or
 * noisy panels. It didn't seem to have much impact on the devices tested,
 * so currently set to 0.
 */
private static final float DRAG_THRESHHOLD = 0.0f;

private OnPatternListener mOnPatternListener;
private ArrayList<Cell> mPattern = new ArrayList<Cell>(9);

/**
 * Lookup table for the circles of the pattern we are currently drawing.
 * This will be the cells of the complete pattern unless we are animating,
 * in which case we use this to hold the cells we are drawing for the in
 * progress animation.
 */
private boolean[][] mPatternDrawLookup = new boolean[3][3];

/**
 * the in progress point: - during interaction: where the user's finger is -
 * during animation: the current tip of the animating line
 */
private float mInProgressX = -1;
private float mInProgressY = -1;

private long mAnimatingPeriodStart;

private DisplayMode mPatternDisplayMode = DisplayMode.Correct;
private boolean mInputEnabled = true;
private boolean mInStealthMode = false;
private boolean mEnableHapticFeedback = true;
private boolean mPatternInProgress = false;

private float mDiameterFactor = 0.10f; // TODO: move to attrs
private final int mStrokeAlpha = 128;
private float mHitFactor = 0.6f;

private float mSquareWidth;
private float mSquareHeight;

private Bitmap mBitmapBtnDefault;
private Bitmap mBitmapBtnTouched;
private Bitmap mBitmapCircleDefault;
private Bitmap mBitmapCircleGreen;
private Bitmap mBitmapCircleRed;

private Bitmap mBitmapArrowGreenUp;
private Bitmap mBitmapArrowRedUp;

private final Path mCurrentPath = new Path();
private final Rect mInvalidate = new Rect();
private final Rect mTmpInvalidateRect = new Rect();

private int mBitmapWidth;
private int mBitmapHeight;

private int mAspect;
private final Matrix mArrowMatrix = new Matrix();
private final Matrix mCircleMatrix = new Matrix();

/**
 * Represents a cell in the 3 X 3 matrix of the unlock pattern view.
 */
public static class Cell {
    int row;
    int column;

    // keep # objects limited to 9
    static Cell[][] sCells = new Cell[3][3];
    static {
        for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 3; j++) {
                sCells[i][j] = new Cell(i, j);
            }
        }
    }

    /**
     * @param row
     *            The row of the cell.
     * @param column
     *            The column of the cell.
     */
    private Cell(int row, int column) {
        checkRange(row, column);
        this.row = row;
        this.column = column;
    }

    public int getRow() {
        return row;
    }

    public int getColumn() {
        return column;
    }

    /**
     * @param row
     *            The row of the cell.
     * @param column
     *            The column of the cell.
     */
    public static synchronized Cell of(int row, int column) {
        checkRange(row, column);
        return sCells[row][column];
    }

    private static void checkRange(int row, int column) {
        if (row < 0 || row > 2) {
            throw new IllegalArgumentException("row must be in range 0-2");
        }
        if (column < 0 || column > 2) {
            throw new IllegalArgumentException("column must be in range 0-2");
        }
    }

    public String toString() {
        return "(row=" + row + ",clmn=" + column + ")";
    }
}

/**
 * How to display the current pattern.
 */
public enum DisplayMode {

    /**
     * The pattern drawn is correct (i.e draw it in a friendly color)
     */
    Correct,

    /**
     * Animate the pattern (for demo, and help).
     */
    Animate,

    /**
     * The pattern is wrong (i.e draw a foreboding color)
     */
    Wrong
}

/**
 * The call back interface for detecting patterns entered by the user.
 */
public static interface OnPatternListener {

    /**
     * A new pattern has begun.
     */
    void onPatternStart();

    /**
     * The pattern was cleared.
     */
    void onPatternCleared();

    /**
     * The user extended the pattern currently being drawn by one cell.
     * 
     * @param pattern
     *            The pattern with newly added cell.
     */
    void onPatternCellAdded(List<Cell> pattern);

    /**
     * A pattern was detected from the user.
     * 
     * @param pattern
     *            The pattern.
     */
    void onPatternDetected(List<Cell> pattern);
}

public LockPatternView(Context context) {
    this(context, null);
}

public LockPatternView(Context context, AttributeSet attrs) {
    super(context, attrs);

    TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LockPatternView);

    final String aspect = a.getString(R.styleable.LockPatternView_aspect);

    if ("square".equals(aspect)) {
        mAspect = ASPECT_SQUARE;
    } else if ("lock_width".equals(aspect)) {
        mAspect = ASPECT_LOCK_WIDTH;
    } else if ("lock_height".equals(aspect)) {
        mAspect = ASPECT_LOCK_HEIGHT;
    } else {
        mAspect = ASPECT_SQUARE;
    }

    setClickable(true);

    mPathPaint.setAntiAlias(true);
    mPathPaint.setDither(true);
    mPathPaint.setColor(Color.WHITE); // TODO this should be from the style
    mPathPaint.setAlpha(mStrokeAlpha);
    mPathPaint.setStyle(Paint.Style.STROKE);
    mPathPaint.setStrokeJoin(Paint.Join.ROUND);
    mPathPaint.setStrokeCap(Paint.Cap.ROUND);

    // lot's of bitmaps!
    mBitmapBtnDefault = getBitmapFor(R.drawable.btn_code_lock_default_holo);
    mBitmapBtnTouched = getBitmapFor(R.drawable.btn_code_lock_touched_holo);
    mBitmapCircleDefault = getBitmapFor(R.drawable.indicator_code_lock_point_area_default_holo);
    mBitmapCircleGreen = getBitmapFor(R.drawable.indicator_code_lock_point_area_green_holo);
    mBitmapCircleRed = getBitmapFor(R.drawable.indicator_code_lock_point_area_red_holo);

    mBitmapArrowGreenUp = getBitmapFor(R.drawable.indicator_code_lock_drag_direction_green_up);
    mBitmapArrowRedUp = getBitmapFor(R.drawable.indicator_code_lock_drag_direction_red_up);

    // bitmaps have the size of the largest bitmap in this group
    final Bitmap bitmaps[] = { mBitmapBtnDefault, mBitmapBtnTouched, mBitmapCircleDefault, mBitmapCircleGreen,
            mBitmapCircleRed };

    for (Bitmap bitmap : bitmaps) {
        mBitmapWidth = Math.max(mBitmapWidth, bitmap.getWidth());
        mBitmapHeight = Math.max(mBitmapHeight, bitmap.getHeight());
    }

}

private Bitmap getBitmapFor(int resId) {
    return BitmapFactory.decodeResource(getContext().getResources(), resId);
}

/**
 * @return Whether the view is in stealth mode.
 */
public boolean isInStealthMode() {
    return mInStealthMode;
}

/**
 * @return Whether the view has tactile feedback enabled.
 */
public boolean isTactileFeedbackEnabled() {
    return mEnableHapticFeedback;
}

/**
 * Set whether the view is in stealth mode. If true, there will be no
 * visible feedback as the user enters the pattern.
 *
 * @param inStealthMode
 *            Whether in stealth mode.
 */
public void setInStealthMode(boolean inStealthMode) {
    mInStealthMode = inStealthMode;
}

/**
 * Set whether the view will use tactile feedback. If true, there will be
 * tactile feedback as the user enters the pattern.
 *
 * @param tactileFeedbackEnabled
 *            Whether tactile feedback is enabled
 */
public void setTactileFeedbackEnabled(boolean tactileFeedbackEnabled) {
    mEnableHapticFeedback = tactileFeedbackEnabled;
}

/**
 * Set the call back for pattern detection.
 * 
 * @param onPatternListener
 *            The call back.
 */
public void setOnPatternListener(OnPatternListener onPatternListener) {
    mOnPatternListener = onPatternListener;
}

/**
 * Set the pattern explicitely (rather than waiting for the user to input a
 * pattern).
 * 
 * @param displayMode
 *            How to display the pattern.
 * @param pattern
 *            The pattern.
 */
public void setPattern(DisplayMode displayMode, List<Cell> pattern) {
    mPattern.clear();
    mPattern.addAll(pattern);
    clearPatternDrawLookup();
    for (Cell cell : pattern) {
        mPatternDrawLookup[cell.getRow()][cell.getColumn()] = true;
    }

    setDisplayMode(displayMode);
}

/**
 * Set the display mode of the current pattern. This can be useful, for
 * instance, after detecting a pattern to tell this view whether change the
 * in progress result to correct or wrong.
 * 
 * @param displayMode
 *            The display mode.
 */
public void setDisplayMode(DisplayMode displayMode) {
    mPatternDisplayMode = displayMode;
    if (displayMode == DisplayMode.Animate) {
        if (mPattern.size() == 0) {
            throw new IllegalStateException(
                    "you must have a pattern to " + "animate if you want to set the display mode to animate");
        }
        mAnimatingPeriodStart = SystemClock.elapsedRealtime();
        final Cell first = mPattern.get(0);
        mInProgressX = getCenterXForColumn(first.getColumn());
        mInProgressY = getCenterYForRow(first.getRow());
        clearPatternDrawLookup();
    }
    invalidate();
}

private void notifyCellAdded() {
    sendAccessEvent(R.string.lockscreen_access_pattern_cell_added);
    if (mOnPatternListener != null) {
        mOnPatternListener.onPatternCellAdded(mPattern);
    }
}

private void notifyPatternStarted() {
    sendAccessEvent(R.string.lockscreen_access_pattern_start);
    if (mOnPatternListener != null) {
        mOnPatternListener.onPatternStart();
    }
}

private void notifyPatternDetected() {
    sendAccessEvent(R.string.lockscreen_access_pattern_detected);
    if (mOnPatternListener != null) {
        mOnPatternListener.onPatternDetected(mPattern);
    }
}

private void notifyPatternCleared() {
    sendAccessEvent(R.string.lockscreen_access_pattern_cleared);
    if (mOnPatternListener != null) {
        mOnPatternListener.onPatternCleared();
    }
}

/**
 * Clear the pattern.
 */
public void clearPattern() {
    resetPattern();
}

/**
 * Reset all pattern state.
 */
private void resetPattern() {
    mPattern.clear();
    clearPatternDrawLookup();
    mPatternDisplayMode = DisplayMode.Correct;
    invalidate();
}

/**
 * Clear the pattern lookup table.
 */
private void clearPatternDrawLookup() {
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 3; j++) {
            mPatternDrawLookup[i][j] = false;
        }
    }
}

/**
 * Disable input (for instance when displaying a message that will timeout
 * so user doesn't get view into messy state).
 */
public void disableInput() {
    mInputEnabled = false;
}

/**
 * Enable input.
 */
public void enableInput() {
    mInputEnabled = true;
}

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    final int width = w - getPaddingLeft() - getPaddingRight();
    mSquareWidth = width / 3.0f;

    final int height = h - getPaddingTop() - getPaddingBottom();
    mSquareHeight = height / 3.0f;
}

private int resolveMeasured(int measureSpec, int desired) {
    int result = 0;
    int specSize = MeasureSpec.getSize(measureSpec);
    switch (MeasureSpec.getMode(measureSpec)) {
    case MeasureSpec.UNSPECIFIED:
        result = desired;
        break;
    case MeasureSpec.AT_MOST:
        result = Math.max(specSize, desired);
        break;
    case MeasureSpec.EXACTLY:
    default:
        result = specSize;
    }
    return result;
}

@Override
protected int getSuggestedMinimumWidth() {
    // View should be large enough to contain 3 side-by-side target bitmaps
    return 3 * mBitmapWidth;
}

@Override
protected int getSuggestedMinimumHeight() {
    // View should be large enough to contain 3 side-by-side target bitmaps
    return 3 * mBitmapWidth;
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    final int minimumWidth = getSuggestedMinimumWidth();
    final int minimumHeight = getSuggestedMinimumHeight();
    int viewWidth = resolveMeasured(widthMeasureSpec, minimumWidth);
    int viewHeight = resolveMeasured(heightMeasureSpec, minimumHeight);

    switch (mAspect) {
    case ASPECT_SQUARE:
        viewWidth = viewHeight = Math.min(viewWidth, viewHeight);
        break;
    case ASPECT_LOCK_WIDTH:
        viewHeight = Math.min(viewWidth, viewHeight);
        break;
    case ASPECT_LOCK_HEIGHT:
        viewWidth = Math.min(viewWidth, viewHeight);
        break;
    }
    // Log.v(TAG, "LockPatternView dimensions: " + viewWidth + "x" +
    // viewHeight);
    setMeasuredDimension(viewWidth, viewHeight);
}

/**
 * Determines whether the point x, y will add a new point to the current
 * pattern (in addition to finding the cell, also makes heuristic choices
 * such as filling in gaps based on current pattern).
 * 
 * @param x
 *            The x coordinate.
 * @param y
 *            The y coordinate.
 */
private Cell detectAndAddHit(float x, float y) {
    final Cell cell = checkForNewHit(x, y);
    if (cell != null) {

        // check for gaps in existing pattern
        Cell fillInGapCell = null;
        final ArrayList<Cell> pattern = mPattern;
        if (!pattern.isEmpty()) {
            final Cell lastCell = pattern.get(pattern.size() - 1);
            int dRow = cell.row - lastCell.row;
            int dColumn = cell.column - lastCell.column;

            int fillInRow = lastCell.row;
            int fillInColumn = lastCell.column;

            if (Math.abs(dRow) == 2 && Math.abs(dColumn) != 1) {
                fillInRow = lastCell.row + ((dRow > 0) ? 1 : -1);
            }

            if (Math.abs(dColumn) == 2 && Math.abs(dRow) != 1) {
                fillInColumn = lastCell.column + ((dColumn > 0) ? 1 : -1);
            }

            fillInGapCell = Cell.of(fillInRow, fillInColumn);
        }

        if (fillInGapCell != null && !mPatternDrawLookup[fillInGapCell.row][fillInGapCell.column]) {
            addCellToPattern(fillInGapCell);
        }
        addCellToPattern(cell);
        if (mEnableHapticFeedback) {
            performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY,
                    HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING
                            | HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING);
        }
        return cell;
    }
    return null;
}

private void addCellToPattern(Cell newCell) {
    mPatternDrawLookup[newCell.getRow()][newCell.getColumn()] = true;
    mPattern.add(newCell);
    notifyCellAdded();
}

// helper method to find which cell a point maps to
private Cell checkForNewHit(float x, float y) {

    final int rowHit = getRowHit(y);
    if (rowHit < 0) {
        return null;
    }
    final int columnHit = getColumnHit(x);
    if (columnHit < 0) {
        return null;
    }

    if (mPatternDrawLookup[rowHit][columnHit]) {
        return null;
    }
    return Cell.of(rowHit, columnHit);
}

/**
 * Helper method to find the row that y falls into.
 * 
 * @param y
 *            The y coordinate
 * @return The row that y falls in, or -1 if it falls in no row.
 */
private int getRowHit(float y) {

    final float squareHeight = mSquareHeight;
    float hitSize = squareHeight * mHitFactor;

    float offset = getPaddingTop() + (squareHeight - hitSize) / 2f;
    for (int i = 0; i < 3; i++) {

        final float hitTop = offset + squareHeight * i;
        if (y >= hitTop && y <= hitTop + hitSize) {
            return i;
        }
    }
    return -1;
}

/**
 * Helper method to find the column x fallis into.
 * 
 * @param x
 *            The x coordinate.
 * @return The column that x falls in, or -1 if it falls in no column.
 */
private int getColumnHit(float x) {
    final float squareWidth = mSquareWidth;
    float hitSize = squareWidth * mHitFactor;

    float offset = getPaddingLeft() + (squareWidth - hitSize) / 2f;
    for (int i = 0; i < 3; i++) {

        final float hitLeft = offset + squareWidth * i;
        if (x >= hitLeft && x <= hitLeft + hitSize) {
            return i;
        }
    }
    return -1;
}

@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
@SuppressLint("NewApi")
@Override
public boolean onHoverEvent(MotionEvent event) {
    AccessibilityManager accessibilityManager = (AccessibilityManager) getContext()
            .getSystemService(Context.ACCESSIBILITY_SERVICE);
    if (accessibilityManager.isTouchExplorationEnabled()) {
        final int action = event.getAction();
        switch (action) {
        case MotionEvent.ACTION_HOVER_ENTER:
            event.setAction(MotionEvent.ACTION_DOWN);
            break;
        case MotionEvent.ACTION_HOVER_MOVE:
            event.setAction(MotionEvent.ACTION_MOVE);
            break;
        case MotionEvent.ACTION_HOVER_EXIT:
            event.setAction(MotionEvent.ACTION_UP);
            break;
        }
        onTouchEvent(event);
        event.setAction(action);
    }
    return super.onHoverEvent(event);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
    if (!mInputEnabled || !isEnabled()) {
        return false;
    }

    switch (event.getAction()) {
    case MotionEvent.ACTION_DOWN:
        handleActionDown(event);
        return true;
    case MotionEvent.ACTION_UP:
        handleActionUp(event);
        return true;
    case MotionEvent.ACTION_MOVE:
        handleActionMove(event);
        return true;
    case MotionEvent.ACTION_CANCEL:
        if (mPatternInProgress) {
            mPatternInProgress = false;
            resetPattern();
            notifyPatternCleared();
        }
        if (PROFILE_DRAWING) {
            if (mDrawingProfilingStarted) {
                Debug.stopMethodTracing();
                mDrawingProfilingStarted = false;
            }
        }
        return true;
    }
    return false;
}

@SuppressLint("NewApi")
private void handleActionMove(MotionEvent event) {
    // Handle all recent motion events so we don't skip any cells even when
    // the device
    // is busy...
    final float radius = (mSquareWidth * mDiameterFactor * 0.5f);
    final int historySize = event.getHistorySize();
    mTmpInvalidateRect.setEmpty();
    boolean invalidateNow = false;
    for (int i = 0; i < historySize + 1; i++) {
        final float x = i < historySize ? event.getHistoricalX(i) : event.getX();
        final float y = i < historySize ? event.getHistoricalY(i) : event.getY();
        Cell hitCell = detectAndAddHit(x, y);
        final int patternSize = mPattern.size();
        if (hitCell != null && patternSize == 1) {
            mPatternInProgress = true;
            notifyPatternStarted();
        }
        // note current x and y for rubber banding of in progress patterns
        final float dx = Math.abs(x - mInProgressX);
        final float dy = Math.abs(y - mInProgressY);
        if (dx > DRAG_THRESHHOLD || dy > DRAG_THRESHHOLD) {
            invalidateNow = true;
        }

        if (mPatternInProgress && patternSize > 0) {
            final ArrayList<Cell> pattern = mPattern;
            final Cell lastCell = pattern.get(patternSize - 1);
            float lastCellCenterX = getCenterXForColumn(lastCell.column);
            float lastCellCenterY = getCenterYForRow(lastCell.row);

            // Adjust for drawn segment from last cell to (x,y). Radius
            // accounts for line width.
            float left = Math.min(lastCellCenterX, x) - radius;
            float right = Math.max(lastCellCenterX, x) + radius;
            float top = Math.min(lastCellCenterY, y) - radius;
            float bottom = Math.max(lastCellCenterY, y) + radius;

            // Invalidate between the pattern's new cell and the pattern's
            // previous cell
            if (hitCell != null) {
                final float width = mSquareWidth * 0.5f;
                final float height = mSquareHeight * 0.5f;
                final float hitCellCenterX = getCenterXForColumn(hitCell.column);
                final float hitCellCenterY = getCenterYForRow(hitCell.row);

                left = Math.min(hitCellCenterX - width, left);
                right = Math.max(hitCellCenterX + width, right);
                top = Math.min(hitCellCenterY - height, top);
                bottom = Math.max(hitCellCenterY + height, bottom);
            }

            // Invalidate between the pattern's last cell and the previous
            // location
            mTmpInvalidateRect.union(Math.round(left), Math.round(top), Math.round(right), Math.round(bottom));
        }
    }
    mInProgressX = event.getX();
    mInProgressY = event.getY();

    // To save updates, we only invalidate if the user moved beyond a
    // certain amount.
    if (invalidateNow) {
        mInvalidate.union(mTmpInvalidateRect);
        invalidate(mInvalidate);
        mInvalidate.set(mTmpInvalidateRect);
    }
}

@SuppressLint("NewApi")
private void sendAccessEvent(int resId) {
    announceForAccessibility(getContext().getString(resId));
}

private void handleActionUp(MotionEvent event) {
    // report pattern detected
    if (!mPattern.isEmpty()) {
        mPatternInProgress = false;
        notifyPatternDetected();
        invalidate();
    }
    if (PROFILE_DRAWING) {
        if (mDrawingProfilingStarted) {
            Debug.stopMethodTracing();
            mDrawingProfilingStarted = false;
        }
    }
}

private void handleActionDown(MotionEvent event) {
    resetPattern();
    final float x = event.getX();
    final float y = event.getY();
    final Cell hitCell = detectAndAddHit(x, y);
    if (hitCell != null) {
        mPatternInProgress = true;
        mPatternDisplayMode = DisplayMode.Correct;
        notifyPatternStarted();
    } else if (mPatternInProgress) {
        mPatternInProgress = false;
        notifyPatternCleared();
    }
    if (hitCell != null) {
        final float startX = getCenterXForColumn(hitCell.column);
        final float startY = getCenterYForRow(hitCell.row);

        final float widthOffset = mSquareWidth / 2f;
        final float heightOffset = mSquareHeight / 2f;

        invalidate((int) (startX - widthOffset), (int) (startY - heightOffset), (int) (startX + widthOffset),
                (int) (startY + heightOffset));
    }
    mInProgressX = x;
    mInProgressY = y;
    if (PROFILE_DRAWING) {
        if (!mDrawingProfilingStarted) {
            Debug.startMethodTracing("LockPatternDrawing");
            mDrawingProfilingStarted = true;
        }
    }
}

private float getCenterXForColumn(int column) {
    return getPaddingLeft() + column * mSquareWidth + mSquareWidth / 2f;
}

private float getCenterYForRow(int row) {
    return getPaddingTop() + row * mSquareHeight + mSquareHeight / 2f;
}

@Override
protected void onDraw(Canvas canvas) {
    final ArrayList<Cell> pattern = mPattern;
    final int count = pattern.size();
    final boolean[][] drawLookup = mPatternDrawLookup;

    if (mPatternDisplayMode == DisplayMode.Animate) {

        // figure out which circles to draw

        // + 1 so we pause on complete pattern
        final int oneCycle = (count + 1) * MILLIS_PER_CIRCLE_ANIMATING;
        final int spotInCycle = (int) (SystemClock.elapsedRealtime() - mAnimatingPeriodStart) % oneCycle;
        final int numCircles = spotInCycle / MILLIS_PER_CIRCLE_ANIMATING;

        clearPatternDrawLookup();
        for (int i = 0; i < numCircles; i++) {
            final Cell cell = pattern.get(i);
            drawLookup[cell.getRow()][cell.getColumn()] = true;
        }

        // figure out in progress portion of ghosting line

        final boolean needToUpdateInProgressPoint = numCircles > 0 && numCircles < count;

        if (needToUpdateInProgressPoint) {
            final float percentageOfNextCircle = ((float) (spotInCycle % MILLIS_PER_CIRCLE_ANIMATING))
                    / MILLIS_PER_CIRCLE_ANIMATING;

            final Cell currentCell = pattern.get(numCircles - 1);
            final float centerX = getCenterXForColumn(currentCell.column);
            final float centerY = getCenterYForRow(currentCell.row);

            final Cell nextCell = pattern.get(numCircles);
            final float dx = percentageOfNextCircle * (getCenterXForColumn(nextCell.column) - centerX);
            final float dy = percentageOfNextCircle * (getCenterYForRow(nextCell.row) - centerY);
            mInProgressX = centerX + dx;
            mInProgressY = centerY + dy;
        }
        // TODO: Infinite loop here...
        invalidate();
    }

    final float squareWidth = mSquareWidth;
    final float squareHeight = mSquareHeight;

    float radius = (squareWidth * mDiameterFactor * 0.5f);
    mPathPaint.setStrokeWidth(radius);

    final Path currentPath = mCurrentPath;
    currentPath.rewind();

    // draw the circles
    final int paddingTop = getPaddingTop();
    final int paddingLeft = getPaddingLeft();

    for (int i = 0; i < 3; i++) {
        float topY = paddingTop + i * squareHeight;
        // float centerY = getPaddingTop() + i * mSquareHeight +
        // (mSquareHeight / 2);
        for (int j = 0; j < 3; j++) {
            float leftX = paddingLeft + j * squareWidth;
            drawCircle(canvas, (int) leftX, (int) topY, drawLookup[i][j]);
        }
    }

    // TODO: the path should be created and cached every time we hit-detect
    // a cell
    // only the last segment of the path should be computed here
    // draw the path of the pattern (unless the user is in progress, and
    // we are in stealth mode)
    final boolean drawPath = (!mInStealthMode || mPatternDisplayMode == DisplayMode.Wrong);

    // draw the arrows associated with the path (unless the user is in
    // progress, and
    // we are in stealth mode)
    boolean oldFlag = (mPaint.getFlags() & Paint.FILTER_BITMAP_FLAG) != 0;
    mPaint.setFilterBitmap(true); // draw with higher quality since we
                                    // render with transforms
    if (drawPath) {
        for (int i = 0; i < count - 1; i++) {
            Cell cell = pattern.get(i);
            Cell next = pattern.get(i + 1);

            // only draw the part of the pattern stored in
            // the lookup table (this is only different in the case
            // of animation).
            if (!drawLookup[next.row][next.column]) {
                break;
            }

            float leftX = paddingLeft + cell.column * squareWidth;
            float topY = paddingTop + cell.row * squareHeight;

            drawArrow(canvas, leftX, topY, cell, next);
        }
    }

    if (drawPath) {
        boolean anyCircles = false;
        for (int i = 0; i < count; i++) {
            Cell cell = pattern.get(i);

            // only draw the part of the pattern stored in
            // the lookup table (this is only different in the case
            // of animation).
            if (!drawLookup[cell.row][cell.column]) {
                break;
            }
            anyCircles = true;

            float centerX = getCenterXForColumn(cell.column);
            float centerY = getCenterYForRow(cell.row);
            if (i == 0) {
                currentPath.moveTo(centerX, centerY);
            } else {
                currentPath.lineTo(centerX, centerY);
            }
        }

        // add last in progress section
        if ((mPatternInProgress || mPatternDisplayMode == DisplayMode.Animate) && anyCircles) {
            currentPath.lineTo(mInProgressX, mInProgressY);
        }
        canvas.drawPath(currentPath, mPathPaint);
    }

    mPaint.setFilterBitmap(oldFlag); // restore default flag
}

private void drawArrow(Canvas canvas, float leftX, float topY, Cell start, Cell end) {
    boolean green = mPatternDisplayMode != DisplayMode.Wrong;

    final int endRow = end.row;
    final int startRow = start.row;
    final int endColumn = end.column;
    final int startColumn = start.column;

    // offsets for centering the bitmap in the cell
    final int offsetX = ((int) mSquareWidth - mBitmapWidth) / 2;
    final int offsetY = ((int) mSquareHeight - mBitmapHeight) / 2;

    // compute transform to place arrow bitmaps at correct angle inside
    // circle.
    // This assumes that the arrow image is drawn at 12:00 with it's top
    // edge
    // coincident with the circle bitmap's top edge.
    Bitmap arrow = green ? mBitmapArrowGreenUp : mBitmapArrowRedUp;
    final int cellWidth = mBitmapWidth;
    final int cellHeight = mBitmapHeight;

    // the up arrow bitmap is at 12:00, so find the rotation from x axis and
    // add 90 degrees.
    final float theta = (float) Math.atan2((double) (endRow - startRow), (double) (endColumn - startColumn));
    final float angle = (float) Math.toDegrees(theta) + 90.0f;

    // compose matrix
    float sx = Math.min(mSquareWidth / mBitmapWidth, 1.0f);
    float sy = Math.min(mSquareHeight / mBitmapHeight, 1.0f);
    mArrowMatrix.setTranslate(leftX + offsetX, topY + offsetY); // transform
                                                                // to cell
                                                                // position
    mArrowMatrix.preTranslate(mBitmapWidth / 2, mBitmapHeight / 2);
    mArrowMatrix.preScale(sx, sy);
    mArrowMatrix.preTranslate(-mBitmapWidth / 2, -mBitmapHeight / 2);
    mArrowMatrix.preRotate(angle, cellWidth / 2.0f, cellHeight / 2.0f); // rotate
                                                                        // about
                                                                        // cell
                                                                        // center
    mArrowMatrix.preTranslate((cellWidth - arrow.getWidth()) / 2.0f, 0.0f); // translate
                                                                            // to
                                                                            // 12:00
                                                                            // pos
    canvas.drawBitmap(arrow, mArrowMatrix, mPaint);
}

/**
 * @param canvas
 * @param leftX
 * @param topY
 * @param partOfPattern
 *            Whether this circle is part of the pattern.
 */
private void drawCircle(Canvas canvas, int leftX, int topY, boolean partOfPattern) {
    Bitmap outerCircle;
    Bitmap innerCircle;

    if (!partOfPattern || (mInStealthMode && mPatternDisplayMode != DisplayMode.Wrong)) {
        // unselected circle
        outerCircle = mBitmapCircleDefault;
        innerCircle = mBitmapBtnDefault;
    } else if (mPatternInProgress) {
        // user is in middle of drawing a pattern
        outerCircle = mBitmapCircleGreen;
        innerCircle = mBitmapBtnTouched;
    } else if (mPatternDisplayMode == DisplayMode.Wrong) {
        // the pattern is wrong
        outerCircle = mBitmapCircleRed;
        innerCircle = mBitmapBtnDefault;
    } else if (mPatternDisplayMode == DisplayMode.Correct || mPatternDisplayMode == DisplayMode.Animate) {
        // the pattern is correct
        outerCircle = mBitmapCircleGreen;
        innerCircle = mBitmapBtnDefault;
    } else {
        throw new IllegalStateException("unknown display mode " + mPatternDisplayMode);
    }

    final int width = mBitmapWidth;
    final int height = mBitmapHeight;

    final float squareWidth = mSquareWidth;
    final float squareHeight = mSquareHeight;

    int offsetX = (int) ((squareWidth - width) / 2f);
    int offsetY = (int) ((squareHeight - height) / 2f);

    // Allow circles to shrink if the view is too small to hold them.
    float sx = Math.min(mSquareWidth / mBitmapWidth, 1.0f);
    float sy = Math.min(mSquareHeight / mBitmapHeight, 1.0f);

    mCircleMatrix.setTranslate(leftX + offsetX, topY + offsetY);
    mCircleMatrix.preTranslate(mBitmapWidth / 2, mBitmapHeight / 2);
    mCircleMatrix.preScale(sx, sy);
    mCircleMatrix.preTranslate(-mBitmapWidth / 2, -mBitmapHeight / 2);

    canvas.drawBitmap(outerCircle, mCircleMatrix, mPaint);
    canvas.drawBitmap(innerCircle, mCircleMatrix, mPaint);
}

@Override
protected Parcelable onSaveInstanceState() {
    Parcelable superState = super.onSaveInstanceState();
    return new SavedState(superState, patternToString(mPattern), mPatternDisplayMode.ordinal(), mInputEnabled,
            mInStealthMode, mEnableHapticFeedback);
}

@Override
protected void onRestoreInstanceState(Parcelable state) {
    final SavedState ss = (SavedState) state;
    super.onRestoreInstanceState(ss.getSuperState());
    setPattern(DisplayMode.Correct, stringToPattern(ss.getSerializedPattern()));
    mPatternDisplayMode = DisplayMode.values()[ss.getDisplayMode()];
    mInputEnabled = ss.isInputEnabled();
    mInStealthMode = ss.isInStealthMode();
    mEnableHapticFeedback = ss.isTactileFeedbackEnabled();
}

/**
 * Deserialize a pattern.
 * 
 * @param string
 *            The pattern serialized with {@link #patternToString}
 * @return The pattern.
 */
public static List<LockPatternView.Cell> stringToPattern(String string) {
    List<LockPatternView.Cell> result = new ArrayList<LockPatternView.Cell>();

    final byte[] bytes = string.getBytes();
    for (int i = 0; i < bytes.length; i++) {
        byte b = bytes[i];
        result.add(LockPatternView.Cell.of(b / 3, b % 3));
    }
    return result;
}

/**
 * Serialize a pattern.
 * 
 * @param pattern
 *            The pattern.
 * @return The pattern in string form.
 */
public static String patternToString(List<LockPatternView.Cell> pattern) {
    if (pattern == null) {
        return "";
    }
    final int patternSize = pattern.size();

    byte[] res = new byte[patternSize];
    for (int i = 0; i < patternSize; i++) {
        LockPatternView.Cell cell = pattern.get(i);
        res[i] = (byte) (cell.getRow() * 3 + cell.getColumn());
    }
    return new String(res);
}

/**
 * The parecelable for saving and restoring a lock pattern view.
 */
private static class SavedState extends BaseSavedState {

    private final String mSerializedPattern;
    private final int mDisplayMode;
    private final boolean mInputEnabled;
    private final boolean mInStealthMode;
    private final boolean mTactileFeedbackEnabled;

    /**
     * Constructor called from {@link LockPatternView#onSaveInstanceState()}
     */
    private SavedState(Parcelable superState, String serializedPattern, int displayMode, boolean inputEnabled,
            boolean inStealthMode, boolean tactileFeedbackEnabled) {
        super(superState);
        mSerializedPattern = serializedPattern;
        mDisplayMode = displayMode;
        mInputEnabled = inputEnabled;
        mInStealthMode = inStealthMode;
        mTactileFeedbackEnabled = tactileFeedbackEnabled;
    }

    /**
     * Constructor called from {@link #CREATOR}
     */
    private SavedState(Parcel in) {
        super(in);
        mSerializedPattern = in.readString();
        mDisplayMode = in.readInt();
        mInputEnabled = (Boolean) in.readValue(null);
        mInStealthMode = (Boolean) in.readValue(null);
        mTactileFeedbackEnabled = (Boolean) in.readValue(null);
    }

    public String getSerializedPattern() {
        return mSerializedPattern;
    }

    public int getDisplayMode() {
        return mDisplayMode;
    }

    public boolean isInputEnabled() {
        return mInputEnabled;
    }

    public boolean isInStealthMode() {
        return mInStealthMode;
    }

    public boolean isTactileFeedbackEnabled() {
        return mTactileFeedbackEnabled;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        super.writeToParcel(dest, flags);
        dest.writeString(mSerializedPattern);
        dest.writeInt(mDisplayMode);
        dest.writeValue(mInputEnabled);
        dest.writeValue(mInStealthMode);
        dest.writeValue(mTactileFeedbackEnabled);
    }

    public static final Parcelable.Creator<SavedState> CREATOR = new Creator<SavedState>() {
        public SavedState createFromParcel(Parcel in) {
            return new SavedState(in);
        }

        public SavedState[] newArray(int size) {
            return new SavedState[size];
        }
    };
}

}
dit

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值