在上一篇介绍了手势密码的使用,这一篇将主要介绍手势密码的原理,手势密码的功能主要是由自定义PatternLockView实现的。那咱这就一步一步来揭开PatternLockView的面纱。
效果图
步骤
第一步
自定义PatternLockView继承View,重写两个构造方法,一个在xml中定义会调用,一个在java代码中创建对象会调用。但不管怎么定义,都会走到这个构造中。
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);
mPathWidth = (int) typedArray.getDimension(R.styleable.PatternLockView_pathWidth,
ResourceUtils.getDimensionInPx(getContext(), R.dimen.pattern_lock_path_width));
mNormalDotStateColor = typedArray.getColor(R.styleable.PatternLockView_normalDotStateColor,
ResourceUtils.getColor(getContext(), R.color.white));
mCorrectDotStateColor = typedArray.getColor(R.styleable.PatternLockView_correctDotStateColor,
ResourceUtils.getColor(getContext(), R.color.white));
mCorrectDotStrokeColor = typedArray.getColor(R.styleable.PatternLockView_correctDotStrokeStateColor,
ResourceUtils.getColor(getContext(), R.color.white));
mCorrectLineStateColor = typedArray.getColor(R.styleable.PatternLockView_correctLineStateColor,
ResourceUtils.getColor(getContext(), R.color.white));
mWrongLineStateColor = typedArray.getColor(R.styleable.PatternLockView_wrongLineStateColor,
ResourceUtils.getColor(getContext(), R.color.white));
mWrongDotStateColor = typedArray.getColor(R.styleable.PatternLockView_wrongDotStateColor,
ResourceUtils.getColor(getContext(), R.color.white));
mWrongDotStrokeStateColor = typedArray.getColor(R.styleable.PatternLockView_wrongDotStrokeStateColor,
ResourceUtils.getColor(getContext(), R.color.white));
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);
} finally {
typedArray.recycle();
}
//获取绘制点的个数
mPatternSize = sDotCount * sDotCount;
//存放选中的点
mPattern = new ArrayList<>(mPatternSize);
//二维数组 选中点时置为true
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();
}
这里主要拿到我们自定义的点的数量 点的颜色 线的颜色 线的宽度等信息,创建了存放选中点的集合 存放监听对象的集合和两个二维数组及点对象。
再来看下initView()方法
private void initView() {
//设置View可点击
setClickable(true);
//设置点与点连线画笔
mPathPaint = new Paint();
mPathPaint.setAntiAlias(true);
mPathPaint.setDither(true);
mPathPaint.setColor(mCorrectLineStateColor);
mPathPaint.setStyle(Paint.Style.STROKE);
//当画笔样式为STROKE或FILL_OR_STROKE时,设置笔刷的图形样式,如圆形样式Cap.ROUND,或方形样式Cap.SQUARE
mPathPaint.setStrokeJoin(Paint.Join.ROUND);
//设置绘制时各图形的结合方式,如平滑效果等
mPathPaint.setStrokeCap(Paint.Cap.ROUND);
mPathPaint.setStrokeWidth(mPathWidth);
//设置画点的画笔
mDotPaint = new Paint();
mDotPaint.setAntiAlias(true);
mDotPaint.setDither(true);
//设置点外环的画笔
mRingPaint = new Paint();
mRingPaint.setAntiAlias(true);
mRingPaint.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);
}
}
这里主要对各个画笔做了初始化工作,也可以通过Java代码进行更改,初始化工作完成后,就要捕捉手势了。
第二步
通过onTouchEvent()方法获取手势坐标
@Override
public boolean onTouchEvent(MotionEvent event) {
//判断View是否可用
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();
return true;
}
return false;
}
这里对手指的按下 移动 抬起 取消都做了判断
取消的操作比较简单,就是重置各个状态
下边分别来看下按下 移动 抬起
handleActionDown按下方法
private void handleActionDown(MotionEvent event) {
//每次按下都重置下状态
resetPattern();
//获取手指按下的坐标,以View区域的左上顶点为(0,0)坐标
float x = event.getX();
float y = event.getY();
//通过detectAndAddHit方法判断首次按下是否触碰到点
Dot hitDot = detectAndAddHit(x, y);
//如果触碰到点,则改变状态,开始画选中点颜色和与点的连线
//如果没有触碰上,则不画线(通过mPatternInProgress状态控制)
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;
// 局部刷新其实没多大用,在onDraw里边还是所有代码都走
invalidate((int) (startX - widthOffset),
(int) (startY - heightOffset),
(int) (startX + widthOffset), (int) (startY + heightOffset));
}
//把首次按下的坐标赋值给mInProgressX mInProgressY
//当选中时可开始画线
mInProgressX = x;
mInProgressY = y;
}
按下操作主要是获取首次按下坐标,并通过坐标拿到是否触碰到某一个点上,如果触碰到点上,则改变点的颜色并开始画点与手指的连线。
handleActionMove移动方法
private void handleActionMove(MotionEvent event) {
float x = event.getX();
float y = event.getY();
Dot hitDot = detectAndAddHit(x, y);
int patternSize = mPattern.size();
//手指按下时没有选中点,当滑动时,滑动到点上进入此判断语句
//改变mPatternInProgress mPatternViewMode状态,重画选中点和外环的颜色及开始画与点的连线
if (hitDot != null && patternSize == 1) {
mPatternInProgress = true;
mPatternViewMode = CORRECT;
notifyPatternStarted();
}
mInProgressX = event.getX();
mInProgressY = event.getY();
//简单粗暴 重新绘制
invalidate();
}
移动操作主要是根据不断移动的坐标,判断是否触碰到点上,如果触碰上了就添加到选中点的集合内,并重画选中点和外环的颜色及开始画与点的连线。
handleActionUp抬起方法
private void handleActionUp(MotionEvent event) {
// 判断手势集合中是否有选中的点
if (!mPattern.isEmpty()) {
//重置状态 阻止画最后一个点与手指之间的连线
mPatternInProgress = false;
notifyPatternDetected();
//抬起的时候再绘制一次
invalidate();
}
}
抬起操作主要是判断存放点的集合是否为空,如果不为空,则重置状态,阻止画最后一个点与手指之间的连线。
detectAndAddHit方法
/**
* 根据x,y坐标确定是否点击到点上
*/
private Dot detectAndAddHit(float x, float y) {
//通过checkForNewHit判断坐标是否在一个点上
final Dot dot = checkForNewHit(x, y);
//如果在则返回点 如果不在则返回null
if (dot != null) {
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;
//重新计算行
/**
* 重新计算行
* 例如:从(0,1)直接到(2,1),想跳过(1,1)时,则通过此方法把第一行添加进去
*/
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);
}
/**
* 例如:从(0,1)直接到(2,1),想跳过(1,1)时,则通过此方法把第一行添加进去
*/
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;
}
detectAndAddHit方法主要是根据坐标拿到点,并判断前一个点与现在点中间是否还有必须经过但还有添加的点,如果有则优先添加中间点,如果没有则直接添加现在的点。
checkForNewHit方法
/**
* 检查是否滑动到点上
*/
private Dot checkForNewHit(float x, float y) {
//判断y坐标是否在点上
final int rowHit = getRowHit(y);
if (rowHit < 0) {
return null;
}
//判断x坐标是否在点上
final int columnHit = getColumnHit(x);
if (columnHit < 0) {
return null;
}
/**
* 如果已经选中,则不再选中
*/
if (mPatternDrawLookup[rowHit][columnHit]) {
return null;
}
//返回一个点对象
return Dot.of(rowHit, columnHit);
}
checkForNewHit方法主要是根据x,y坐标判断是否在一个点上,如果是则返回一个点对象。
getRowHit getColumnHit方法
/**
* 根据y坐标判断是否在某一个点的y坐标上
* if (y >= hitTop && y <= hitTop + hitSize)这行给点加了一个范围,在此范围内都算点上了点
*/
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坐标判断是否在某一个点的x坐标上
* if (x >= hitLeft && x <= hitLeft + hitSize)这行给点加了一个范围,在此范围内都算点上了点
*/
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;
}
这两个方法是给点加了一个范围,在此范围内都算点上了点。
addCellToPattern方法
private void addCellToPattern(Dot newDot) {
//将选中的点置为true
mPatternDrawLookup[newDot.mRow][newDot.mColumn] = true;
//给集合中添加选中的点
mPattern.add(newDot);
//开始点放大缩小动画(隐藏模式用)
startDotSelectedAnimation(newDot);
//回调给用户选中的点
notifyPatternProgress();
}
addCellToPattern方法是将选中的点添到集合中,并把mPatternDrawLookup中点的位置改为true。
以上就是按下 移动 抬起的主要方法,下面再来看下点和线是如何绘制的。
第三步
首先通过onSizeChanged方法来确定patternlockview的大小
onSizeChanged方法
@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;
}
通过onSizeChanged方法,获取到一个点占据的宽和高
然后就是onDraw绘制方法了,所有的绘制工作都是由它来完成,同时在滑动过程中,此方法也是在不断的调用。
onDraw方法
@Override
protected void onDraw(Canvas canvas) {
//拿到选中点的集合
ArrayList<Dot> pattern = mPattern;
int patternSize = pattern.size();
//拿到存放点是否选中的二维数组
boolean[][] drawLookupTable = mPatternDrawLookup;
//拿到路径对象
Path currentPath = mCurrentPath;
//清除所有的直线和曲线的路径,但保持内部数据结构,以便更快地重用。
currentPath.rewind();
//判断是否是隐藏模式
boolean drawPath = !mInStealthMode;
if (drawPath) {
//根据当前模式设置连接线的颜色
mPathPaint.setColor(getCurrentLineStateColor());
//是否绘制最后一个点与手指之间的连线
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]) {
return;
}
anyCircles = true;
//拿到某点的中心坐标
float centerX = getCenterXForColumn(dot.mColumn);
float centerY = getCenterYForRow(dot.mRow);
if (i != 0) {
DotState state = mDotStates[dot.mRow][dot.mColumn];
currentPath.rewind();
//当到第二个的时候,则lastX,lastY就是选中的第一个点的中心
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 && anyCircles) {
currentPath.rewind();
currentPath.moveTo(lastX, lastY);
currentPath.lineTo(mInProgressX, mInProgressY);
//把线连接上
canvas.drawPath(currentPath, mPathPaint);
}
}
//循环画点
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);
float size = dotState.mSize * dotState.mScale;
float translationY = dotState.mTranslateY;
drawCircle(canvas, (int) centerX, (int) centerY + translationY,
size, drawLookupTable[i][j], dotState.mAlpha);
}
}
}
drawCircle方法
private void drawCircle(Canvas canvas, float centerX, float centerY,
float size, boolean partOfPattern, float alpha) {
//如果隐藏模式则不走
if (partOfPattern && !isInStealthMode()) {
//设置点外环的颜色
mRingPaint.setColor(getCurrentDotStrokeColor(partOfPattern));
//画点的外环
canvas.drawCircle(centerX, centerY, size, mRingPaint);
}
//画点
mDotPaint.setColor(getCurrentDotStateColor(partOfPattern));
mDotPaint.setAlpha((int) (alpha * 255));
canvas.drawCircle(centerX, centerY, size / 2, mDotPaint);
}
在onDraw方法中对画什么颜色的点,什么颜色的线都做了判断。
至此整个绘制过程就算完成了,剩下的一些颜色的判断,各个状态的回调,各个模式都比较简单,就不再这里特别说明了。
贡献
这个库是从Aritra Roy的PatternLockView获取并添加了一些改进使其更加灵活,如果您发现bug或想改进它的任何方面,可以自由地用拉请求进行贡献。