Android中绘制简单几何图形和路径Path


背景

马上就到2018年过年了,然后我又刚好有兴致,就来玩玩Android中的简单几何图形的绘制和使用Path类来绘制路径。

Path和Canvas

在Android中,和我们平时画图一样是有画笔和画布的,Path是画笔,Canvas是画布。与画的样式属性有关,如大小或者颜色等,是由Path来完成的;与画的形状,即画什么东西是由Canva完成的。关于这两个类的各个属性和方法的具体使用,可以浏览爱哥的博客几篇文章。在这里,只是用它们简单的几个函数画一些简单的图形,最后还会给出一个综合一点的demo,主要是为了加强认识绘制时坐标关系。

先贴上我的代码:
布局文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:id="@+id/root_draw_view"
android:gravity="center_horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">

<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="有点意思"/>

<com.example.hyj.ht_test.widget.draw.MyDrawView
android:layout_width="300dp"
android:layout_height="300dp" />

</LinearLayout>

MyDrawView.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
public class MyDrawView extends View {

private Paint mPointPaint;
private float[] mFPts;
private RectF mRectF;
private RectF mRectOvalF;
private RectF mRightBottomRectF;
private Path mPath;
private Path mPath1;

public MyDrawView(Context context) {
super(context);
init();
}

public MyDrawView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}

public MyDrawView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}

private int mPointStrokeWidth;

private void init() {
mPointStrokeWidth = 20;
mPointPaint = new Paint();
mPointPaint.setColor(Color.RED);
mPointPaint.setStrokeWidth(mPointStrokeWidth);
mPointPaint.setStyle(Paint.Style.FILL);

mPath = new Path();
mPath1 = new Path();
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);

int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);

int widthResult = 100;
int heightResult = 100;

if(widthMode != MeasureSpec.AT_MOST) {
widthResult = widthSize;
}

if(heightMode != MeasureSpec.AT_MOST) {
heightResult = heightSize;
}

int resultSize = widthResult > heightResult
? heightResult : widthResult;

setMeasuredDimension(resultSize, resultSize);
}


@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mFPts = new float[] {
0, 0,
getMeasuredWidth() / 2, 0,
getMeasuredWidth(), 0,
getMeasuredWidth(), getMeasuredHeight() / 2,
getMeasuredWidth(), getMeasuredHeight(),
getMeasuredWidth() / 2, getMeasuredHeight(),
0, getMeasuredHeight(),
0, getMeasuredHeight() /2,
getMeasuredHeight() / 2, getMeasuredHeight() /2
};
mRectF = new RectF(0, 0, getMeasuredWidth(), getMeasuredHeight());
mRectOvalF = new RectF(mPointStrokeWidth, mPointStrokeWidth,
getMeasuredWidth() - mPointStrokeWidth, getMeasuredHeight() / 2);
mRightBottomRectF = new RectF(getMeasuredWidth() / 2, getMeasuredHeight() /2,
getMeasuredWidth() - mPointStrokeWidth, getMeasuredHeight() - mPointStrokeWidth);
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);

canvas.drawLine(mFPts[0], mFPts[1], mFPts[2], mFPts[2], mPointPaint);
mPointPaint.setColor(Color.BLUE);
canvas.drawLines(mFPts, mPointPaint);

}
}

MyDrawView中没有考虑padding的影响。

画点

几何图形中,最简单的就是点了,首先画点。

drawPoint(float x, float y, Paint paint)
drawPoints(float[] pts, Paint paint)
drawPoints(float[] pts, int offset, int count, Paint paint)

x是点的横坐标,y是点的纵坐标。坐标的点也可以放到数组pts中,可见数组的个数一般是偶数个,offset是开始绘制前,数组中忽略的元素个数。count是忽略了offset个点后,开始取count个元素来绘制点。

1
2
3
canvas.drawPoints(mFPts, mPointPaint);
mPointPaint.setColor(Color.BLUE);
canvas.drawPoint(getMeasuredWidth() / 2, 0, mPointPaint);

图片

画线

由点组成线,两点确定一条直线。

drawLine(float startX, float startY, float stopX, float stopY, Paint paint)
drawLines(float[] pts, int offset, int count, Paint paint)
drawLines(float[] pts, Paint paint)

第一个是,直接指定直线的两个点坐标。pts是点的坐标,每两个数组元素确定一个点坐标,每四个元素确定直线的两个点的坐标。

1
2
3
canvas.drawLine(mFPts[0], mFPts[1], mFPts[2], mFPts[2], mPointPaint);
mPointPaint.setColor(Color.BLUE);
canvas.drawLines(mFPts, mPointPaint);

图片

画矩形

