很早以前就想自己写一个九宫格解锁了,现在终于一鼓作气安排上了,难度在自定义view里面算是中等吧。主要是要有实现的思路,然后一步一步去实现你的想法,其中考验的就是你的算法能力。
演示效果:
Github代码地址
实现思路
1.创建一个Dot类存每个圆格的圆心坐标。
2.在onmeasure()方法中设置圆的半径,和9个圆的中心点。
3.在onTouchEvent()方法中,实时对当前触摸点和9个圆做碰撞检测,如果进入该圆,就将该圆序号存储起来。
4.在onDraw()方法中,绘制每个外圆环和实心圆。遍历存储的点绘制路径,以及更改圆的颜色。
核心点
在onTouch事件中,需要实时对触碰点和9个圆做碰撞检测,判定方法为触碰点到圆心之前的距离小于半径,即为经过该圆,需要用到Api有次方公式Math.pow()和开方公司Math.sqrt()。
主要代码实现(思路实现见注释)
/**
* create by libo
* create on 2020/7/24
* description 九宫格解锁自定义view
*/
public class UnlockNineSquaresView extends View {
/**
* 记录9个点的坐标集合
*/
private List<Dot> dots = new ArrayList<>();
/**
* 按照顺序记录需要连线的dot的序号,使用LinkedHashSet从而更满足有序不重复的特点
*/
private LinkedHashSet<Integer> drawDots = new LinkedHashSet();
private final int DOT_COUNT = 9;
/* 自身宽度高度 */
private int width;
/**
* 外圆环画笔
*/
private Paint circlePaint;
/**
* 内实心圆画笔
*/
private Paint innerDotPaint;
/**
* 内实心半透明画笔
*/
private Paint transparentPaint;
/** 连线画笔 */
private Paint linePaint;
/** 外圆半径 */
private int outerCircleRadius;
/** 内圆半径 */
private int innerCircleRadius;
private int innerTransRadius;
/** 未选中颜色 */
private int normalColor;
/** 选中颜色 */
private int checkedColor;
/**
* 每个单元个宽度
*/
private int unitWidth;
private Path linePath;
private float curX, curY;
/** 解锁密码数字字符串,默认密码123456 */
private String password = "123456";
private OnUnlockListener onUnlockListener;
public UnlockNineSquaresView(Context context) {
super(context);
init();
}
public UnlockNineSquaresView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
width = MeasureSpec.getSize(widthMeasureSpec);
unitWidth = width / 6; //需要固定为width的1/6
outerCircleRadius = width / 10;
innerCircleRadius = width / 45;
innerTransRadius = width / 30;
initDotParams();
setMeasuredDimension(width, width);
}
private void init() {
initPaint();
}
private void initPaint() {
normalColor = getResources().getColor(R.color.blue);
checkedColor = getResources().getColor(R.color.deep_blue);
circlePaint = new Paint();
circlePaint.setColor(normalColor);
circlePaint.setStyle(Paint.Style.STROKE);
circlePaint.setStrokeWidth(3);
circlePaint.setAntiAlias(true);
innerDotPaint = new Paint();
innerDotPaint.setColor(normalColor);
innerDotPaint.setAntiAlias(true);
transparentPaint = new Paint();
transparentPaint.setColor(getResources().getColor(R.color.trans_blue));
transparentPaint.setAntiAlias(true);
linePaint = new Paint();
linePaint.setColor(checkedColor);
linePaint.setStyle(Paint.Style.STROKE);
linePaint.setStrokeWidth(6);
linePaint.setAntiAlias(true);
linePath = new Path();
}
/**
* 设置各个dot的位置
*/
private void initDotParams() {
drawDots.clear();
dots.clear(); //重复调用onMeasure需要重置dots
//根据行列值来设置当前横纵坐标 (j,i)
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
//i表行数,j表列数,当前为第i行j列位置
int left = getLeft() + (j * 2 + 1) * unitWidth;
int top = getTop() + (i * 2 + 1) * unitWidth;
dots.add(new Dot(left, top));
}
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
collisionDetection(event.getX(), event.getY());
break;
case MotionEvent.ACTION_UP:
resetState();
break;
}
postInvalidate();
return true;
}
/**
* 实时与每个圆碰撞检测
* @param curX 当前触摸x位置
* @param curY 当前触摸y位置
*/
private void collisionDetection(float curX, float curY) {
if (drawDots.size() == password.length()) { //输完密码结束碰撞检测
return;
}
this.curX = curX;
this.curY = curY;
for (int i=0;i<dots.size();i++) {
//遍历每个圆判断当前触摸点是否在某个圆之内
double difX = Math.pow(dots.get(i).x - curX, 2);
double difY = Math.pow(dots.get(i).y - curY, 2);
if (Math.sqrt(difX + difY) <= outerCircleRadius) {
//触摸点与圆心距离小于半径,即判断为触摸点在圆内
if (!drawDots.contains(i)) { //避免重复添加
drawDots.add(i);
}
checkPsdCorrect();
}
}
}
/**
* 校验密码是否正确
*/
private void checkPsdCorrect() {
if (drawDots.size() != password.length()) {
return;
}
//输完密码,去验证密码是否正确
StringBuilder stringBuilder = new StringBuilder();
for (int num : drawDots) {
stringBuilder.append(num+1);
}
if (stringBuilder.toString().equals(password)) { //stringBuilder.toString()为当前输入密码
if (onUnlockListener != null) {
onUnlockListener.unlockSuccess();
}
} else {
if (onUnlockListener != null) {
onUnlockListener.unlockFail();
virate();
}
}
}
/**
* 解锁失败调用一次震动
*/
private void virate() {
Vibrator vibrator = (Vibrator)getContext().getSystemService(Context.VIBRATOR_SERVICE);
long [] pattern = {100,400,100,400};
vibrator.vibrate(pattern,-1);
}
/**
* 每次抬手需要重置最初状态,即未输密码状态
*/
private void resetState() {
drawDots.clear(); //选中圆清除
linePath.reset(); //连线清除
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
circlePaint.setColor(getResources().getColor(R.color.blue));
innerDotPaint.setColor(getResources().getColor(R.color.blue));
for (int i = 0; i < DOT_COUNT; i++) {
canvas.drawCircle(dots.get(i).x, dots.get(i).y, outerCircleRadius, circlePaint); //画每个dot的外圆
//画每个dot的内圆
canvas.drawCircle(dots.get(i).x, dots.get(i).y, innerCircleRadius, innerDotPaint); //画每个dot的内圆
}
//path移动到第一个点
int curPos;
if (drawDots.iterator().hasNext()) {
curPos = drawDots.iterator().next();
linePath.moveTo(dots.get(curPos).x, dots.get(curPos).y);
}
circlePaint.setColor(getResources().getColor(R.color.deep_blue));
innerDotPaint.setColor(getResources().getColor(R.color.deep_blue));
//对已经存储的点按照顺序连线
for (Integer drawDot : drawDots) {
linePath.lineTo(dots.get(drawDot).x, dots.get(drawDot).y);
canvas.drawCircle(dots.get(drawDot).x, dots.get(drawDot).y, outerCircleRadius, circlePaint); //画每个dot的外圆
canvas.drawCircle(dots.get(drawDot).x, dots.get(drawDot).y, innerCircleRadius, innerDotPaint); //画每个dot的内圆
canvas.drawCircle(dots.get(drawDot).x, dots.get(drawDot).y, innerTransRadius, transparentPaint); //画每个dot的透明圆
}
canvas.drawPath(linePath, linePaint);
}
public void setPassword(String password) {
this.password = password;
}
/**
* 每格圆形类
*/
class Dot {
private int x;
private int y;
public Dot(int x, int y) {
this.x = x;
this.y = y;
}
}
public void setOnUnlockListener(OnUnlockListener onUnlockListener) {
this.onUnlockListener = onUnlockListener;
}
interface OnUnlockListener {
void unlockSuccess();
void unlockFail();
}
}
MainActivity调用:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final TextView tvResult = findViewById(R.id.tv_show_result);
TextView tvTips = findViewById(R.id.tv_tips);
tvTips.setText("当前密码为Z字型");
UnlockNineSquaresView unlockNineSquaresView = findViewById(R.id.unlockview);
unlockNineSquaresView.setPassword("1235789");
unlockNineSquaresView.setOnUnlockListener(new UnlockNineSquaresView.OnUnlockListener() {
@Override
public void unlockSuccess() {
tvResult.setText("密码正确");
}
@Override
public void unlockFail() {
tvResult.setText("密码错误");
}
});
}
}
后续优化点:
在今后的优化中,还是朝着选中状态的优化,密码输入检验更完善,密码校验成功变绿色,失败变红色。然后对代码进行封装,实现更多自定义属性方法使用,这些方面进行持续优化。