onDraw这个方法在自定义中尤其重要,我们可以measure之后通过Canvas进行绘制,九宫格解锁这个View现在已经被人脸跟指纹给替代了,但是做起来还是有点东西的。
下面就是做这个View的思路:
- 九个格子的布局
- 格子之间的连线
- 格子之间的连线所要考虑的问题
一 . 格子布局
之前写过onlayout来进行View的排放,那要继承于ViewGroup,这次我继承之View来实现。在onDraw中进行位置的摆放。整个的思路就是整出一个正方形,然后均与摆放9个格子的位置。本次摆放的方法是摆放一个5*5的矩形,即两个圆之间的间隔也是一个圆,这样方便了后期的计算。首先是计算每个球的圆心:
radius = (measureWidth - 100) / 5;
innerPadding = 50;
radius = radius / 2;
int centerX = innerPadding + radius, centerY = innerPadding + radius;
for (int i = 0; i < 9; i++) {
pointList.add(new Point(centerX, centerY));
centerX += 4 * radius;
if (centerX > (measureWidth - 100)) {
centerX = innerPadding + radius;
centerY += 4 * radius;
}
}
这里的操作就跟之前的Android onLayout()摆放位置是一个道理,然后在onDraw()中:
for (Point mPoint : pointList) {
if (mPoint.isSelected) {
paint.setColor(Color.BLUE);
} else {
paint.setColor(Color.WHITE);
}
canvas.drawCircle(mPoint.x, mPoint.y, radius, paint);
}
这里默认的是白色的,当被选中的时候换成蓝色的。很简单我们的九宫格就画出来了,那么看看如何进行交互的。
二. 格子之间的连线
第一反应是用canvas.drawLine()来实现,实际操作并没有什么用,因为当画到一个点的时候,出发点就是当前的这个点了,之前的线段用drawLine()并不好用绘制,这里要用Path来实现,通过LineTo到某个点然后drawPath():
canvas.drawPath(path, linePaint);
canvas.drawLine(startX, startY, endX, endY, linePaint);
onDraw中的剩余代码,第一句表示的是画线段,记录已经画出的线段,第二个则是记录画出的点,这里固定(startX,startY),改变 (endX, endY)来实现伸缩的线段。
我们来看一下onTouch方法。onTouch有Down, Move,Up,这三种是比较常用的方法,这个后面再进行拓展。
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
break;
case MotionEvent.ACTION_UP:
break;
}
return true;
}
看一下Down里面的方法:
setAllunSelected();
path.reset();
int i = getPoint(x, y);
if (DEFAULT_NUM == i) {
invalidate();
return false;
} else {
//说明点在点子上了
selectList.add(i);
pointList.get(i).setSelected(true);
path.moveTo(pointList.get(i).getX(), pointList.get(i).getY());
startX = pointList.get(i).getX();
startY = pointList.get(i).getY();
}
首先把之前的都清除掉用setAllunSelected()把所有点的状态置成白色的状态,再把path重置一下,int i = getPoint(x, y); 来计算一下当前按下的这个点是不是在某个九宫格中的某个圆里面,这时候把当前的点放进selectList集合里面并且把这个点的状态置成被选中的状态,这就成了我们可以记录的画出密码的线路,也就是我们的密码。因为是第一次按下去,所以把当前的startX,startY换成当前选中的点,做为我们的出发点。
后面紧跟onMove代码:
endX = x;
endY = y;
int j = getPoint(x, y);
if (DEFAULT_NUM != j) {
if(selectList.contains(j)){
invalidate();
break;
}
int missNum = getPointNum(j);
if(missNum!=DEFAULT_NUM && !selectList.contains(j) ){
selectList.add(missNum);
pointList.get(missNum).setSelected(true);
}
selectList.add(j);
pointList.get(j).setSelected(true);
path.lineTo(pointList.get(j).getX(), pointList.get(j).getY());
startX = pointList.get(j).getX();
startY = pointList.get(j).getY();
}
invalidate();
这里我们做的就是移动到当前的点我们就改成endX,endY,还是通过getPoint这个方法来判断是不是按在某个点里面:
private int getPoint(float x, float y) {
for (int i = 0; i < pointList.size(); i++) {
int j = getPosition(x, y, i);
if (DEFAULT_NUM != j) {
return j;
}
}
return DEFAULT_NUM;
}
private int getPosition(float x, float y, int position) {
Point point = pointList.get(position);
if (Math.hypot(Math.abs(x - point.x), Math.abs(y - point.y)) < radius) {
return position;
}
return DEFAULT_NUM;
}
三.连线要考虑的问题
getPoint()方法就是记录就是计算当前点距离是不是在某个我们画的圆里面,分别到9个圆心的距离就可以判断是不是在当前这个圆里面,然后可以确定是点击的第几个圆。后面我们进行了一次判断假如我们selectList中已经有了某个点,当我们再滑动到他的时候不进行任何操作。后面加了一个方法getMissNum这个方法是后面思考的时候加上的主要的实现就是看图1跟图2的区别:
实际区别就是能不能主动吸附到中间这个点的问题代码:
private int getPointNum(int position){
if(selectList.size()<1)
{
return DEFAULT_NUM;
}
int size =selectList.size();
int i = selectList.get(size-1);
Point p = pointList.get(position);
Point point = pointList.get(i);
//判断是不是由上往下,从左往右
boolean b = false ;
if(position>i){
b = true;
}
if(Math.abs(p.getX()-point.getX())!=4*radius ||Math.abs(p.getX()-point.getX())!=4*radius){
//说明不是相邻的两个点
if(Math.abs(p.getX()-point.getX()) == 0 ||Math.abs(p.getY()-point.getY())==0 ){
//说明一排或者一列中间有个没点到的
if(Math.abs(p.getY()-point.getY()) == 0) {
if(b){
return i+1;
}else{
return i-1;
}
}else {
if(b){
return i+3;
}else{
return i-3;
}
}
}else if(Math.abs(p.getX()-point.getX())==8*radius && Math.abs(p.getY()-point.getY())==8*radius){
//这里是对角线
return CENTER_NUM;
}
}
return DEFAULT_NUM;
}
本来想通过Path转换来看看某个点在不在这个Path区域中,后来实践之后发现并不能做的出来,也把代码贴出来:
private boolean pointInPath(Path path, Point point) {
RectF bounds = new RectF();
path.computeBounds(bounds, true);
Region region = new Region();
region.setPath(path, new Region((int) bounds.left, (int) bounds.top,
(int) bounds.right, (int) bounds.bottom));
return region.contains((int)point.x, (int)point.y);
}
最后就是将绘制好的密码扔出去,可以的话再添加个回掉把密码组给扔出去。
完整代码:
public class LockView extends View {
final int DEFAULT_NUM = -1;
final int CENTER_NUM = 4;
String TAG = "LockView";
int measureWidth;
private int innerPadding;
private int radius;
List<Point> pointList;
Paint paint;
Paint linePaint;
float startX, startY;
float endX, endY;
Path path;
ArrayList<Integer> selectList;
boolean dismiss = false;
boolean isMove = false;
public LockView(Context context) {
this(context, null);
}
public LockView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public LockView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
pointList = new ArrayList<>();
init();
}
private void init() {
paint = new Paint();
paint.setStrokeWidth(2);
paint.setStyle(Paint.Style.FILL);
paint.setAntiAlias(true);
paint.setColor(Color.WHITE);
linePaint = new Paint();
linePaint.setStrokeWidth(20);
linePaint.setStyle(Paint.Style.STROKE);
linePaint.setAntiAlias(true);
linePaint.setColor(Color.WHITE);
linePaint.setStrokeJoin(Paint.Join.ROUND);
//线条结束处绘制一个半圆
linePaint.setStrokeCap(Paint.Cap.ROUND);
selectList = new ArrayList<>();
path = new Path();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
int width = MeasureSpec.getSize(widthMeasureSpec);
int size = Math.min(width, height);
setMeasuredDimension(size, size);
measureWidth = getMeasuredWidth();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
Log.d(TAG, "onSizeChanged: " + measureWidth);
radius = (measureWidth - 100) / 5;
innerPadding = 50;
radius = radius / 2;
int centerX = innerPadding + radius, centerY = innerPadding + radius;
for (int i = 0; i < 9; i++) {
pointList.add(new Point(centerX, centerY));
centerX += 4 * radius;
if (centerX > (measureWidth - 100)) {
centerX = innerPadding + radius;
centerY += 4 * radius;
}
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if(dismiss){
setAllunSelected();
path.reset();
}
for (Point mPoint : pointList) {
if (mPoint.isSelected) {
paint.setColor(Color.BLUE);
} else {
paint.setColor(Color.WHITE);
}
canvas.drawCircle(mPoint.x, mPoint.y, radius, paint);
}
canvas.drawPath(path, linePaint);
canvas.drawLine(startX, startY, endX, endY, linePaint);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
setAllunSelected();
dismiss = false;
path.reset();
int i = getPoint(x, y);
if (DEFAULT_NUM == i) {
invalidate();
return false;
} else {
//说明点在点子上了
selectList.add(i);
pointList.get(i).setSelected(true);
path.moveTo(pointList.get(i).getX(), pointList.get(i).getY());
startX = pointList.get(i).getX();
startY = pointList.get(i).getY();
}
break;
case MotionEvent.ACTION_MOVE:
endX = x;
endY = y;
int j = getPoint(x, y);
if (DEFAULT_NUM != j) {
if(selectList.contains(j)){
invalidate();
break;
}
int missNum = getPointNum(j);
if(missNum!=DEFAULT_NUM && !selectList.contains(j) ){
selectList.add(missNum);
pointList.get(missNum).setSelected(true);
}
selectList.add(j);
pointList.get(j).setSelected(true);
path.lineTo(pointList.get(j).getX(), pointList.get(j).getY());
startX = pointList.get(j).getX();
startY = pointList.get(j).getY();
}
invalidate();
break;
case MotionEvent.ACTION_UP:
endX = startX;
endY = startY;
dismiss = true ;
invalidate();
break;
default:
break;
}
return true;
}
private int getPoint(float x, float y) {
for (int i = 0; i < pointList.size(); i++) {
int j = getPosition(x, y, i);
if (DEFAULT_NUM != j) {
return j;
}
}
return DEFAULT_NUM;
}
private int getPosition(float x, float y, int position) {
Point point = pointList.get(position);
if (Math.hypot(Math.abs(x - point.x), Math.abs(y - point.y)) < radius) {
return position;
}
return DEFAULT_NUM;
}
class Point {
float x;
float y;
boolean isSelected;
public Point(float x, float y) {
this.x = x;
this.y = y;
}
public float getX() {
return x;
}
public void setX(float x) {
this.x = x;
}
public float getY() {
return y;
}
public void setY(float y) {
this.y = y;
}
public boolean isSelected() {
return isSelected;
}
public void setSelected(boolean selected) {
isSelected = selected;
}
}
private void setAllunSelected() {
for (int i = 0; i < pointList.size(); i++) {
pointList.get(i).setSelected(false);
}
selectList.clear();
}
private int getPointNum(int position){
if(selectList.size()<1)
{
return DEFAULT_NUM;
}
int size =selectList.size();
int i = selectList.get(size-1);
Point p = pointList.get(position);
Point point = pointList.get(i);
//判断是不是由上往下,从左往右
boolean b = false ;
if(position>i){
b = true;
}
if(Math.abs(p.getX()-point.getX())!=4*radius ||Math.abs(p.getX()-point.getX())!=4*radius){
//说明不是相邻的两个点
if(Math.abs(p.getX()-point.getX()) == 0 ||Math.abs(p.getY()-point.getY())==0 ){
//说明一排或者一列中间有个没点到的
if(Math.abs(p.getY()-point.getY()) == 0) {
if(b){
return i+1;
}else{
return i-1;
}
}else {
if(b){
return i+3;
}else{
return i-3;
}
}
}else if(Math.abs(p.getX()-point.getX())==8*radius && Math.abs(p.getY()-point.getY())==8*radius){
//这里是对角线
return CENTER_NUM;
}
}
return DEFAULT_NUM;
}
private boolean pointInPath(Path path, Point point) {
RectF bounds = new RectF();
path.computeBounds(bounds, true);
Region region = new Region();
region.setPath(path, new Region((int) bounds.left, (int) bounds.top, (int) bounds.right, (int) bounds.bottom));
return region.contains((int)point.x, (int)point.y);
}
}
最后关于一些想使用的自定义属性自己可以直接定义一下,包括圆的样式什么的都是可以自己自定义搞一下的。这里就不写了。只是实现了一下这个简单的功能,对onDraw()方法中画图有一定认识。主要有画circle,画Path ,画Line的实战。