主界面:
设置密码、验证密码、修改密码
fragment
实现方式,自定义View。
如何自定义View?自定义什么View?自定义View需要什么样子的功能?能否让它更加有可修改性?
参考资料:
Android 自定义控件实现九宫格解锁
自定义View的样式实现
获取界面大小,获取中心坐标,获取每个半径的大小
@Override
protected void onDraw(Canvas canvas) {
// super.onDraw(canvas);
drawDefaultCircle(canvas);
}
private void drawDefaultCircle(Canvas canvas) {
if (viewHeight == 0) { //获取view的长和宽。之后绘制圆形
Log.d(TAG, "必须在onDraw中,才能获得view的大小");
viewHeight = getMeasuredHeight();
viewWidth = getMeasuredWidth();
}
float minLength = Math.min(viewHeight, viewWidth); //画正方形式的九宫格
center_x = (getRight() - getLeft()) / 2;
center_y = (getBottom() - getTop()) / 2; //获得绘制的中心点
int pointNumber = getResources().getInteger(R.integer.point_number);
int rowNumber = (int) Math.sqrt(pointNumber); //画几排?每排几个?因为是正方形所以是排数和行数相同
circleRadius_default = minLength / rowNumber * 2 + rowNumber + 1; //2n个半径,每个圆相隔1个半径(带系统边界) = n+1
canvas.drawCircle(center_x, center_y,
circleRadius_default, paint_default); //先画一个看看
}
circleRadius_default = minLength / (rowNumber * 2 + rowNumber + 1);
private void init() {
paint_default = new Paint(Paint.ANTI_ALIAS_FLAG);
paint_default.setStyle(Paint.Style.STROKE); //描边画笔
paint_default.setColor(getContext().getColor(R.color.Color8328_1)); //画笔颜色
paint_default.setStrokeWidth(5);
//这个时候用getMeasuredHeight()是不是还不会显示的哇。因为View还没有初始化
viewHeight = getMeasuredHeight();
viewWidth = getMeasuredWidth();
}
@Override
protected void onDraw(Canvas canvas) {
// super.onDraw(canvas);
drawDefaultCircle(canvas);
}
private void drawDefaultCircle(Canvas canvas) {
if (viewHeight == 0) { //获取view的长和宽。之后绘制圆形
Log.d(TAG, "必须在onDraw中,才能获得view的大小");
viewHeight = getMeasuredHeight();
viewWidth = getMeasuredWidth();
}
float minLength = Math.min(viewHeight, viewWidth); //画正方形式的九宫格
// Log.d(TAG, "view的长度为:");
center_x = (getRight() - getLeft()) / 2;
center_y = (getBottom() - getTop()) / 2; //获得绘制的中心点
int pointNumber = getResources().getInteger(R.integer.point_number);
//画几排?每排几个?因为是正方形所以是排数和行数相同
int rowNumber = (int) Math.sqrt(pointNumber);
Log.d(TAG, "行列数 " + rowNumber);
//2n个半径,每个圆相隔1个半径(带系统边界) = n+1
circleRadius_default = minLength / (rowNumber * 2 + rowNumber + 1);
for (int i = 0; i < rowNumber; i++) {
//一排,修改y坐标
float point_y = center_y - (1 - i) * 3 * circleRadius_default;
for (int j = 0; j < rowNumber; j++) {
//修改x坐标
float point_x = center_x - (1 - j) * 3 * circleRadius_default;
canvas.drawCircle(point_x, point_y,
circleRadius_default, paint_default);
Log.d(TAG, "坐标位置 x:" + point_x + " y:" + point_y);
}
}
}
给自定义View添加点击事件
重写onTouchEvent
方法,在方法中可以获得当前触摸的位置和方式。模板代码如下
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
break;
case MotionEvent.ACTION_UP:
if (x + getLeft() < getRight() && y + getTop() < getBottom()) {
mListener.onClick(this);
mViewClick.onClick(x - startX, y - startY);
}
break;
}
return true;
}
Q&A:
长按滑动的时候,是怎么时时刻刻获得位置的呢?
还记得之前做的一个项目里,按住屏幕滑动的话,会不断的触发事件的。事实上,只要手在屏幕上发生动作,就会触发这个方法,所以不必在意。
定义实体类Point,让它进行绘制,判断是否发生状态的改变
程序的设计非常重要
我刚开始一直在犯错误,犯错误的问题就在于:
在什么情况下点会被选中,在什么情况下点会被清空
判断x,y在不在圆形内,每次在圆内,都代表圆的绘制情况需要改变。
很明显,这是错的
因为手会经过圆形范围,会多次被判断为在圆内
判断x,y上一次是不是在圆内,记录上一次的情况。如果这一次和上一次的情况不一样,说明选中的状态发生了改变。
的确发生了改变,但是这是单选的情况才是这么看。比如从圆内移动到圆外。的确是选择状态发生了改变。可惜在这里,从圆内到圆外说明移动到下一个点,依然是选中状态
记录上一次的状态,只有当上一次不包含,到这一次包含,才说明状态的改变。情况如下:
但是这依然是错的- -。正确的事务逻辑不是这样的。工具就只是工具,即使你知道怎么挥动斧头,但是也不能说明你会劈材(或者劈人)。
真的是很需要认真理解软件执行过程中会发生什么,这非常非常重要。
这个小练手项目逻辑还是有点复杂的,我之前就是犯了不考虑清楚逻辑直接上手的错误。不过也有对相关技能不熟练的原因,比如一开始我的确对怎么画出正确位置的圆有点懵。
上面是一个思考这个问题的简单过程,也是我最开始的思考草稿。后来在编写的时候,因为涉及到具体的实现,以及一些没有考虑清楚的点,所以增加了不少小细节。尽量在代码里说清楚
学习怎么良好的编码,设计模式,类的职责,各种各样 plz
我尝试自己分了类的职责,但是没有一个指导方法,所以挺难。应该多去看看书啊,比如代码大全,比如计算机编程艺术之类的。哎,我好菜。。。
三个类,LockView
类,myPoint
类,和一个自定义的数据结构myPointList
类
分别对应了不同的职责。LockView
主要用来画画;myPointList
类大概就是和点列表相关的操作,比如获得坐标,获得序号,维护被选择的点等等;myPoint
就是坐标和半径以及判断一个坐标是否在圆内。
稍微贴一下代码。myPoint
和myPointList
的代码是很容易理解的。
package com.example.ninepatch.utils;
public class myPoint2 {
private float f_x, f_y;
private float f_r;
public myPoint2(float f_x, float f_y, float f_r) {
this.f_x = f_x;
this.f_y = f_y;
this.f_r = f_r; }
public float getF_x() { return f_x; }
public float getF_y() { return f_y; }
public float getF_r() { return f_r; }
public boolean isInside(float f_touchX, float f_touchY) {
double distance = (Math.pow((f_touchX - this.f_x), 2) +
Math.pow(f_touchY - this.f_y, 2)) - Math.pow(f_r, 2); //如果距离大于半径,则>0
if (distance >= 0) {
return false; //距离大于半径,就没有包含
} else {
return true;
} }}
myPointList
部分的代码主要做的事情
public class myPointList {
private static final String TAG = "myPointList";
private int size, rows;
private myPoint2[] points; //点的集合
private ArrayList<Integer> selectList = new ArrayList<>(); //已经被选择的list,用order序号存
public myPointList(int size) {
this.size = size;
points = new myPoint2[this.size];
rows = (int) Math.sqrt(size); }
public myPoint2 findByXY(int position_x, int position_y) {
return points[position_x * rows + position_y]; }
public myPoint2 findByOrder(int order) {
return points[order]; }
public int getXByOrder(int order) {
return order - 3 * getYByOrder(order); }
public int getYByOrder(int order) {
return order / rows; }
public int getOrderByXY(int x, int y) {
return x * rows + y; }
public void addSeclectPoint(int x, int y) {
int order = getOrderByXY(x, y);
selectList.add(order); }
public void addSelectPoint(int order) {
selectList.add(order); }
public myPoint2 getLastSelectPoint() {
return findByOrder(selectList.get(selectList.size() - 1)); }
/**
* 用该点的序号查看它是否已经被选择了。
* 如果没有被选择,则重绘
* 如果被选择,但是是最后一个,没有行为
* 如果被选择,而是是次点,那就把尾点unSelect
* 如果已经被选择,且不是尾点和次点,则结束行为。
* @return
*/
public int checkSelected(int x, int y) {
int order = getOrderByXY(x, y);
return checkSelected(order); }
public int checkSelected(int order) {
for (int i = 0; i < selectList.size(); i++) {
if (selectList.get(i) == order) {
if (i == selectList.size() - 1) {
return ViewConfig.LASTSELECT;
} else if (i == selectList.size() - 2) {
return ViewConfig.LAST2SELECT;
} else {
return ViewConfig.PASTSELECT;
}
}
}
return ViewConfig.NEWSELECT; }
/**
* 查看尾点和次点的相对关系,以便于绘制触摸点的连线。
* @return
*/
//返回的方向
public int backDirection() {
if (selectList.size() >= 2) {
int lastX = getXByOrder(selectList.get(selectList.size() - 1));
int lastY = getYByOrder(selectList.get(selectList.size() - 1));
int last2X = getXByOrder(selectList.get(selectList.size() - 2));
int last2Y = getYByOrder(selectList.get(selectList.size() - 2));
if ((lastX - last2X) > 0) {
//尾点在次点右边
if ((lastY - last2Y) > 0) {
//尾点在次点下面
return ViewConfig.W2N;
} else if (lastY == last2Y) {
//尾点和次点同行
return ViewConfig.WEST;
} else {
//尾点在次点上方
return ViewConfig.S2W;
}
} else if (lastX == last2X) {
//尾点和次点在同一列
if ((lastY - last2Y) > 0) {
//尾点在次点下面
return ViewConfig.NORTH;
} else if (lastY == last2Y) {
//尾点和次点同行
return Integer.MIN_VALUE; //尾点和次点是同一个,绝对有问题
} else {
//尾点在次点上方
return ViewConfig.SOUTH;
}
} else {
//尾点在次点左边
if ((lastY - last2Y) > 0) {
//尾点在次点下面
return ViewConfig.N2E;
} else if (lastY == last2Y) {
//尾点和次点同行
return ViewConfig.EAST;
} else {
//尾点在次点上方
return ViewConfig.E2S;
}
}
}
else { // 只有一个点
return Integer.MAX_VALUE;
}
}
public void initPoints(float f_centerX, float f_centerY, float f_r) {
for (int order = 0; order < size; order++) {
int position_x = getXByOrder(order);
int position_y = getYByOrder(order);
// 从0起
float offset = (rows - 1) / 2;
float f_x = f_centerX - (offset - position_x) * 3 * f_r;
float f_y = f_centerY - (offset - position_y) * 3 * f_r;
points[order] = new myPoint2(f_x, f_y, f_r);
} }
/**
* 如果找到了包含的圆,就返回圆的序号order
* 如果没有找到圆,就返回最小值
* @param fx
* @param fy
* @return
*/
public int findIncludePoint(float fx, float fy) {
for (int order = 0; order < size; order++) {
if (points[order].isInside(fx, fy)) {
//如果包含
return order; } }
return Integer.MIN_VALUE; }
public ArrayList<Integer> getSelectList() { return selectList; }
public int getSize() { return size; }
public void removeLastSelect() { selectList.remove(selectList.size() - 1); }
}
而LockView
的代码是相对复杂的,复杂主要在逻辑,所以不理清楚逻辑是没有办法编写的。
绘制的时候,也就是在复写的onDraw()
方法中,涉及到重绘,所以需要考虑不同的情况。
而这一部分讲解的主要是touch事件
逻辑上就是先画圆形,然后根据当前确定的被选中的点来画圆
接着画线。画线的方式就是:点之间先连线,然后尾点和当前的touch点连线
@Override
protected void onDraw(Canvas canvas) {
if (DrawEvent == EventNew) {
f_viewHeight = getMeasuredHeight();
f_viewWidth = getMeasuredWidth();
float minLength = Math.min(f_viewHeight, f_viewWidth); //画正方形式的九宫格
f_centerX = (getRight() - getLeft()) / 2;
f_centerY = (getBottom() - getTop()) / 2; //获得绘制的中心点
f_r = minLength / (rows * 2 + rows + 1); //获得半径的长度。2n个半径,每个圆相隔1个半径(带系统边界) = n+1
points.initPoints(f_centerX, f_centerY, f_r); //初始化所有点
draw_defaultCircle(canvas);
}
if (DrawEvent == EventTouch) {
draw_defaultCircle(canvas);
draw_fillPoint(canvas);
draw_lines(canvas);
}
if (DrawEvent == EventCheck) {
}
if (DrawEvent == EventRepeat) {
}
}
但是怎么确定我的点究竟选没选中也是一个很大的问题。逻辑上有一些判断,比如我在尾点内沿着反方向滑动,应该是想要取消选择。这个东西涉及到移动的角度问题,分为8个方向,然后如果移动的方向是连线的方向,就取消选择当前尾点。
已知两点,求两点连线与水平线的夹角 - 这个文章里之所以需要一个负号,是因为android的象限和标准象限不同
@Override
public boolean onTouchEvent(MotionEvent event) {
f_touchX = event.getX();
f_touchY= event.getY();
int action = event.getAction();
int order; //判断触摸点在哪个圆内
switch (action) {
case MotionEvent.ACTION_DOWN:
DrawEvent = EventTouch;
order = points.findIncludePoint(f_touchX, f_touchY);
if (order != Integer.MIN_VALUE) {
//如果找到了
isLastInCircle = true;
points.addSelectPoint(order);
this.postInvalidate(); //重绘
}
break;
case MotionEvent.ACTION_MOVE:
Log.d(TAG, "onTouchEvent: action_move");
DrawEvent = EventTouch;
order = points.findIncludePoint(f_touchX, f_touchY);
if (order != Integer.MIN_VALUE) { //如果触摸点在圆内
isLastInCircle = true;
int checkState = points.checkSelected(order); //判断是否曾经被选择
if (checkState == ViewConfig.NEWSELECT) { //发现未被选择
points.addSelectPoint(order);
this.postInvalidate();
} else if (checkState == ViewConfig.LASTSELECT) {//发现是尾点
//绘线即可
this.postInvalidate();
} else if (checkState == ViewConfig.LAST2SELECT){ //发现是次点,消除当前尾点
points.removeLastSelect();
this.postInvalidate();
}else if (checkState == ViewConfig.PASTSELECT) { //发现是非尾点,非次点
//结束这一次密码解锁/设置
//如果是设置密码,不允许这么设置。如果是check密码,就进行比较
if (setPassword) {
DrawEvent = EventRepeat;
} else {
DrawEvent = EventCheck;
}
this.postInvalidate();
}
} else { //如果在圆外,要判断是否为取消选择尾点的路径
Log.d(TAG, "触摸点在圆外");
if (isLastInCircle) { //如果上一次是在圆中的,说明这是一次离开圆的行为,需要判断是否取消选择圆点。
isLastInCircle = false;
Log.d(TAG, "上一次触摸点在圆内");
int backDirection = points.backDirection(); //什么方向是取消选择的方向
Log.d(TAG, "取消选择的方向是:" + backDirection);
myPoint2 point1 = points.getLastSelectPoint();
float lastX = point1.getF_x(); //获得尾点的坐标
float lastY = point1.getF_y();
Log.d(TAG, "我的选择方向是: " + ViewConfig.getTowards(lastX, lastY, f_touchX, f_touchY));
if (ViewConfig.getTowards(lastX, lastY, f_touchX, f_touchY) == backDirection) {
//如果是返回方向
points.removeLastSelect();
this.postInvalidate();//重绘
} else {
//如果一直在圆外,不会取消选中的点
this.postInvalidate();
}
} else { //重绘线
this.postInvalidate();
}
}
break;
case MotionEvent.ACTION_UP:
DrawEvent = EventCheck;
this.postInvalidate();
break;
}
return true;
}
通过android 自定义View -传值,获取是否在设置密码,是否在检查密码,检查密码时候传递正确的密码等行为。然后设置重绘方法。实现最终效果如下