由线可以组成面。矩形可以是长方形,也可以是正方形。

RectF和Rect的区别是参数的类型不同,RectF的参数类型是float,Rect的参数类型是int。

drawRect(float left, float top, float right, float bottom, Paint paint)
drawRect(float left, float top, float right, float bottom, Paint paint)
drawRect(Rect r, Paint paint)
drawRect( RectF rect, Paint paint)

也就是,可以在RectF或者Rect中指定好顶点坐标再传给drawRect,也可以在drawRect方法中直接指定顶点坐标。

1
2
mRectF = new RectF(0, 0, getMeasuredWidth(), getMeasuredHeight());
canvas.drawRect(mRectF, mPointPaint);

代码说明,第一行代码是在onSizeChanged重写方法中的,第二行代码是在onDraw方法中的。因为onDraw方法是会不断被调用的,不适合在里面创建对象。

图片

圆角矩形

圆角矩形是在矩形的基础上生成的。

drawRoundRect(RectF rect, float rx, float ry, Paint paint)
drawRoundRect(float left, float top, float right, float bottom, float rx, float ry, Paint paint)

rx是生成圆角的椭圆的X轴半径
ry是生成圆角的椭圆的Y轴半径

1
canvas.drawRoundRect(mRectF, getMeasuredWidth() / 4, getMeasuredHeight() / 4, mPointPaint);

图片

画圆

圆要指定圆心的坐标和半径的大小。

drawCircle(float cx, float cy, float radius, Paint paint)

cx和cy分别是圆心的横坐标和纵坐标,radius为半径。

1
2
canvas.drawCircle(getMeasuredWidth() / 2, getMeasuredHeight() / 2,
getMeasuredHeight() / 2 - mPointStrokeWidth, mPointPaint);

图片

画椭圆

椭圆是在矩形基础上生成的,以矩形的长为长轴,矩形的宽为短轴。特殊的,当长轴等于短轴时,椭圆就是圆。

drawOval(RectF oval, @NonNull Paint paint)
drawOval(float left, float top, float right, float bottom, Paint paint)

1
2
3
4
mRectOvalF = new RectF(mPointStrokeWidth, mPointStrokeWidth,
getMeasuredWidth() - mPointStrokeWidth, getMeasuredHeight() / 2);

canvas.drawOval(mRectOvalF, mPointPaint);

图片

画弧

弧是在椭圆上按一定角度截取的一部分。

drawArc(RectF oval, float startAngle, float sweepAngle, boolean useCenter, Paint paint)
drawArc(float left, float top, float right, float bottom, float startAngle,
float sweepAngle, boolean useCenter, Paint paint)

oval是椭圆基于的矩形顶点的矩阵,或者在方法中直接指定四个顶点,startAngle是截取的起始角度,sweepAngle是弧持续的角度,useCenter是否显示长短半径。

1
canvas.drawArc(mRectOvalF, 0, 90, true, mPointPaint);

图片

1
canvas.drawArc(mRectOvalF, 0, 90, false, mPointPaint);

图片

Path

在View的绘制过程中,有一个类叫做Path,Path可以帮助我们实现很多自定义形状的路径,特别是配合xfermode属性来使用的时候,可以实现很多效果。

moveTo

路径开始绘制的点叫起始点坐标,默认是(0,0)。可以使用moveTo将绘制路径的起始点移动到某个位置。moveTo不进行绘制,一般用来移动画笔。

lineTo

lineTo用来绘制一条直线路径。

1
2
3
mPath.moveTo(getMeasuredWidth()/ 2, getMeasuredHeight() / 2);
mPath.lineTo(getMeasuredWidth(), getMeasuredHeight());
canvas.drawPath(mPath, mPointPaint);

直线路径的起始点是(getMeasuredWidth()/ 2, getMeasuredHeight() / 2),终点是(getMeasuredWidth(), getMeasuredHeight())

图片

quadTo

quadTo用来画由一个控制点控制的贝塞尔曲线。

1
2
3
mPath.moveTo(mPointStrokeWidth, getMeasuredHeight() / 2);
mPath.quadTo(0, 0, getMeasuredWidth() / 2, mPointStrokeWidth);
canvas.drawPath(mPath, mPointPaint);

起始点是(mPointStrokeWidth, getMeasuredHeight() / 2),控制点是(0, 0),终点是(getMeasuredWidth() / 2, mPointStrokeWidth)

图片

cubicTo

cubicTo用来画由两个控制点控制的贝塞尔曲线。

1
2
3
4
mPath.moveTo(mPointStrokeWidth, getMeasuredHeight() / 2);
mPath.cubicTo(0, 0, getMeasuredWidth() / 2, mPointStrokeWidth,
getMeasuredWidth(), getMeasuredHeight() / 2);
canvas.drawPath(mPath, mPointPaint);

