(十二)Path 及贝塞尔曲线
版权声明:本文为博主原创文章,未经博主允许不得转载。
本文纯个人学习笔记,由于水平有限,难免有所出错,有发现的可以交流一下。
一、Path
Path 的一些简单的 add 开头的方法这边就不讲述,在这里就稍微记录一些特殊的。
1.带 r 开头
rLineTo,rMoveTo 基于前一个点的相对位置
LineTo,MovoTo 不带r开头的,传的是我们的绝对位置
2. Direction
Path 的方法中,有部分需要传入方向 Direction,我们先来看一下 Direction。
/**
* Specifies how closed shapes (e.g. rects, ovals) are oriented when they
* are added to a path.
*/
public enum Direction {
/** clockwise */
CW (0), // must match enum in SkPath.h
/** counter-clockwise */
CCW (1); // must match enum in SkPath.h
Direction(int ni) {
nativeInt = ni;
}
final int nativeInt;
}
Direction 是一个枚举类,CW 表示顺时针,CCW 表示逆时针,当初画路径的时候是感觉不出来有什么区别,但是在调用 Canvas 的 drawTextOnPath 时候就很明显,决定了字是从左到右还是从右到左。
二、Path 的 填充模式 FillType
可以通过 Path 的 内部的另一枚举 FillType,对 Path 形成的图像进行填充模式的选择。
/**
* Enum for the ways a path may be filled.
*/
public enum FillType {
// these must match the values in SkPath.h
/**
* Specifies that "inside" is computed by a non-zero sum of signed
* edge crossings.
*/
WINDING (0),
/**
* Specifies that "inside" is computed by an odd number of edge
* crossings.
*/
EVEN_ODD (1),
/**
* Same as {@link #WINDING}, but draws outside of the path, rather than inside.
*/
INVERSE_WINDING (2),
/**
* Same as {@link #EVEN_ODD}, but draws outside of the path, rather than inside.
*/
INVERSE_EVEN_ODD(3);
FillType(int ni) {
nativeInt = ni;
}
final int nativeInt;
}
1.Path.FillType.WINDING
public class PathFillView extends View{
private Path mPath;
private Paint mPaint;
public PathFillView(Context context) {
super(context);
mPaint = new Paint();
mPaint.setColor(Color.RED);
mPaint.setStrokeWidth(2);
mPaint.setStyle(Paint.Style.FILL);
mPaint.setAntiAlias(true);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// WINDING 模式 --- 取Path所有所在的区域 -- 默认的模式
mPath = new Path();
mPath.offset(100,100);
mPath.addCircle(200, 200, 100, Path.Direction.CW);
mPath.addCircle(300, 300, 100, Path.Direction.CW);
mPath.setFillType(Path.FillType.WINDING);
mPaint.setColor(Color.RED);
canvas.drawPath(mPath,mPaint);
}
}
WINDING 模式取 Path 所有所在的区域,WINDING 模式是默认的模式。
2.Path.FillType.EVEN_ODD
public class PathFillView extends View{
private Path mPath;
private Paint mPaint;
public PathFillView(Context context) {
super(context);
mPaint = new Paint();
mPaint.setColor(Color.RED);
mPaint.setStrokeWidth(2);
mPaint.setStyle(Paint.Style.FILL);
mPaint.setAntiAlias(true);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// EVEN_ODD 模式 --- 取 Path 所在不相交的区域
mPath = new Path();
mPath.offset(100,100);
mPath.addCircle(200, 200, 100, Path.Direction.CW);
mPath.addCircle(300, 300, 100, Path.Direction.CW);
mPath.setFillType(Path.FillType.EVEN_ODD);
mPaint.setColor(Color.RED);
canvas.drawPath(mPath,mPaint);
}
}
EVEN_ODD 模式取所有添加的 Path 都不相交的区域。
3.Path.FillType.INVERSE_WINDING
public class PathFillView extends View{
private Path mPath;
private Paint mPaint;
public PathFillView(Context context) {
super(context);
mPaint = new Paint();
mPaint.setColor(Color.RED);
mPaint.setStrokeWidth(2);
mPaint.setStyle(Paint.Style.FILL);
mPaint.setAntiAlias(true);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// INVERSE_WINDING 模式 -- 取path所有未占的区域
mPath = new Path();
mPath.offset(100,100);
mPath.addCircle(200, 200, 100, Path.Direction.CW);
mPath.addCircle(300, 300, 100, Path.Direction.CW);
mPath.setFillType(Path.FillType.INVERSE_WINDING);
mPaint.setColor(Color.RED);
canvas.drawPath(mPath,mPaint);
}
}
INVERSE_WINDING 模式取 Path 所有未占的区域。分别取 Path 的最小 X 坐标,最小 Y 坐标,最大 X 坐标,最大 Y 坐标形成该 Path 的最小矩形,相对于 Path 所在最小矩形,填充 Path 都未占的区域。也可以理解为 Path 的最小矩形扣去 WINDING 填充的区域。
4.Path.FillType.INVERSE_EVEN_ODD
public class PathFillView extends View{
private Path mPath;
private Paint mPaint;
public PathFillView(Context context) {
super(context);
mPaint = new Paint();
mPaint.setColor(Color.RED);
mPaint.setStrokeWidth(2);
mPaint.setStyle(Paint.Style.FILL);
mPaint.setAntiAlias(true);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// INVERSE_EVEN_ODD 模式 -- 取path所有未占和相交的区域
mPath = new Path();
mPath.offset(100,100);
mPath.addCircle(200, 200, 100, Path.Direction.CW);
mPath.addCircle(300, 300, 100, Path.Direction.CW);
mPath.setFillType(Path.FillType.INVERSE_EVEN_ODD);
mPaint.setColor(Color.RED);
canvas.drawPath(mPath,mPaint);
}
}
INVERSE_EVEN_ODD 模式取path所有未占和相交的区域。也可以理解为 Path 最小矩形扣去 EVEN_ODD 填充的区域。
三、Path 的 叠加模式 OP
OP 在(十)Canvas 的基本使用 http://blog.csdn.net/qq_18983205/article/details/73610341 有提到叠加规则。
public enum Op {
DIFFERENCE(0),
INTERSECT(1),
UNION(2),
XOR(3),
REVERSE_DIFFERENCE(4),
REPLACE(5);
Op(int nativeInt) {
this.nativeInt = nativeInt;
}
/**
* @hide
*/
public final int nativeInt;
}
效果图中 A 为调用者, B 为叠加上来的区域。
Path 的叠加规则与这类似,只是少了一个 REPLACE。
/**
* The logical operations that can be performed when combining two paths.
*
* @see #op(Path, android.graphics.Path.Op)
* @see #op(Path, Path, android.graphics.Path.Op)
*/
public enum Op {
/**
* Subtract the second path from the first path.
*/
DIFFERENCE,
/**
* Intersect the two paths.
*/
INTERSECT,
/**
* Union (inclusive-or) the two paths.
*/
UNION,
/**
* Exclusive-or the two paths.
*/
XOR,
/**
* Subtract the first path from the second path.
*/
REVERSE_DIFFERENCE
}
1.Path.Op.DIFFERENCE
public class PathOpView extends View {
private Paint mPaint;
public PathOpView(Context context) {
super(context);
mPaint = new Paint();
mPaint.setColor(Color.RED);
mPaint.setStrokeWidth(8);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setAntiAlias(true);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// DIFFERENCE --- 减去Path2后Path1区域剩下的部分
Path path1 = new Path();
path1.addCircle(150, 150, 100, Path.Direction.CW);
Path path2 = new Path();
path2.addCircle(200, 200, 100, Path.Direction.CW);
path1.op(path2, Path.Op.DIFFERENCE);
canvas.drawPath(path1, mPaint);
mPaint.setColor(Color.DKGRAY);
mPaint.setStrokeWidth(2);
canvas.drawCircle(150, 150, 100,mPaint);
canvas.drawCircle(200, 200, 100,mPaint);
}
}
2.Path.Op.INTERSECT
public class PathOpView extends View {
private Paint mPaint;
public PathOpView(Context context) {
super(context);
mPaint = new Paint();
mPaint.setColor(Color.RED);
mPaint.setStrokeWidth(8);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setAntiAlias(true);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// INTERSECT --- 保留Path2 和 Path1 共同的部分
Path path1 = new Path();
path1.addCircle(150, 150, 100, Path.Direction.CW);
Path path2 = new Path();
path2.addCircle(200, 200, 100, Path.Direction.CW);
path1.op(path2, Path.Op.INTERSECT);
canvas.drawPath(path1, mPaint);
mPaint.setColor(Color.DKGRAY);
mPaint.setStrokeWidth(2);
canvas.drawCircle(150, 150, 100,mPaint);
canvas.drawCircle(200, 200, 100,mPaint);
}
}
3.Path.Op.UNION
public class PathOpView extends View {
private Paint mPaint;
public PathOpView(Context context) {
super(context);
mPaint = new Paint();
mPaint.setColor(Color.RED);
mPaint.setStrokeWidth(8);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setAntiAlias(true);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// UNION --- 保留Path1 和 Path 2
Path path1 = new Path();
path1.addCircle(150, 150, 100, Path.Direction.CW);
Path path2 = new Path();
path2.addCircle(200, 200, 100, Path.Direction.CW);
path1.op(path2, Path.Op.UNION);
canvas.drawPath(path1, mPaint);
mPaint.setColor(Color.DKGRAY);
mPaint.setStrokeWidth(2);
canvas.drawCircle(150, 150, 100,mPaint);
canvas.drawCircle(200, 200, 100,mPaint);
}
}
4.Path.Op.XOR
public class PathOpView extends View {
private Paint mPaint;
public PathOpView(Context context) {
super(context);
mPaint = new Paint();
mPaint.setColor(Color.RED);
mPaint.setStrokeWidth(8);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setAntiAlias(true);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// XOR --- 保留Path1 和 Path2 不相交的区域
Path path1 = new Path();
path1.addCircle(150, 150, 100, Path.Direction.CW);
Path path2 = new Path();
path2.addCircle(200, 200, 100, Path.Direction.CW);
path1.op(path2, Path.Op.XOR);
canvas.drawPath(path1, mPaint);
mPaint.setColor(Color.DKGRAY);
mPaint.setStrokeWidth(2);
canvas.drawCircle(150, 150, 100,mPaint);
canvas.drawCircle(200, 200, 100,mPaint);
}
}
这边没有填充,看起来像是全部都被沾满,实际要扣去中间重叠部分,如果是全充满的情况,中间两条线是不会被选中。
5.Path.Op.REVERSE_DIFFERENCE
public class PathOpView extends View {
private Paint mPaint;
public PathOpView(Context context) {
super(context);
mPaint = new Paint();
mPaint.setColor(Color.RED);
mPaint.setStrokeWidth(8);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setAntiAlias(true);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// REVERSE_DIFFERENCE --- 减去Path1后Path2区域剩下的部分
Path path1 = new Path();
path1.addCircle(150, 150, 100, Path.Direction.CW);
Path path2 = new Path();
path2.addCircle(200, 200, 100, Path.Direction.CW);
path1.op(path2, Path.Op.REVERSE_DIFFERENCE);
canvas.drawPath(path1, mPaint);
mPaint.setColor(Color.DKGRAY);
mPaint.setStrokeWidth(2);
canvas.drawCircle(150, 150, 100,mPaint);
canvas.drawCircle(200, 200, 100,mPaint);
}
}
四、贝塞尔曲线
这个有点偏数学的知识了,不过感觉有必要在这记录一下,在自定义控件很多时候都要用到贝塞尔曲线。
这边提供一个github:https://github.com/venshine/BezierMaker ,用这个可以很明确的看出贝塞尔曲线的绘制过程。
1.lineTo
很多人只知道 lineTo 是画一条直线,却不知道 lineTo 跟贝塞尔曲线有什么关系。lineTo 也可以理解为一阶贝塞尔曲线。
线上的点是从 P0 匀速运动到 P1 过程中,各个时间点所取的点的集合。
2.二阶贝塞尔曲线
假设运动的时间为 1 秒,先绘制出对应时间点 直线P0P1 和 直线P1P2 当前时间点所在点,这两个点形成一条直线(图中绿色那条),这条直线会随时间变化而变化。在每个时间点,这条直线是唯一的,这个时候选当前时间点绿色直线上的对应点,把整个过长产生的绿色直线上各个时间对应点连接起来就是二阶贝塞尔曲线,即红色那条。
如上图:取时间为 0.26s 的时候,分别取 直线P0P1 和 和 直线P1P2 上当前时间对应的点,连接起来得到当前唯一的绿色直线,再在这条绿色直线上取出当前时间(0.26s)所对应的点。
3.多阶贝塞尔曲线
多阶贝塞尔曲线跟二阶的绘制思路一致,相邻两条直线取当前时间点所在的点连接成新的直线,原本有 n 条直线,经一次获取点连接后直线条数为 n-1,继续重复这个操作,每次会少一条线,直到最后只有一条线的时候,取当前时间在这条线上的点,整个过程这些点连成的线即 n 阶贝塞尔曲线。
阶数越大,说明曲线控制点越多,则曲线能绘制出来的曲折度(弧度)越大。
上一篇: Flutter 贝塞尔曲线实现案例
下一篇: 纯CSS绘制心形图案