1122 lines
40 KiB
Java
1122 lines
40 KiB
Java
/*
|
|
* Copyright (C) 2007 The Android Open Source Project
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
package com.hippo.widget.lockpattern;
|
|
|
|
import android.animation.Animator;
|
|
import android.animation.AnimatorListenerAdapter;
|
|
import android.animation.ValueAnimator;
|
|
import android.annotation.SuppressLint;
|
|
import android.content.Context;
|
|
import android.graphics.Canvas;
|
|
import android.graphics.Paint;
|
|
import android.graphics.Path;
|
|
import android.graphics.Rect;
|
|
import android.os.Debug;
|
|
import android.os.Parcel;
|
|
import android.os.Parcelable;
|
|
import android.os.SystemClock;
|
|
import android.util.AttributeSet;
|
|
import android.view.HapticFeedbackConstants;
|
|
import android.view.MotionEvent;
|
|
import android.view.View;
|
|
import android.view.accessibility.AccessibilityManager;
|
|
import android.view.animation.Interpolator;
|
|
|
|
import androidx.annotation.NonNull;
|
|
import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
|
|
import androidx.interpolator.view.animation.LinearOutSlowInInterpolator;
|
|
|
|
import com.hippo.ehviewer.R;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
|
|
/**
|
|
* Displays and detects the user's unlock attempt, which is a drag of a finger
|
|
* across 9 regions of the screen.
|
|
* <p>
|
|
* Is also capable of displaying a static pattern in "in progress", "wrong" or
|
|
* "correct" states.
|
|
*/
|
|
@SuppressWarnings("IntegerDivisionInFloatingPointContext")
|
|
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)
|
|
private static final boolean PROFILE_DRAWING = false;
|
|
/**
|
|
* 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_THRESHOLD = 0.0f;
|
|
private final Cell[][] mCells;
|
|
private final CellState[][] mCellStates;
|
|
private final int mDotSize;
|
|
private final int mDotSizeActivated;
|
|
private final int mPathWidth;
|
|
private final Paint mPaint = new Paint();
|
|
private final Paint mPathPaint = new Paint();
|
|
private final ArrayList<Cell> mPattern = new ArrayList<>(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 final boolean[][] mPatternDrawLookup = new boolean[3][3];
|
|
private final float mHitFactor = 0.6f;
|
|
private final Path mCurrentPath = new Path();
|
|
private final Rect mInvalidate = new Rect();
|
|
private final Rect mTmpInvalidateRect = new Rect();
|
|
private final int mAspect;
|
|
private final int mRegularColor;
|
|
private final int mErrorColor;
|
|
private final int mSuccessColor;
|
|
private final Interpolator mFastOutSlowInInterpolator;
|
|
private final Interpolator mLinearOutSlowInInterpolator;
|
|
private boolean mDrawingProfilingStarted = false;
|
|
private OnPatternListener mOnPatternListener;
|
|
/**
|
|
* 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 mPatternInProgress = false;
|
|
private float mSquareWidth;
|
|
private float mSquareHeight;
|
|
|
|
public LockPatternView(Context context) {
|
|
this(context, null);
|
|
}
|
|
|
|
public LockPatternView(Context context, AttributeSet attrs) {
|
|
super(context, attrs);
|
|
|
|
//TypedArray a = context.obtainStyledAttributes(attrs, 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);
|
|
|
|
mRegularColor = getResources().getColor(R.color.lock_pattern_view_regular_color);
|
|
mErrorColor = getResources().getColor(R.color.lock_pattern_view_error_color);
|
|
mSuccessColor = getResources().getColor(R.color.lock_pattern_view_success_color);
|
|
|
|
mPathPaint.setColor(mRegularColor);
|
|
|
|
mPathPaint.setStyle(Paint.Style.STROKE);
|
|
mPathPaint.setStrokeJoin(Paint.Join.ROUND);
|
|
mPathPaint.setStrokeCap(Paint.Cap.ROUND);
|
|
|
|
mPathWidth = getResources().getDimensionPixelSize(R.dimen.lock_pattern_dot_line_width);
|
|
mPathPaint.setStrokeWidth(mPathWidth);
|
|
|
|
mDotSize = getResources().getDimensionPixelSize(R.dimen.lock_pattern_dot_size);
|
|
mDotSizeActivated = getResources().getDimensionPixelSize(
|
|
R.dimen.lock_pattern_dot_size_activated);
|
|
|
|
mPaint.setAntiAlias(true);
|
|
mPaint.setDither(true);
|
|
|
|
mCellStates = new CellState[3][3];
|
|
for (int i = 0; i < 3; i++) {
|
|
for (int j = 0; j < 3; j++) {
|
|
mCellStates[i][j] = new CellState();
|
|
mCellStates[i][j].radius = mDotSize / 2;
|
|
mCellStates[i][j].row = i;
|
|
mCellStates[i][j].col = j;
|
|
}
|
|
}
|
|
mCells = new Cell[3][3];
|
|
for (int i = 0; i < 3; i++) {
|
|
for (int j = 0; j < 3; j++) {
|
|
mCells[i][j] = Cell.of(i, j);
|
|
}
|
|
}
|
|
|
|
mFastOutSlowInInterpolator = new FastOutSlowInInterpolator();
|
|
mLinearOutSlowInInterpolator = new LinearOutSlowInInterpolator();
|
|
}
|
|
|
|
/**
|
|
* Returns the greatest common divisor of {@code a, b}. Returns {@code 0} if
|
|
* {@code a == 0 && b == 0}.
|
|
*
|
|
* @throws IllegalArgumentException if {@code a < 0} or {@code b < 0}
|
|
*/
|
|
private static int gcd(int a, int b) {
|
|
|
|
/*
|
|
* The reason we require both arguments to be >= 0 is because otherwise, what do you return
|
|
* on gcd(0, Integer.MIN_VALUE)? BigInteger.gcd would return positive 2^31, but positive
|
|
* 2^31 isn't an int.
|
|
*/
|
|
//checkNonNegative("a", a);
|
|
if (a < 0) {
|
|
throw new IllegalArgumentException("a (" + a + ") must be >= 0");
|
|
}
|
|
//checkNonNegative("b", b);
|
|
if (b < 0) {
|
|
throw new IllegalArgumentException("b (" + b + ") must be >= 0");
|
|
}
|
|
|
|
if (a == 0) {
|
|
// 0 % b == 0, so b divides a, but the converse doesn't hold.
|
|
// BigInteger.gcd is consistent with this decision.
|
|
return b;
|
|
} else if (b == 0) {
|
|
return a; // similar logic
|
|
}
|
|
/*
|
|
* Uses the binary GCD algorithm; see http://en.wikipedia.org/wiki/Binary_GCD_algorithm.
|
|
* This is >40% faster than the Euclidean algorithm in benchmarks.
|
|
*/
|
|
int aTwos = Integer.numberOfTrailingZeros(a);
|
|
a >>= aTwos; // divide out all 2s
|
|
int bTwos = Integer.numberOfTrailingZeros(b);
|
|
b >>= bTwos; // divide out all 2s
|
|
while (a != b) { // both a, b are odd
|
|
|
|
// The key to the binary GCD algorithm is as follows:
|
|
// Both a and b are odd. Assume a > b; then gcd(a - b, b) = gcd(a, b).
|
|
// But in gcd(a - b, b), a - b is even and b is odd, so we can divide out powers of two.
|
|
|
|
// We bend over backwards to avoid branching, adapting a technique from
|
|
// http://graphics.stanford.edu/~seander/bithacks.html#IntegerMinOrMax
|
|
|
|
int delta = a - b; // can't overflow, since a and b are nonnegative
|
|
|
|
int minDeltaOrZero = delta & (delta >> (Integer.SIZE - 1));
|
|
// equivalent to Math.min(delta, 0)
|
|
|
|
a = delta - minDeltaOrZero - minDeltaOrZero; // sets a to Math.abs(a - b)
|
|
// a is now nonnegative and even
|
|
|
|
b += minDeltaOrZero; // sets b to min(old a, b)
|
|
a >>= Integer.numberOfTrailingZeros(a); // divide out all 2s, since 2 doesn't divide b
|
|
}
|
|
return a << Math.min(aTwos, bTwos);
|
|
}
|
|
|
|
public int getCellSize() {
|
|
return mPattern.size();
|
|
}
|
|
|
|
public String getPatternString() {
|
|
return LockPatternUtils.patternToString(mPattern);
|
|
}
|
|
|
|
/**
|
|
* Set the call back for pattern detection.
|
|
*
|
|
* @param onPatternListener The call back.
|
|
*/
|
|
public void setOnPatternListener(
|
|
OnPatternListener onPatternListener) {
|
|
mOnPatternListener = onPatternListener;
|
|
}
|
|
|
|
/**
|
|
* Set the pattern explicitly (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() {
|
|
if (mOnPatternListener != null) {
|
|
mOnPatternListener.onPatternCellAdded(mPattern);
|
|
}
|
|
}
|
|
|
|
private void notifyPatternStarted() {
|
|
if (mOnPatternListener != null) {
|
|
mOnPatternListener.onPatternStart();
|
|
}
|
|
}
|
|
|
|
private void notifyPatternDetected() {
|
|
if (mOnPatternListener != null) {
|
|
mOnPatternListener.onPatternDetected(mPattern);
|
|
}
|
|
}
|
|
|
|
private void notifyPatternCleared() {
|
|
if (mOnPatternListener != null) {
|
|
mOnPatternListener.onPatternCleared();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
}
|
|
}
|
|
|
|
@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;
|
|
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 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);
|
|
}
|
|
|
|
// From
|
|
// https://github.com/google/guava/blob/c462d69329709f72a17a64cb229d15e76e72199c/guava/src/com/google/common/math/IntMath.java
|
|
|
|
/**
|
|
* 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
|
|
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 dGcd = gcd(Math.abs(dRow), Math.abs(dColumn));
|
|
if (dGcd > 0) {
|
|
int fillInRow = lastCell.row;
|
|
int fillInColumn = lastCell.column;
|
|
int fillInRowStep = dRow / dGcd;
|
|
int fillInColumnStep = dColumn / dGcd;
|
|
for (int i = 1; i < dGcd; ++i) {
|
|
fillInRow += fillInRowStep;
|
|
fillInColumn += fillInColumnStep;
|
|
if (!mPatternDrawLookup[fillInRow][fillInColumn]) {
|
|
addCellToPattern(mCells[fillInRow][fillInColumn]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
addCellToPattern(cell);
|
|
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);
|
|
if (!mInStealthMode) {
|
|
startCellActivatedAnimation(newCell);
|
|
}
|
|
notifyCellAdded();
|
|
}
|
|
|
|
private void startCellActivatedAnimation(Cell cell) {
|
|
final CellState cellState = mCellStates[cell.row][cell.column];
|
|
startRadiusAnimation(mDotSize / 2, mDotSizeActivated / 2, 96, mLinearOutSlowInInterpolator,
|
|
cellState, () -> startRadiusAnimation(mDotSizeActivated / 2, mDotSize / 2, 192,
|
|
mFastOutSlowInInterpolator,
|
|
cellState, null));
|
|
startLineEndAnimation(cellState, mInProgressX, mInProgressY,
|
|
getCenterXForColumn(cell.column), getCenterYForRow(cell.row));
|
|
}
|
|
|
|
private void startLineEndAnimation(final CellState state,
|
|
final float startX, final float startY, final float targetX, final float targetY) {
|
|
ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1);
|
|
valueAnimator.addUpdateListener(animation -> {
|
|
float t = (float) animation.getAnimatedValue();
|
|
state.lineEndX = (1 - t) * startX + t * targetX;
|
|
state.lineEndY = (1 - t) * startY + t * targetY;
|
|
invalidate();
|
|
});
|
|
valueAnimator.addListener(new AnimatorListenerAdapter() {
|
|
@Override
|
|
public void onAnimationEnd(Animator animation) {
|
|
state.lineAnimator = null;
|
|
}
|
|
});
|
|
valueAnimator.setInterpolator(mFastOutSlowInInterpolator);
|
|
valueAnimator.setDuration(100);
|
|
valueAnimator.start();
|
|
state.lineAnimator = valueAnimator;
|
|
}
|
|
|
|
private void startRadiusAnimation(float start, float end, long duration,
|
|
Interpolator interpolator, final CellState state,
|
|
final Runnable endRunnable) {
|
|
ValueAnimator valueAnimator = ValueAnimator.ofFloat(start, end);
|
|
valueAnimator.addUpdateListener(animation -> {
|
|
state.radius = (float) animation.getAnimatedValue();
|
|
invalidate();
|
|
});
|
|
if (endRunnable != null) {
|
|
valueAnimator.addListener(new AnimatorListenerAdapter() {
|
|
@Override
|
|
public void onAnimationEnd(Animator animation) {
|
|
endRunnable.run();
|
|
}
|
|
});
|
|
}
|
|
valueAnimator.setInterpolator(interpolator);
|
|
valueAnimator.setDuration(duration);
|
|
valueAnimator.start();
|
|
}
|
|
|
|
// 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 mCells[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;
|
|
}
|
|
|
|
@Override
|
|
public boolean onHoverEvent(@NonNull 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);
|
|
}
|
|
|
|
@SuppressLint("ClickableViewAccessibility")
|
|
@Override
|
|
public boolean onTouchEvent(@NonNull MotionEvent event) {
|
|
if (!mInputEnabled || !isEnabled()) {
|
|
return false;
|
|
}
|
|
|
|
switch (event.getAction()) {
|
|
case MotionEvent.ACTION_DOWN:
|
|
handleActionDown(event);
|
|
return true;
|
|
case MotionEvent.ACTION_UP:
|
|
handleActionUp();
|
|
return true;
|
|
case MotionEvent.ACTION_MOVE:
|
|
handleActionMove(event);
|
|
return true;
|
|
case MotionEvent.ACTION_CANCEL:
|
|
if (mPatternInProgress) {
|
|
setPatternInProgress(false);
|
|
resetPattern();
|
|
notifyPatternCleared();
|
|
}
|
|
if (PROFILE_DRAWING) {
|
|
if (mDrawingProfilingStarted) {
|
|
Debug.stopMethodTracing();
|
|
mDrawingProfilingStarted = false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private void setPatternInProgress(boolean progress) {
|
|
mPatternInProgress = progress;
|
|
}
|
|
|
|
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 = mPathWidth;
|
|
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) {
|
|
setPatternInProgress(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_THRESHOLD || dy > DRAG_THRESHOLD) {
|
|
invalidateNow = true;
|
|
}
|
|
|
|
if (mPatternInProgress && patternSize > 0) {
|
|
final Cell lastCell = mPattern.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);
|
|
}
|
|
}
|
|
|
|
private void handleActionUp() {
|
|
// report pattern detected
|
|
if (!mPattern.isEmpty()) {
|
|
setPatternInProgress(false);
|
|
cancelLineAnimations();
|
|
notifyPatternDetected();
|
|
invalidate();
|
|
}
|
|
if (PROFILE_DRAWING) {
|
|
if (mDrawingProfilingStarted) {
|
|
Debug.stopMethodTracing();
|
|
mDrawingProfilingStarted = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void cancelLineAnimations() {
|
|
for (int i = 0; i < 3; i++) {
|
|
for (int j = 0; j < 3; j++) {
|
|
CellState state = mCellStates[i][j];
|
|
if (state.lineAnimator != null) {
|
|
state.lineAnimator.cancel();
|
|
state.lineEndX = Float.MIN_VALUE;
|
|
state.lineEndY = Float.MIN_VALUE;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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) {
|
|
setPatternInProgress(true);
|
|
mPatternDisplayMode = DisplayMode.Correct;
|
|
notifyPatternStarted();
|
|
} else if (mPatternInProgress) {
|
|
setPatternInProgress(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 Path currentPath = mCurrentPath;
|
|
currentPath.rewind();
|
|
|
|
// draw the circles
|
|
for (int i = 0; i < 3; i++) {
|
|
float centerY = getCenterYForRow(i);
|
|
for (int j = 0; j < 3; j++) {
|
|
CellState cellState = mCellStates[i][j];
|
|
float centerX = getCenterXForColumn(j);
|
|
float translationY = cellState.translationY;
|
|
drawCircle(canvas, (int) centerX, (int) centerY + translationY, cellState.radius,
|
|
drawLookup[i][j], cellState.alpha);
|
|
}
|
|
}
|
|
|
|
// 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 we are in stealth mode)
|
|
final boolean drawPath = !mInStealthMode;
|
|
|
|
if (drawPath) {
|
|
mPathPaint.setColor(getCurrentColor(true /* partOfPattern */));
|
|
// Anyway other drawing sets their own alpha ignoring the original; And in this way we
|
|
// can use ?colorControlNormal better.
|
|
mPathPaint.setAlpha(255);
|
|
|
|
boolean anyCircles = false;
|
|
float lastX = 0f;
|
|
float lastY = 0f;
|
|
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) {
|
|
CellState state = mCellStates[cell.row][cell.column];
|
|
currentPath.rewind();
|
|
currentPath.moveTo(lastX, lastY);
|
|
if (state.lineEndX != Float.MIN_VALUE && state.lineEndY != Float.MIN_VALUE) {
|
|
currentPath.lineTo(state.lineEndX, state.lineEndY);
|
|
} else {
|
|
currentPath.lineTo(centerX, centerY);
|
|
}
|
|
canvas.drawPath(currentPath, mPathPaint);
|
|
}
|
|
lastX = centerX;
|
|
lastY = centerY;
|
|
}
|
|
|
|
// draw last in progress section
|
|
if ((mPatternInProgress || mPatternDisplayMode == DisplayMode.Animate)
|
|
&& anyCircles) {
|
|
currentPath.rewind();
|
|
currentPath.moveTo(lastX, lastY);
|
|
currentPath.lineTo(mInProgressX, mInProgressY);
|
|
|
|
mPathPaint.setAlpha((int) (calculateLastSegmentAlpha(
|
|
mInProgressX, mInProgressY, lastX, lastY) * 255f));
|
|
canvas.drawPath(currentPath, mPathPaint);
|
|
}
|
|
}
|
|
}
|
|
|
|
private float calculateLastSegmentAlpha(float x, float y, float lastX, float lastY) {
|
|
float diffX = x - lastX;
|
|
float diffY = y - lastY;
|
|
float dist = (float) Math.sqrt(diffX * diffX + diffY * diffY);
|
|
float frac = dist / mSquareWidth;
|
|
return Math.min(1f, Math.max(0f, (frac - 0.3f) * 4f));
|
|
}
|
|
|
|
private int getCurrentColor(boolean partOfPattern) {
|
|
if (!partOfPattern || mInStealthMode || mPatternInProgress) {
|
|
// unselected circle
|
|
return mRegularColor;
|
|
} else if (mPatternDisplayMode == DisplayMode.Wrong) {
|
|
// the pattern is wrong
|
|
return mErrorColor;
|
|
} else if (mPatternDisplayMode == DisplayMode.Correct ||
|
|
mPatternDisplayMode == DisplayMode.Animate) {
|
|
return mSuccessColor;
|
|
} else {
|
|
throw new IllegalStateException("unknown display mode " + mPatternDisplayMode);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param partOfPattern Whether this circle is part of the pattern.
|
|
*/
|
|
private void drawCircle(Canvas canvas, float centerX, float centerY, float radius,
|
|
boolean partOfPattern, float alpha) {
|
|
mPaint.setColor(getCurrentColor(partOfPattern));
|
|
mPaint.setAlpha((int) (alpha * 255));
|
|
canvas.drawCircle(centerX, centerY, radius, mPaint);
|
|
}
|
|
|
|
@Override
|
|
protected Parcelable onSaveInstanceState() {
|
|
Parcelable superState = super.onSaveInstanceState();
|
|
return new SavedState(superState,
|
|
LockPatternUtils.patternToString(mPattern),
|
|
mPatternDisplayMode.ordinal(),
|
|
mInputEnabled, mInStealthMode);
|
|
}
|
|
|
|
@Override
|
|
protected void onRestoreInstanceState(Parcelable state) {
|
|
final SavedState ss = (SavedState) state;
|
|
super.onRestoreInstanceState(ss.getSuperState());
|
|
setPattern(
|
|
DisplayMode.Correct,
|
|
LockPatternUtils.stringToPattern(ss.getSerializedPattern()));
|
|
mPatternDisplayMode = DisplayMode.values()[ss.getDisplayMode()];
|
|
mInputEnabled = ss.isInputEnabled();
|
|
mInStealthMode = ss.isInStealthMode();
|
|
}
|
|
|
|
/**
|
|
* 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 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);
|
|
}
|
|
|
|
/**
|
|
* Represents a cell in the 3 X 3 matrix of the unlock pattern view.
|
|
*/
|
|
public static class Cell {
|
|
// 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);
|
|
}
|
|
}
|
|
}
|
|
|
|
int row;
|
|
int column;
|
|
|
|
/**
|
|
* @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;
|
|
}
|
|
|
|
/**
|
|
* @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 int getRow() {
|
|
return row;
|
|
}
|
|
|
|
public int getColumn() {
|
|
return column;
|
|
}
|
|
|
|
@NonNull
|
|
public String toString() {
|
|
return "(row=" + row + ",clmn=" + column + ")";
|
|
}
|
|
}
|
|
|
|
public static class CellState {
|
|
public float lineEndX = Float.MIN_VALUE;
|
|
public float lineEndY = Float.MIN_VALUE;
|
|
public ValueAnimator lineAnimator;
|
|
int row;
|
|
int col;
|
|
float radius;
|
|
float translationY;
|
|
float alpha = 1f;
|
|
}
|
|
|
|
/**
|
|
* The parecelable for saving and restoring a lock pattern view.
|
|
*/
|
|
private static class SavedState extends BaseSavedState {
|
|
|
|
public static final Creator<SavedState> CREATOR =
|
|
new Creator<SavedState>() {
|
|
@Override
|
|
public SavedState createFromParcel(Parcel in) {
|
|
return new SavedState(in);
|
|
}
|
|
|
|
@Override
|
|
public SavedState[] newArray(int size) {
|
|
return new SavedState[size];
|
|
}
|
|
};
|
|
private final String mSerializedPattern;
|
|
private final int mDisplayMode;
|
|
private final boolean mInputEnabled;
|
|
private final boolean mInStealthMode;
|
|
|
|
/**
|
|
* Constructor called from {@link LockPatternView#onSaveInstanceState()}
|
|
*/
|
|
private SavedState(Parcelable superState, String serializedPattern, int displayMode,
|
|
boolean inputEnabled, boolean inStealthMode) {
|
|
super(superState);
|
|
mSerializedPattern = serializedPattern;
|
|
mDisplayMode = displayMode;
|
|
mInputEnabled = inputEnabled;
|
|
mInStealthMode = inStealthMode;
|
|
}
|
|
|
|
/**
|
|
* Constructor called from {@link #CREATOR}
|
|
*/
|
|
@SuppressLint("ParcelClassLoader")
|
|
private SavedState(Parcel in) {
|
|
super(in);
|
|
mSerializedPattern = in.readString();
|
|
mDisplayMode = in.readInt();
|
|
mInputEnabled = (Boolean) in.readValue(null);
|
|
mInStealthMode = (Boolean) in.readValue(null);
|
|
}
|
|
|
|
public String getSerializedPattern() {
|
|
return mSerializedPattern;
|
|
}
|
|
|
|
public int getDisplayMode() {
|
|
return mDisplayMode;
|
|
}
|
|
|
|
public boolean isInputEnabled() {
|
|
return mInputEnabled;
|
|
}
|
|
|
|
public boolean isInStealthMode() {
|
|
return mInStealthMode;
|
|
}
|
|
|
|
@Override
|
|
public void writeToParcel(@NonNull Parcel dest, int flags) {
|
|
super.writeToParcel(dest, flags);
|
|
dest.writeString(mSerializedPattern);
|
|
dest.writeInt(mDisplayMode);
|
|
dest.writeValue(mInputEnabled);
|
|
dest.writeValue(mInStealthMode);
|
|
}
|
|
}
|
|
} |