起始点是(mPointStrokeWidth, getMeasuredHeight() / 2),两个控制点是(0, 0)和(getMeasuredWidth() / 2, mPointStrokeWidth),终点是(getMeasuredWidth(), getMeasuredHeight() / 2)。

图片

arcTo

arcTo用来画一条圆弧路径。与前面画圆弧一样的,圆弧是截取椭圆的一部分,而椭圆是基于矩形的。

1
2
3
4
mRectOvalF = new RectF(mPointStrokeWidth, mPointStrokeWidth,
getMeasuredWidth() - mPointStrokeWidth, getMeasuredHeight() / 2);
mPath.arcTo(mRectOvalF, 0, 90, false);
canvas.drawPath(mPath, mPointPaint);

和刚开始的圆弧参数定义一样,指定基于的矩形的四个顶点,startAngle截取的起始角度,sweepAngle弧持续的角度,useCenter是否显示长短半径。

图片

Path的addArc、addRoundRect、addOval、addRect、addCircle

它们实现的几何路径,可以自己尝试一下。

Path.Op

在开头,mPointPaint首先设置画笔的样式为描边STROKE,后面为了更好看出Path.Op的效果会改为FILL填充。

1
2
3
4
5
6
7
mRightBottomRectF = new RectF(getMeasuredWidth() / 2, getMeasuredHeight() /2, getMeasuredWidth() - mPointStrokeWidth, getMeasuredHeight() - mPointStrokeWidth);

mPath.addCircle(getMeasuredWidth() / 2, getMeasuredHeight() / 2,
getMeasuredHeight() / 2 - mPointStrokeWidth, Path.Direction.CCW);
canvas.drawPath(mPath, mPointPaint);
mPath1.addRect(mRightBottomRectF, Path.Direction.CCW);
canvas.drawPath(mPath1, mPointPaint);

图片

Path.Op.DIFFERENCE

1
2
3
4
5
6
mPath.addCircle(getMeasuredWidth() / 2, getMeasuredHeight() / 2,
getMeasuredHeight() / 2 - mPointStrokeWidth,
Path.Direction.CCW);
mPath1.addRect(mRightBottomRectF, Path.Direction.CCW);
mPath.op(mPath1, Path.Op.DIFFERENCE);
canvas.drawPath(mPath, mPointPaint);

图片

Path.Op.INTERSECT

1
2
3
4
5
6
mPath.addCircle(getMeasuredWidth() / 2, getMeasuredHeight() / 2,
getMeasuredHeight() / 2 - mPointStrokeWidth,
Path.Direction.CCW);
mPath1.addRect(mRightBottomRectF, Path.Direction.CCW);
mPath.op(mPath1, Path.Op.INTERSECT);
canvas.drawPath(mPath, mPointPaint);

图片

Path.Op.REVERSE_DIFFERENCE

1
2
3
4
5
6
mPath.addCircle(getMeasuredWidth() / 2, getMeasuredHeight() / 2,
getMeasuredHeight() / 2 - mPointStrokeWidth,
Path.Direction.CCW);
mPath1.addRect(mRightBottomRectF, Path.Direction.CCW);
mPath.op(mPath1, Path.Op.REVERSE_DIFFERENCE);
canvas.drawPath(mPath, mPointPaint);

图片

Path.Op.XOR

1
2
3
4
5
6
mPath.addCircle(getMeasuredWidth() / 2, getMeasuredHeight() / 2,
getMeasuredHeight() / 2 - mPointStrokeWidth,
Path.Direction.CCW);
mPath1.addRect(mRightBottomRectF, Path.Direction.CCW);
mPath.op(mPath1, Path.Op.XOR);
canvas.drawPath(mPath, mPointPaint);

图片

最后,例子###

(一)例子一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
public class MyDrawView extends View {

private Paint mGraphPaint;
private Paint mPointPaint;
private Paint mRectPaint;
private RectF mRectF;
private Paint mLinesPaint;
private float mFPts[];
private float mFLinePts[];
private Path mPath;

public MyDrawView(Context context) {
super(context);
init();
}

public MyDrawView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}

public MyDrawView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}

private int mPointStrokeWidth;

