没图我说个球:
1.箭头
2.折线
3.直线
以上只能看到画出来的部分,平移的部分我没移植到这个demo下面(因为遇到了一些bug,懒得搞了),
但是下面会讲到平移部分。
简介:
该文章将涉及以下知识点:
1.path的绘制与平移(直线、箭头、可弯折直线);
2.path上每个点坐标的获取;
3.使用region获取填充型path(圆、椭圆、实心矩形等)的区域,得到该region后可判断你的path是否被点击;
PS:第3点我特地强调了“填充型path”,因为线条是无法用region获取区域的,获取到的区域永远是空(含泪实测证明)
正文:
1.绘制与平移
在主类中重写onTouchEvent方法,根据flag的值判断画哪种类型的view,然后调用该view的setPath方法进行绘制
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mLastX = (int) event.getRawX();
mLastY = (int) event.getRawY();
break;
case MotionEvent.ACTION_UP:
mX = (int) event.getX();
mY = (int) event.getY();
rawX = (int) event.getRawX();
rawY = (int) event.getRawY();
if(!isEnd){
switch (flag){
case FLAG_DRAW_ARROW:
arrowView.setSetTouchArea(true);
break;
case FLAG_DRAW_LINE:
customLine.setSetTouchArea(true);
break;
case FLAG_DRAW_BENDABLE_LINE:
//do nothing
break;
}
flag = FLAG_DRAW_NOTHING;
isEnd = true;
}
break;
case MotionEvent.ACTION_MOVE:
mMoveX = (int) event.getRawX();
mMoveY = (int) event.getRawY();
if(!isEnd){
switch (flag){
case FLAG_DRAW_ARROW:
arrowView.clear();
arrowView.setPath(mLastX,mLastY,mMoveX,mMoveY);
break;
case FLAG_DRAW_LINE:
customLine.setPath(mLastX,mLastY,mMoveX,mMoveY);
break;
case FLAG_DRAW_BENDABLE_LINE:
bendableLine.setPath(mLastX,mLastY,mMoveX,mMoveY);
break;
}
}
break;
}
return true;
}
直线的setPath方法以及onDraw实现:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawPath(mPath, mPaint);
}
public void setPath(float startX, float startY, float endX, float endY) {
mPath.reset();
mPath.moveTo(startX, startY);
mPath.lineTo(endX, endY);
invalidate();
}
箭头的:
@Override
protected void onDraw(Canvas canvas) {
if(hasOffset){
canvas.drawPath(path,paint);
canvas.drawPath(arrowPath,arrowPaint);
return;
}
setArrowPath();
canvas.drawPath(path, paint);
canvas.drawPath(arrowPath, arrowPaint);
}
/**
* 画箭头
*/
public void setArrowPath() {
double H = 18; // 箭头高度
double L = 13.5; // 底边的一半
double angle = Math.atan(L / H); // 箭头角度
double arrowLength = Math.sqrt(L * L + H * H); // 箭头的长度
//箭头就是个三角形,我们已经有一个点了,根据箭头的角度和长度,确定另外2个点的位置
double[] point1 = rotateVec(endX - startX, endY - startY, angle, arrowLength);
double[] point2 = rotateVec(endX - startX, endY - startY, -angle, arrowLength);
double point1_x = endX - point1[0];
double point1_y = endY - point1[1];
double point2_x = endX - point2[0];
double point2_y = endY - point2[1];
int x3 = (int) point1_x;
int y3 = (int) point1_y;
int x4 = (int) point2_x;
int y4 = (int) point2_y;
// 画线
arrowPath.moveTo(endX, endY);
arrowPath.lineTo(x3, y3);
arrowPath.lineTo(x4, y4);
arrowPath.close();
}
// 计算
/**
* @param diffX X的差值
* @param diffY Y的差值
* @param angle 箭头的角度(箭头三角形的线与直线的角度)
* @param arrowLength 箭头的长度
*/
public double[] rotateVec(float diffX, float diffY, double angle, double arrowLength) {
double arr[] = new double[2];
// 下面的是公式,得出的是以滑动出的线段末点为中心点旋转angle角度后,线段起点的坐标,这个旋转后的线段也就是“变长了的箭头的三角形的一条边”
//推导见注释1
double x = diffX * Math.cos(angle) - diffY * Math.sin(angle);
double y = diffX * Math.sin(angle) + diffY * Math.cos(angle);
double d = Math.sqrt(x * x + y * y);
//根据相似三角形,得出真正的箭头三角形顶点坐标,这里见注释2
x = x / d * arrowLength;
y = y / d * arrowLength;
arr[0] = x;
arr[1] = y;
return arr;
}
public void setPath(float startX, float startY, float endX, float endY) {
path.moveTo(startX,startY);
path.lineTo(endX,endY);
this.startX = startX;
this.startY = startY;
this.endX = endX;
this.endY = endY;
invalidate();
}
public void clear() {
path.reset();
arrowPath.reset();
}
折线的:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawPaths(canvas);
}
public void setPath(float startX, float startY, final float endX, final float endY) {
if (pathList.size() <= 0) {
Path path = new Path();
path.moveTo(startX, startY);
path.lineTo(endX, endY);
lastStartX = startX;
lastStartY = startY;
lastEndX = endX;
lastEndY = endY;
pathList.add(path);
currentPath = path;
} else {
LogUtil.d(TAG+" lastEndx = "+lastEndX+" , lastEndY = "+lastEndY+" , endX = "+endX+" , endY = "+endY);
if (Math.abs(lastEndX-endX) <=2 && Math.abs(lastEndY-endY) <= 2) {
if(isTimerRunning){
return;
}
//开启悬停计时
if (mTimer == null) {
mTimer = new Timer();
}
mTimer.schedule(new TimerTask() {
@Override
public void run() {
LogUtil.d(TAG+" do timerTask");
Path path = new Path();
path.moveTo(lastEndX, lastEndY);
path.lineTo(endX, endY);
lastStartX = lastEndX;
lastStartY = lastEndY;
lastEndX = endX;
lastEndY = endY;
pathList.add(path);
currentPath = path;
currentPathIndex++;
LogUtil.d(TAG + " 生成新直线 : size = " + pathList.size() + " , currentIndex = " + currentPathIndex+" end timer");
mHandler.sendEmptyMessage(0);
isTimerRunning = false;
}
}, STANDBY_SECONDS);
isTimerRunning = true;
LogUtil.d(TAG+" run timer");
} else {
currentPath.reset();
currentPath.moveTo(lastStartX, lastStartY);
currentPath.lineTo(endX, endY);
LogUtil.d(TAG + " 更新直线 line to " + endX + ", " + endY);
lastEndX = endX;
lastEndY = endY;
pathList.set(currentPathIndex, currentPath);
}
}
invalidate();
}
平移:
平移用到的是path自带方法offset(dx,dy),参数就是x轴与y轴的偏移量。
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (isContains(event.getRawX(),event.getRawY())) {
LogUtil.i(TAG + " touch down id = " + identity);
if (onLineTouchListener != null) {
onLineTouchListener.onLineTouch(this);
}
lastX = (int) event.getRawX();
lastY = (int) event.getRawY();
canMove = true;
}else{
canMove = false;
return false;
}
break;
case MotionEvent.ACTION_MOVE:
if(canMove){
int tempRawX = (int) event.getRawX();
int tempRawY = (int) event.getRawY();
int dx = tempRawX - lastX;
int dy = tempRawY - lastY;
lastX = tempRawX;
lastY = tempRawY;
dxTemp += dx;
dyTemp += dy;
dxTotal += dx;
dyTotal += dy;
// this.setTranslationX(dxTotal);
// this.setTranslationY(dyTotal);
mPath.offset(dx,dy);
invalidate();
}
break;
case MotionEvent.ACTION_UP:
if(dxTemp != 0 || dyTemp != 0){
setSetTouchArea(true);
dxTemp = 0;
dyTemp = 0;
}
break;
}
return super.onTouchEvent(event);
}
2.path上点坐标的获取
这里我用直线举个栗子,其他path的获取都是一样的方式。
先讲讲原理:
这里用到一个官方提供的类,叫PathMeasure,通过将它与path绑定,能够获取到path的长度,以及path上距离起点distance的点的坐标,然后我们可以写一个循环,获取path上所有点的坐标。
//用list存放path上的点坐标
private List<float[]> pathPosList = new ArrayList<>;
//绑定path
mPathMeasure.setPath(mPath, false);
/*这里我每次循环完不是i++,而是i+=10,因为如果用i++会得到上千个点坐标,我不需要那么高的精度,这里根据自己需求调整*/
for (int i = 0; i < mPathMeasure.getLength(); i+=10) {
float[] pos = new float[2];
float[] tan = new float[2];
mPathMeasure.getPosTan(i, pos, tan);
pathPosList.add(pos);
}
3.使用Region获取path所占用的区域(对线条无用)
private void getPathRegion(){
Region globalRegion = new Region(0,0,ScreenUtil.screenWidth,ScreenUtil.screenHeight);
Region mPathRegion = new Region();
mPathRegion.setPath(mPath,globalRegion);
}
代码很简单,解释下原理,region.setPath方法,获取的是传入的path与传入的region的交集。
总结:
用path基本想画啥都能画出来,尤其是典型的几何图形,官方都封装好了相应的方法。如果想要做path画出来的非规则图形的点击,填充图形用Region,线条用点坐标来判断。
源码下载地址:https://download.youkuaiyun.com/download/yonghuming_jesse/11164357
以上。
如果有我说的不够详细,你没看懂的地方,请在下方留言,博主会尽快回复。