/**
* Displays a powerful, customizable and Material Design complaint pattern lock in the screen which
* can be used to lock any Activity or Fragment from the user
*/
public class PatternLockView extends View {
/**
* Represents the aspect ratio for the View
*/
@IntDef({ASPECT_RATIO_SQUARE, ASPECT_RATIO_WIDTH_BIAS, ASPECT_RATIO_HEIGHT_BIAS})
@Retention(RetentionPolicy.SOURCE)
public @interface AspectRatio {
// Width and height will be same. Minimum of width and height
int ASPECT_RATIO_SQUARE = 0;
// Width will be fixed. The height will be the minimum of width and height
int ASPECT_RATIO_WIDTH_BIAS = 1;
// Height will be fixed. The width will be the minimum of width and height
int ASPECT_RATIO_HEIGHT_BIAS = 2;
}
/**
* Represents the different modes in which this view can be represented
*/
@IntDef({CORRECT, AUTO_DRAW, WRONG})
@Retention(RetentionPolicy.SOURCE)
public @interface PatternViewMode {
/**
* This state represents a correctly drawn pattern by the user. The color of the path and
* the dots both would be changed to this color.
* <p>
* (NOTE - Consider showing this state in a friendly color)
*/
int CORRECT = 0;
/**
* Automatically draw the pattern for demo or tutorial purposes.
*/
int AUTO_DRAW = 1;
/**
* This state represents a wrongly drawn pattern by the user. The color of the path and
* the dots both would be changed to this color.
* <p>
* (NOTE - Consider showing this state in an attention-seeking color)
*/
int WRONG = 2;
}
private static final int DEFAULT_PATTERN_DOT_COUNT = 3;
private static final boolean PROFILE_DRAWING = false;
/**
* The time (in millis) spend in 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;
// Amount of time (in millis) spent to animate a dot
private static final int DEFAULT_DOT_ANIMATION_DURATION = 190;
// Amount of time (in millis) spent to animate a path ends
private static final int DEFAULT_PATH_END_ANIMATION_DURATION = 100;
// This can be used to avoid updating the display for very small motions or noisy panels
private static final float DEFAULT_DRAG_THRESHOLD = 0.0f;
private DotState[][] mDotStates;//点状态标示矩阵
private int mPatternSize;//总共的宫格数量
private boolean mDrawingProfilingStarted = false;
private long mAnimatingPeriodStart;
private float mHitFactor = 0.6f;//点击响应占宫格的比例
// Made static so that the static inner class can use it
private static int sDotCount;//一行的点数
private boolean mAspectRatioEnabled;//是否按比例缩放View
private int mAspectRatio;//以宽、高优先缩放
private int mNormalStateColor;//基本色彩
private int mWrongStateColor;//错误色彩
private int mCorrectStateColor;//正确色彩
private int mPathWidth;//路径线条的宽度
private int mDotNormalSize;//正常的点大小
private int mDotSelectedSize;//选中时的点大小
private int mDotAnimationDuration;//点动画时长
private int mPathEndAnimationDuration;//线动画时长
private Paint mDotPaint; //点画笔
private Paint mPathPaint;//路径画笔
private float mViewWidth;//一个宫格的宽高
private float mViewHeight;
private int mPatternViewMode = CORRECT;//当前的模式
private boolean mInputEnabled = true;//是否可编辑:true时可绘制手势
private boolean mInStealthMode = false;//是否在隐秘模式:true不画出来线条
private boolean mEnableHapticFeedback = true;//路径绘制过程中,是否震动
private List<PatternLockViewListener> mPatternListeners;
// The pattern represented as a list of connected {@link Dot}
private ArrayList<Dot> mPattern;//链接中的路径点
/**
* Lookup table for the dots of the pattern we are currently drawing.
* This will be the dots of the complete pattern unless we are animating,
* in which case we use this to hold the dots we are drawing for the in
* progress animation.
*/
private boolean[][] mPatternDrawLookup; // 已选择的点所对应的 矩阵标示
private boolean mPatternInProgress = false;// 标示是否在 手势绘制过程中
private float mInProgressX = -1;//最终触摸位置:绘制断头线
private float mInProgressY = -1;//最终触摸位置:绘制断头线
private final Path mCurrentPath = new Path();
private final Rect mInvalidate = new Rect();
private final Rect mTempInvalidateRect = new Rect();
private Interpolator mFastOutSlowInInterpolator;//动画 加速器
private Interpolator mLinearOutSlowInInterpolator;//动画 加速器
public PatternLockView(Context context) {
this(context, null);
}
public PatternLockView(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.PatternLockView);
try {
sDotCount = typedArray.getInt(R.styleable.PatternLockView_dotCount,
DEFAULT_PATTERN_DOT_COUNT);
mAspectRatioEnabled = typedArray.getBoolean(R.styleable.PatternLockView_aspectRatioEnabled,
false);
mAspectRatio = typedArray.getInt(R.styleable.PatternLockView_aspectRatio,
ASPECT_RATIO_SQUARE);
mPathWidth = (int) typedArray.getDimension(R.styleable.PatternLockView_pathWidth,
ResourceUtils.getDimensionInPx(getContext(), R.dimen.pattern_lock_path_width));
mNormalStateColor = typedArray.getColor(R.styleable.PatternLockView_normalStateColor,
ResourceUtils.getColor(getContext(), R.color.white));
mCorrectStateColor = typedArray.getColor(R.styleable.PatternLockView_correctStateColor,
ResourceUtils.getColor(getContext(), R.color.white));
mWrongStateColor = typedArray.getColor(R.styleable.PatternLockView_wrongStateColor,
ResourceUtils.getColor(getContext(), R.color.pomegranate));
mDotNormalSize = (int) typedArray.getDimension(R.styleable.PatternLockView_dotNormalSize,
ResourceUtils.getDimensionInPx(getContext(), R.dimen.pattern_lock_dot_size));
mDotSelectedSize = (int) typedArray.getDimension(R.styleable
.PatternLockView_dotSelectedSize,
ResourceUtils.getDimensionInPx(getContext(), R.dimen.pattern_lock_dot_selected_size));
mDotAnimationDuration = typedArray.getInt(R.styleable.PatternLockView_dotAnimationDuration,
DEFAULT_DOT_ANIMATION_DURATION);
mPathEndAnimationDuration = typedArray.getInt(R.styleable.PatternLockView_pathEndAnimationDuration,
DEFAULT_PATH_END_ANIMATION_DURATION);
} finally {
typedArray.recycle();
}
// The pattern will always be symmetrical
mPatternSize = sDotCount * sDotCount;
mPattern = new ArrayList<>(mPatternSize);
mPatternDrawLookup = new boolean[sDotCount][sDotCount];
//初始化 所有基本点 和 点的直径大小
mDotStates = new DotState[sDotCount][sDotCount];
for (int i = 0; i < sDotCount; i++) {
for (int j = 0; j < sDotCount; j++) {
mDotStates[i][j] = new DotState();
mDotStates[i][j].mSize = mDotNormalSize;
}
}
mPatternListeners = new ArrayList<>();
initView();
}
private void initView() {
setClickable(true);
mPathPaint = new Paint();
mPathPaint.setAntiAlias(true);
mPathPaint.setDither(true);
mPathPaint.setColor(mNormalStateColor);
mPathPaint.setStyle(Paint.Style.STROKE);
mPathPaint.setStrokeJoin(Paint.Join.ROUND);
mPathPaint.setStrokeCap(Paint.Cap.ROUND);
mPathPaint.setStrokeWidth(mPathWidth);
mDotPaint = new Paint();
mDotPaint.setAntiAlias(true);
mDotPaint.setDither(true);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
&& !isInEditMode()) {
mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(
getContext(), android.R.interpolator.fast_out_slow_in);
mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator(
getContext(), android.R.interpolator.linear_out_slow_in);
}
}
/**************************************一、测量过程******************************************/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (!mAspectRatioEnabled) {
return;
}
int oldWidth = resolveMeasured(widthMeasureSpec, getSuggestedMinimumWidth());
int oldHeight = resolveMeasured(heightMeasureSpec, getSuggestedMinimumHeight());
int newWidth;
int newHeight;
switch (mAspectRatio) {
case ASPECT_RATIO_SQUARE:
newWidth = newHeight = Math.min(oldWidth, oldHeight);
break;
case ASPECT_RATIO_WIDTH_BIAS:
newWidth = oldWidth;
newHeight = Math.min(oldWidth, oldHeight);
break;
case ASPECT_RATIO_HEIGHT_BIAS:
newWidth = Math.min(oldWidth, oldHeight);
newHeight = oldHeight;
break;
default:
throw new IllegalStateException("Unknown aspect ratio");
}
setMeasuredDimension(newWidth, newHeight);
}
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 onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
int adjustedWidth = width - getPaddingLeft() - getPaddingRight();
mViewWidth = adjustedWidth / (float) sDotCount;
int adjustedHeight = height - getPaddingTop() - getPaddingBottom();
mViewHeight = adjustedHeight / (float) sDotCount;
}
/**************************************三、绘制过程******************************************/
@Override
protected void onDraw(Canvas canvas) {
ArrayList<Dot> pattern = mPattern;
int patternSize = pattern.size();
boolean[][] drawLookupTable = mPatternDrawLookup;// 已选择的点所对应的 矩阵标示
if (mPatternViewMode == AUTO_DRAW) {
int oneCycle = (patternSize + 1) * MILLIS_PER_CIRCLE_ANIMATING;
int spotInCycle = (int) (SystemClock.elapsedRealtime() - mAnimatingPeriodStart)
% oneCycle;
int numCircles = spotInCycle / MILLIS_PER_CIRCLE_ANIMATING;
clearPatternDrawLookup();
for (int i = 0; i < numCircles; i++) {
Dot dot = pattern.get(i);
drawLookupTable[dot.mRow][dot.mColumn] = true;
}
boolean needToUpdateInProgressPoint = numCircles > 0
&& numCircles < patternSize;
if (needToUpdateInProgressPoint) {
float percentageOfNextCircle = ((float) (spotInCycle % MILLIS_PER_CIRCLE_ANIMATING))
/ MILLIS_PER_CIRCLE_ANIMATING;
Dot currentDot = pattern.get(numCircles - 1);
float centerX = getCenterXForColumn(currentDot.mColumn);
float centerY = getCenterYForRow(currentDot.mRow);
Dot nextDot = pattern.get(numCircles);
float dx = percentageOfNextCircle
* (getCenterXForColumn(nextDot.mColumn) - centerX);
float dy = percentageOfNextCircle
* (getCenterYForRow(nextDot.mRow) - centerY);
mInProgressX = centerX + dx;
mInProgressY = centerY + dy;
}
invalidate();
}
Path currentPath = mCurrentPath;
currentPath.rewind();//清除掉path里的线条和曲线,但是会保留内部的数据结构以便重用;
//【】绘制点
for (int i = 0; i < sDotCount; i++) {
float centerY = getCenterYForRow(i);
for (int j = 0; j < sDotCount; j++) {
DotState dotState = mDotStates[i][j];
float centerX = getCenterXForColumn(j);//x
float size = dotState.mSize * dotState.mScale;//直径
float translationY = dotState.mTranslateY;//y的偏移量
drawCircle(canvas, centerX
, (int) centerY + translationY
, size
, Color.YELLOW
, 255 );
drawCircle(canvas, (int) centerX, (int) centerY + translationY,
size, drawLookupTable[i][j], dotState.mAlpha);
}
}
//【】绘制路径 Draw the path of the pattern (unless we are in stealth mode)
boolean drawPath = !mInStealthMode;
if (drawPath) {
mPathPaint.setColor(getCurrentColor(true));
boolean anyCircles = false;//标示 路径中是否没有任何点
float lastX = 0f;
float lastY = 0f;
for (int i = 0; i < patternSize; i++) {
Dot dot = pattern.get(i);
// 只绘制 路径中的点延伸的线条
if (!drawLookupTable[dot.mRow][dot.mColumn]) {
break;
}
anyCircles = true;
float centerX = getCenterXForColumn(dot.mColumn);
float centerY = getCenterYForRow(dot.mRow);
if (i != 0) {
DotState state = mDotStates[dot.mRow][dot.mColumn];
currentPath.rewind();//清除掉path里的线条和曲线,但是会保留内部的数据结构以便重用;
currentPath.moveTo(lastX, lastY);
if (state.mLineEndX != Float.MIN_VALUE
&& state.mLineEndY != Float.MIN_VALUE) {
currentPath.lineTo(state.mLineEndX, state.mLineEndY);
} else {
currentPath.lineTo(centerX, centerY);
}
canvas.drawPath(currentPath, mPathPaint);
}
lastX = centerX;
lastY = centerY;
}
// 【】绘制断头线:最后一个圆心到当前移动位置的连线
if ((mPatternInProgress || mPatternViewMode == AUTO_DRAW)
&& 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 fraction = dist / mViewWidth;
return Math.min(1f, Math.max(0f, (fraction - 0.3f) * 4f));
}
/******************************************四、不保留活动*****************************************************/
@Override
protected Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
return new SavedState(superState,
PatternLockUtils.patternToString(this, mPattern),
mPatternViewMode, mInputEnabled, mInStealthMode,
mEnableHapticFeedback);
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
final SavedState savedState = (SavedState) state;
super.onRestoreInstanceState(savedState.getSuperState());
setPattern(CORRECT,
PatternLockUtils.stringToPattern(this, savedState.getSerializedPattern()));
mPatternViewMode = savedState.getDisplayMode();
mInputEnabled = savedState.isInputEnabled();
mInStealthMode = savedState.isInStealthMode();
mEnableHapticFeedback = savedState.isTactileFeedbackEnabled();
}
/**
* The parcelable 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 PatternLockView#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);
}
@SuppressWarnings("unused")
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];
}
};
}
/*****************************************五、控制********************************************/
@Override
public boolean onHoverEvent(MotionEvent event) {
if (((AccessibilityManager) getContext().getSystemService(
Context.ACCESSIBILITY_SERVICE)).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:
mPatternInProgress = false;
resetPattern();
notifyPatternCleared();
if (PROFILE_DRAWING) {
if (mDrawingProfilingStarted) {
Debug.stopMethodTracing();
mDrawingProfilingStarted = false;
}
}
return true;
}
return false;
}
/**
* 5.1 down事件响应
* 1. 清理屏幕
* 2. 获取当前手指的坐标,调用detectAndAddHit(float x, float y)判断落点宫格,加入全局选中列表
* 3. 发送通知与局部刷新
* @param event
*/
private void handleActionDown(MotionEvent event) {
resetPattern();
float x = event.getX();
float y = event.getY();
Dot hitDot = detectAndAddHit(x, y);
if (hitDot != null) {//如果命中一个宫格,表示开始一次手势过程
mPatternInProgress = true;
mPatternViewMode = CORRECT;
notifyPatternStarted();
} else {//未命中,则清除
mPatternInProgress = false;
notifyPatternCleared();
}
if (hitDot != null) {
float startX = getCenterXForColumn(hitDot.mColumn);
float startY = getCenterYForRow(hitDot.mRow);
float widthOffset = mViewWidth / 2f;
float heightOffset = mViewHeight / 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("PatternLockDrawing");
mDrawingProfilingStarted = true;
}
}
}
/**
* 判断落点位置是否命中新宫格:
* 如果命中,则进一步判断 本次命中的新宫格 和 上一次宫格 间是否有未链接的宫格
* 没有间隙宫格,则仅将 本次命中的新宫格 加入 全局链路中
* 有间隙宫格,则默认也加入 全局链路中
*
* 未命中则返回null
*/
private Dot detectAndAddHit(float x, float y) {
final Dot dot = checkForNewHit(x, y);
if (dot != null) {
// Check for gaps in existing pattern
Dot fillInGapDot = null;
final ArrayList<Dot> pattern = mPattern;
//选中间隙宫格
if (!pattern.isEmpty()) {
Dot lastDot = pattern.get(pattern.size() - 1);
int dRow = dot.mRow - lastDot.mRow;
int dColumn = dot.mColumn - lastDot.mColumn;
int fillInRow = lastDot.mRow;
int fillInColumn = lastDot.mColumn;
if (Math.abs(dRow) == 2 && Math.abs(dColumn) != 1) {
fillInRow = lastDot.mRow + ((dRow > 0) ? 1 : -1);
}
if (Math.abs(dColumn) == 2 && Math.abs(dRow) != 1) {
fillInColumn = lastDot.mColumn + ((dColumn > 0) ? 1 : -1);
}
fillInGapDot = Dot.of(fillInRow, fillInColumn);
}
//判断间隙宫格是否在 已选择链路中
if (fillInGapDot != null
&& !mPatternDrawLookup[fillInGapDot.mRow][fillInGapDot.mColumn]) {
//有间隙宫格,则默认也加入 全局链路中
addCellToPattern(fillInGapDot);
}
//将 本次命中的新宫格 加入 全局链路中
addCellToPattern(dot);
if (mEnableHapticFeedback) {
performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY,
HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING
| HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING);
}
return dot;
}
return null;
}
//判断命中宫格: x,y命中的,并且不在已记录链表中的宫格
//根据getRowHit(float y)与getColumnHit(float x)返回的行、列判断是否是新的图形点,如果是返回新点,否则返回 null
private Dot 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 Dot.of(rowHit, columnHit);
}
//遍历所有图形点行高,寻找坐标 y 在哪行图案点的行高范围内
private int getRowHit(float y) {
final float squareHeight = mViewHeight;
float hitSize = squareHeight * mHitFactor;
float offset = getPaddingTop() + (squareHeight - hitSize) / 2f;
for (int i = 0; i < sDotCount; i++) {
float hitTop = offset + squareHeight * i;
if (y >= hitTop && y <= hitTop + hitSize) {
return i;
}
}
return -1;
}
//遍历所有图形点列宽,寻找坐标 x 在哪列图案点的列宽范围内
private int getColumnHit(float x) {
final float squareWidth = mViewWidth;
float hitSize = squareWidth * mHitFactor;
float offset = getPaddingLeft() + (squareWidth - hitSize) / 2f;
for (int i = 0; i < sDotCount; i++) {
final float hitLeft = offset + squareWidth * i;
if (x >= hitLeft && x <= hitLeft + hitSize) {
return i;
}
}
return -1;
}
//将目标点加入已选择链路,并开启一次动画
private void addCellToPattern(Dot newDot) {
mPatternDrawLookup[newDot.mRow][newDot.mColumn] = true;
mPattern.add(newDot);
if (!mInStealthMode) {
startDotSelectedAnimation(newDot);
}
notifyPatternProgress();
}
/**
* 检查手指移动过程中每一个点的坐标,判断
* : 如果 pattern list 不为空,则把最后一个图形点的坐标与当前手指坐标的区域进行局部刷新
* : 如果在移动过程中加入了新的图形点则以此点坐标继续局部刷新
* @param event
*/
private void handleActionMove(MotionEvent event) {
float radius = mPathWidth;
int historySize = event.getHistorySize();//历史个数
mTempInvalidateRect.setEmpty();
boolean invalidateNow = false;
//遍历 移动的历史位置
for (int i = 0; i < historySize + 1; i++) {
float x = i < historySize ? event.getHistoricalX(i) : event
.getX();
float y = i < historySize ? event.getHistoricalY(i) : event
.getY();
//历史位置 是否命中某个宫格
Dot hitDot = detectAndAddHit(x, y);
int patternSize = mPattern.size();
if (hitDot != null && patternSize == 1) {
//当前链路没有宫格 则记录 开启了一次绘制
mPatternInProgress = true;
notifyPatternStarted();
}
// 是否要立即绘制:判断当前落点 和上一次的位置距离
float dx = Math.abs(x - mInProgressX);
float dy = Math.abs(y - mInProgressY);
if (dx > DEFAULT_DRAG_THRESHOLD || dy > DEFAULT_DRAG_THRESHOLD) {
invalidateNow = true;
}
//已经在 一次流程中的
if (mPatternInProgress && patternSize > 0) {
final ArrayList<Dot> pattern = mPattern;
final Dot lastDot = pattern.get(patternSize - 1);
float lastCellCenterX = getCenterXForColumn(lastDot.mColumn);
float lastCellCenterY = getCenterYForRow(lastDot.mRow);
// 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 (hitDot != null) {
float width = mViewWidth * 0.5f;
float height = mViewHeight * 0.5f;
float hitCellCenterX = getCenterXForColumn(hitDot.mColumn);
float hitCellCenterY = getCenterYForRow(hitDot.mRow);
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
mTempInvalidateRect.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(mTempInvalidateRect);
invalidate(mInvalidate);
mInvalidate.set(mTempInvalidateRect);
}
}
/**
* 检查 pattern list 如果不为空则停止添加,发送完成消息,全局刷新
* @param event
*/
private void handleActionUp(MotionEvent event) {
// Report pattern detected
if (!mPattern.isEmpty()) {
mPatternInProgress = false;
cancelLineAnimations();
notifyPatternDetected();
invalidate();
}
if (PROFILE_DRAWING) {
if (mDrawingProfilingStarted) {
Debug.stopMethodTracing();
mDrawingProfilingStarted = false;
}
}
}
private void cancelLineAnimations() {
for (int i = 0; i < sDotCount; i++) {
for (int j = 0; j < sDotCount; j++) {
DotState state = mDotStates[i][j];
if (state.mLineAnimator != null) {
state.mLineAnimator.cancel();
state.mLineEndX = Float.MIN_VALUE;
state.mLineEndY = Float.MIN_VALUE;
}
}
}
}
/*****************************************动画类******************************************************/
private void startDotSelectedAnimation(Dot dot) {
final DotState dotState = mDotStates[dot.mRow][dot.mColumn];
startSizeAnimation(mDotNormalSize, mDotSelectedSize, mDotAnimationDuration,
mLinearOutSlowInInterpolator, dotState, new Runnable() {
@Override
public void run() {
startSizeAnimation(mDotSelectedSize, mDotNormalSize, mDotAnimationDuration,
mFastOutSlowInInterpolator, dotState, null);
}
});
startLineEndAnimation(dotState, mInProgressX, mInProgressY,
getCenterXForColumn(dot.mColumn), getCenterYForRow(dot.mRow));
}
private void startLineEndAnimation(final DotState state,
final float startX, final float startY, final float targetX,
final float targetY) {
ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float t = (Float) animation.getAnimatedValue();
state.mLineEndX = (1 - t) * startX + t * targetX;
state.mLineEndY = (1 - t) * startY + t * targetY;
invalidate();
}
});
valueAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
state.mLineAnimator = null;
}
});
valueAnimator.setInterpolator(mFastOutSlowInInterpolator);
valueAnimator.setDuration(mPathEndAnimationDuration);
valueAnimator.start();
state.mLineAnimator = valueAnimator;
}
private void startSizeAnimation(float start, float end, long duration,
Interpolator interpolator, final DotState state,
final Runnable endRunnable) {
ValueAnimator valueAnimator = ValueAnimator.ofFloat(start, end);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
state.mSize = (Float) animation.getAnimatedValue();
invalidate();
}
});
if (endRunnable != null) {
valueAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
if (endRunnable != null) {
endRunnable.run();
}
}
});
}
valueAnimator.setInterpolator(interpolator);
valueAnimator.setDuration(duration);
valueAnimator.start();
}
/***************************************控制类utils*********************************************/
//清空路径并重绘
private void resetPattern() {
mPattern.clear();//清除当前路径点
clearPatternDrawLookup();//已选择的点所对应的 矩阵标示 全部置为false标示未选中
mPatternViewMode = CORRECT;
invalidate();
}
private void clearPatternDrawLookup() {
for (int i = 0; i < sDotCount; i++) {
for (int j = 0; j < sDotCount; j++) {
mPatternDrawLookup[i][j] = false;
}
}
}
private void notifyPatternProgress() {
sendAccessEvent(R.string.message_pattern_dot_added);
notifyListenersProgress(mPattern);
}
private void notifyPatternStarted() {
sendAccessEvent(R.string.message_pattern_started);
notifyListenersStarted();
}
private void notifyPatternDetected() {
sendAccessEvent(R.string.message_pattern_detected);
notifyListenersComplete(mPattern);
}
private void notifyPatternCleared() {
sendAccessEvent(R.string.message_pattern_cleared);
notifyListenersCleared();
}
private void sendAccessEvent(int resId) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
setContentDescription(getContext().getString(resId));
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
setContentDescription(null);
} else {
announceForAccessibility(getContext().getString(resId));
}
}
private void notifyListenersStarted() {
for (PatternLockViewListener patternListener : mPatternListeners) {
if (patternListener != null) {
patternListener.onStarted();
}
}
}
private void notifyListenersProgress(List<Dot> pattern) {
for (PatternLockViewListener patternListener : mPatternListeners) {
if (patternListener != null) {
patternListener.onProgress(pattern);
}
}
}
private void notifyListenersComplete(List<Dot> pattern) {
for (PatternLockViewListener patternListener : mPatternListeners) {
if (patternListener != null) {
patternListener.onComplete(pattern);
}
}
}
private void notifyListenersCleared() {
for (PatternLockViewListener patternListener : mPatternListeners) {
if (patternListener != null) {
patternListener.onCleared();
}
}
}
/*************************************数据结构***********************************************/
/**
* Represents a cell in the matrix of the pattern view
*/
public static class Dot implements Parcelable {
private int mRow;
private int mColumn;
private static Dot[][] sDots;
static {
sDots = new Dot[sDotCount][sDotCount];
// Initializing the dots
for (int i = 0; i < sDotCount; i++) {
for (int j = 0; j < sDotCount; j++) {
sDots[i][j] = new Dot(i, j);
}
}
}
private Dot(int row, int column) {
checkRange(row, column);
this.mRow = row;
this.mColumn = column;
}
/**
* Gets the identifier of the dot. It is counted from left to right, top to bottom of the
* matrix, starting by zero
*/
public int getId() {
return mRow * sDotCount + mColumn;
}
public int getRow() {
return mRow;
}
public int getColumn() {
return mColumn;
}
/**
* @param row The mRow of the cell.
* @param column The mColumn of the cell.
*/
public static synchronized Dot of(int row, int column) {
checkRange(row, column);
return sDots[row][column];
}
/**
* Gets a cell from its identifier
*/
public static synchronized Dot of(int id) {
return of(id / sDotCount, id % sDotCount);
}
private static void checkRange(int row, int column) {
if (row < 0 || row > sDotCount - 1) {
throw new IllegalArgumentException("mRow must be in range 0-"
+ (sDotCount - 1));
}
if (column < 0 || column > sDotCount - 1) {
throw new IllegalArgumentException("mColumn must be in range 0-"
+ (sDotCount - 1));
}
}
@Override
public String toString() {
return "(Row = " + mRow + ", Col = " + mColumn + ")";
}
@Override
public boolean equals(Object object) {
if (object instanceof Dot) {
return mColumn == ((Dot) object).mColumn
&& mRow == ((Dot) object).mRow;
}
return super.equals(object);
}
@Override
public int hashCode() {
int result = mRow;
result = 31 * result + mColumn;
return result;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(mColumn);
dest.writeInt(mRow);
}
public static final Creator<Dot> CREATOR = new Creator<Dot>() {
@Override
public Dot createFromParcel(Parcel in) {
return new Dot(in);
}
@Override
public Dot[] newArray(int size) {
return new Dot[size];
}
};
private Dot(Parcel in) {
mColumn = in.readInt();
mRow = in.readInt();
}
}
public static class DotState {
float mScale = 1.0f;
float mTranslateY = 0.0f;
float mAlpha = 1.0f;
float mSize;
float mLineEndX = Float.MIN_VALUE;
float mLineEndY = Float.MIN_VALUE;
ValueAnimator mLineAnimator;
}
/***********************************绘制 Utils**********************************************/
//获取指定行的圆心所在Y坐标
private float getCenterYForRow(int row) {
return getPaddingTop() + row * mViewHeight + mViewHeight / 2f;
}
//获取指定列的圆心所在x坐标
private float getCenterXForColumn(int column) {
return getPaddingLeft() + column * mViewWidth + mViewWidth / 2f;
}
// 绘制圆
//partOfPattern 该点是否在路径中
private void drawCircle(Canvas canvas, float centerX, float centerY,
float size, boolean partOfPattern, float alpha) {
drawCircle(canvas, centerX, centerY, size / 2, getCurrentColor(partOfPattern), (int) (alpha * 255) );
}
private void drawCircle(Canvas canvas, float centerX, float centerY,
float raduis, int color, int alpha) {
mDotPaint.setColor(color);
mDotPaint.setAlpha(alpha);
canvas.drawCircle(centerX, centerY, raduis, mDotPaint);
}
// 获取目标圆的颜色
//partOfPattern 该点是否在路径中:不在的话,返回默认色彩;在的话,根据是错误还是正确,进一步区分
private int getCurrentColor(boolean partOfPattern) {
if (!partOfPattern || mInStealthMode || mPatternInProgress) {
return mNormalStateColor;
} else if (mPatternViewMode == WRONG) {
return mWrongStateColor;
} else if (mPatternViewMode == CORRECT
|| mPatternViewMode == AUTO_DRAW) {
return mCorrectStateColor;
} else {
throw new IllegalStateException("Unknown view mode " + mPatternViewMode);
}
}
/***********************************get | set**********************************************/
/**
* Returns the list of dots in the current selected pattern. This list is independent of the
* internal pattern dot list
*/
@SuppressWarnings("unchecked")
public List<Dot> getPattern() {
return (List<Dot>) mPattern.clone();
}
@PatternViewMode
public int getPatternViewMode() {
return mPatternViewMode;
}
public boolean isInStealthMode() {
return mInStealthMode;
}
public boolean isTactileFeedbackEnabled() {
return mEnableHapticFeedback;
}
public boolean isInputEnabled() {
return mInputEnabled;
}
public int getDotCount() {
return sDotCount;
}
public boolean isAspectRatioEnabled() {
return mAspectRatioEnabled;
}
@AspectRatio
public int getAspectRatio() {
return mAspectRatio;
}
public int getNormalStateColor() {
return mNormalStateColor;
}
public int getWrongStateColor() {
return mWrongStateColor;
}
public int getCorrectStateColor() {
return mCorrectStateColor;
}
public int getPathWidth() {
return mPathWidth;
}
public int getDotNormalSize() {
return mDotNormalSize;
}
public int getDotSelectedSize() {
return mDotSelectedSize;
}
public int getPatternSize() {
return mPatternSize;
}
public int getDotAnimationDuration() {
return mDotAnimationDuration;
}
public int getPathEndAnimationDuration() {
return mPathEndAnimationDuration;
}
/**
* Set the pattern explicitly rather than waiting for the user to input a
* pattern. You can use this for help or demo purposes
*
* @param patternViewMode The mode in which the pattern should be displayed
* @param pattern The pattern
*/
public void setPattern(@PatternViewMode int patternViewMode, List<Dot> pattern) {
mPattern.clear();
mPattern.addAll(pattern);
clearPatternDrawLookup();
for (Dot dot : pattern) {
mPatternDrawLookup[dot.mRow][dot.mColumn] = true;
}
setViewMode(patternViewMode);
}
/**
* 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.
*/
public void setViewMode(@PatternViewMode int patternViewMode) {
mPatternViewMode = patternViewMode;
if (patternViewMode == AUTO_DRAW) {
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 Dot first = mPattern.get(0);
mInProgressX = getCenterXForColumn(first.mColumn);
mInProgressY = getCenterYForRow(first.mRow);
clearPatternDrawLookup();
}
invalidate();
}
public void setDotCount(int dotCount) {
sDotCount = dotCount;
mPatternSize = sDotCount * sDotCount;
mPattern = new ArrayList<>(mPatternSize);
mPatternDrawLookup = new boolean[sDotCount][sDotCount];
mDotStates = new DotState[sDotCount][sDotCount];
for (int i = 0; i < sDotCount; i++) {
for (int j = 0; j < sDotCount; j++) {
mDotStates[i][j] = new DotState();
mDotStates[i][j].mSize = mDotNormalSize;
}
}
requestLayout();
invalidate();
}
public void setAspectRatioEnabled(boolean aspectRatioEnabled) {
mAspectRatioEnabled = aspectRatioEnabled;
requestLayout();
}
public void setAspectRatio(@AspectRatio int aspectRatio) {
mAspectRatio = aspectRatio;
requestLayout();
}
public void setNormalStateColor(@ColorInt int normalStateColor) {
mNormalStateColor = normalStateColor;
}
public void setWrongStateColor(@ColorInt int wrongStateColor) {
mWrongStateColor = wrongStateColor;
}
public void setCorrectStateColor(@ColorInt int correctStateColor) {
mCorrectStateColor = correctStateColor;
}
public void setPathWidth(@Dimension int pathWidth) {
mPathWidth = pathWidth;
initView();
invalidate();
}
public void setDotNormalSize(@Dimension int dotNormalSize) {
mDotNormalSize = dotNormalSize;
for (int i = 0; i < sDotCount; i++) {
for (int j = 0; j < sDotCount; j++) {
mDotStates[i][j] = new DotState();
mDotStates[i][j].mSize = mDotNormalSize;
}
}
invalidate();
}
public void setDotSelectedSize(@Dimension int dotSelectedSize) {
mDotSelectedSize = dotSelectedSize;
}
public void setDotAnimationDuration(int dotAnimationDuration) {
mDotAnimationDuration = dotAnimationDuration;
invalidate();
}
public void setPathEndAnimationDuration(int pathEndAnimationDuration) {
mPathEndAnimationDuration = pathEndAnimationDuration;
}
/**
* Set whether the View is in stealth mode. If {@code true}, there will be
* no visible feedback (path drawing, dot animating, etc) as the user enters the pattern
*/
public void setInStealthMode(boolean inStealthMode) {
mInStealthMode = inStealthMode;
}
public void setTactileFeedbackEnabled(boolean tactileFeedbackEnabled) {
mEnableHapticFeedback = tactileFeedbackEnabled;
}
/**
* Enabled/disables any user input of the view. This can be useful to lock the view temporarily
* while showing any message to the user so that the user cannot get the view in
* an unwanted state
*/
public void setInputEnabled(boolean inputEnabled) {
mInputEnabled = inputEnabled;
}
public void setEnableHapticFeedback(boolean enableHapticFeedback) {
mEnableHapticFeedback = enableHapticFeedback;
}
public void addPatternLockListener(PatternLockViewListener patternListener) {
mPatternListeners.add(patternListener);
}
public void removePatternLockListener(PatternLockViewListener patternListener) {
mPatternListeners.remove(patternListener);
}
public void clearPattern() {
resetPattern();
}
}
(4.2.42)手势解锁之九宫格控件PatternLockView
最新推荐文章于 2024-08-15 10:10:29 发布