private void init() {
mGraphPaint = new Paint();
mGraphPaint.setColor(Color.GREEN);
mGraphPaint.setStrokeWidth(5);
mGraphPaint.setStyle(Paint.Style.STROKE);
mGraphPaint.setShadowLayer(50, 30,30, Color.BLUE);

mPointStrokeWidth = 20;
mPointPaint = new Paint();
mPointPaint.setColor(Color.RED);
mPointPaint.setStrokeWidth(mPointStrokeWidth);
mPointPaint.setStyle(Paint.Style.FILL);

mRectPaint = new Paint();
mRectPaint.setColor(Color.BLACK);
mRectPaint.setStrokeWidth(5);
mRectPaint.setStyle(Paint.Style.STROKE);

mLinesPaint = new Paint();
mLinesPaint.setColor(Color.GRAY);
mLinesPaint.setStrokeWidth(5);
mLinesPaint.setStyle(Paint.Style.STROKE);

mPath = new Path();
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);

int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);

int widthResult = 100;
int heightResult = 100;

if(widthMode != MeasureSpec.AT_MOST) {
widthResult = widthSize;
}

if(heightMode != MeasureSpec.AT_MOST) {
heightResult = heightSize;
}

int resultSize = widthResult > heightResult
? heightResult : widthResult;

setMeasuredDimension(resultSize, resultSize);
}

private int num = 30;

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mRectF = new RectF(
num - mPointStrokeWidth / 2,
num - mPointStrokeWidth / 2,
getMeasuredWidth() - num + mPointStrokeWidth / 2,
getMeasuredHeight() - num + mPointStrokeWidth / 2);

mFPts = new float[] {
getMeasuredWidth() / 2, num,
getMeasuredWidth() - num, getMeasuredHeight()/ 2,
getMeasuredWidth() / 2 ,getMeasuredHeight() - num,
num, getMeasuredHeight()/ 2
};
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);

canvas.drawCircle( getMeasuredHeight() / 2,
getMeasuredHeight() / 2,
getMeasuredHeight() / 2 - num,
mGraphPaint);

canvas.drawRect(mRectF, mRectPaint);

canvas.drawPoints(mFPts, mPointPaint);

canvas.drawLines(mFPts, mLinesPaint);

mPath.moveTo(mFPts[0], mFPts[1]);
mPath.lineTo(mFPts[2], mFPts[3]);
mPath.lineTo(mFPts[4], mFPts[5]);
mPath.lineTo(mFPts[6], mFPts[7]);
mPath.close();
canvas.drawPath(mPath, mLinesPaint);

canvas.drawLine(mFPts[0], mFPts[1], mFPts[4], mFPts[5], mLinesPaint);
canvas.drawLine(mFPts[2], mFPts[3], mFPts[6], mFPts[7], mLinesPaint);

mPath.moveTo(mFPts[6], mFPts[7]);
mPath.quadTo(mFPts[0], mFPts[1], mFPts[2], mFPts[3]);
canvas.drawPath(mPath, mLinesPaint);

mPath.moveTo(num - mPointStrokeWidth / 2,
getMeasuredHeight() - num + mPointStrokeWidth / 2);
mPath.cubicTo(mFPts[4], mFPts[5],
getMeasuredWidth() - num + mPointStrokeWidth / 2, getMeasuredHeight() - num +
mPointStrokeWidth / 2,
mFPts[2], mFPts[3]);
canvas.drawPath(mPath, mLinesPaint);

}
}

效果图:

图片

(二)例子二

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
public class MyDrawView extends View {

private Paint mPaint;

private int mOffsetX;
private int mOffsetY;

public MyDrawView(Context context) {
super(context);
init();
}

public MyDrawView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}

public MyDrawView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}


private void init() {
mPaint = new Paint();
mPaint.setColor(Color.RED);
mPaint.setStrokeWidth(5);
mPaint.setStyle(Paint.Style.STROKE);
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);

int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);

int widthResult = 100;
int heightResult = 100;

if(widthMode != MeasureSpec.AT_MOST) {
widthResult = widthSize;
}

if(heightMode != MeasureSpec.AT_MOST) {
heightResult = heightSize;
}

int resultSize = widthResult > heightResult
? heightResult : widthResult;

setMeasuredDimension(resultSize, resultSize);
}

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mOffsetX = w / 2;
mOffsetY = h / 2 - 55;
}

private Point getHeartPoint(float angle) {
float t = (float) (angle / Math.PI);
float x = (float) (19.5 * (16 * Math.pow(Math.sin(t), 3)));
float y = (float) (-20 * (13 * Math.cos(t) - 5 * Math.cos(2 * t) - 2 * Math.cos(3 * t) - Math.cos(4 * t)));
return new Point(mOffsetX + (int) x, mOffsetY + (int) y);
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);

float angle = 10;
while (angle < 180) {
Point p = getHeartPoint(angle);
canvas.drawPoint(p.x, p.y, mPaint);
angle = angle + 0.02f;
}

}
}

图片

关于画笔和画布的使用,到这里是未完的,其他的效果,以后有时间再补充。谢谢大家的观看。