高级UI之Path和PathMeasure

Path是一个工具类,用来记录线条的轨迹路径,然后通过绘制轨迹路径,可以得到各种各样的图案,而PathMeasure是用来对Path进行测量的工具,再Path的运用中,运用最多的就是贝塞尔曲线,也是本文的重点

贝塞尔曲线

贝塞尔曲线就是这样的一条曲线,它是依据四个位置任意的点坐标绘制出的一条光滑曲线

线性公式

给定点P0、P1,线性贝兹曲线只是一条两点之间的直线。这条线由下式给出:

且其等同于线性插值

二次方公式

二次方贝兹曲线的路径由给定点P0、P1、P2的函数B(t)追踪:

TrueType字型就运用了以贝兹样条组成的二次贝兹曲线

三次方公式

P0、P1、P2、P3四个点在平面或在三维空间中定义了三次方贝兹曲线。曲线起始于P0走向P1,并从P2的方向来到P3。一般不会经过P1或P2;这两个点只是在那里提供方向资讯。P0和P1之间的间距,决定了曲线在转而趋进P3之前,走向P2方向的“长度有多长”
曲线的参数形式为:

现代的成象系统,如PostScript、Asymptote和Metafont,运用了以贝兹样条组成的三次贝兹曲线,用来描绘曲线轮廓

一般参数公式

阶贝兹曲线可如下推断。给定点P0、P1、…、Pn,其贝兹曲线即:

如上公式可如下递归表达: 用表示由点P0、P1、…、Pn所决定的贝兹曲线

公式说明

  1. 开始于P0并结束于Pn的曲线,即所谓的端点插值法属性
  2. 曲线是直线的充分必要条件是所有的控制点都位在曲线上。同样的,贝塞尔曲线是直线的充分必要条件是控制点共线
  3. 曲线的起始点(结束点)相切于贝塞尔多边形的第一节(最后一节)
  4. 一条曲线可在任意点切割成两条或任意多条子曲线,每一条子曲线仍是贝塞尔曲线
  5. 一些看似简单的曲线(如圆)无法以贝塞尔曲线精确的描述,或分段成贝塞尔曲线(虽然当每个内部控制点对单位圆上的外部控制点水平或垂直的的距离为时,分成四段的贝兹曲线,可以小于千分之一的最大半径误差近似于圆)
  6. 位于固定偏移量的曲线(来自给定的贝塞尔曲线),又称作偏移曲线(假平行于原来的曲线,如两条铁轨之间的偏移)无法以贝兹曲线精确的形成(某些琐屑实例除外)。无论如何,现存的启发法通常可为实际用途中给出近似值

Android中的贝塞尔曲线使用

首先得到贝塞尔曲线的图像及要素
然后使用Path绘制
Path工具类

1
Path path = new Path();

二阶贝塞尔:其参数第一对是控制点,第二对是结束点

1
path.quadTo();

e.g.

1
2
3
4
path.moveTo(100, 400);
path.quadTo(200, 0, 500, 400);
path.quadTo(700, 600, 900, 400);
canvas.drawPath(path, paint);

三阶贝塞尔

1
path.cubicTo();

e.g.

1
2
3
path.moveTo(100, 1000);
path.cubicTo(300, 900, 600, 1200, 900, 1000);
canvas.drawPath(path, paint);

生成二阶及三阶贝塞尔曲线如下图

贝塞尔曲线实现波形图

onDraw()里面绘制

1
2
3
4
5
6
7
8
9
10
11
12
13
int waveLen = 200;
int originY = 400;
path.moveTo(-waveLen, originY);
for (int i = -waveLen; i < getWidth() + waveLen; i += waveLen) {
//使用quadTo,绝对位置
//path.quadTo(i + waveLen / 4, 600, i + waveLen / 2, originY);
//path.quadTo(i + waveLen * 3 / 4, 200, i + waveLen, originY);
//canvas.drawPath(path, paint);
//使用rQuadTo,相对位置
path.rQuadTo(waveLen / 4, 200, waveLen / 2, 0);
path.rQuadTo(waveLen / 4, -200, waveLen / 2, 0);
}
canvas.drawPath(path, paint);

实现效果如下

最后封闭空间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int waveLen = 200;
int originY = 400;
path.moveTo(-waveLen, originY);
for (int i = -waveLen; i < getWidth() + waveLen; i += waveLen) {
//使用quadTo,绝对位置
//path.quadTo(i + waveLen / 4, 600, i + waveLen / 2, originY);
//path.quadTo(i + waveLen * 3 / 4, 200, i + waveLen, originY);
//canvas.drawPath(path, paint);
//使用rQuadTo,相对位置
path.rQuadTo(waveLen / 4, 200, waveLen / 2, 0);
path.rQuadTo(waveLen / 4, -200, waveLen / 2, 0);
}
path.lineTo(getWidth(), getHeight());
path.lineTo(0, getHeight());
path.close();
canvas.drawPath(path, paint);

得到图像如下

实现将波纹动起来这里贴出整个自定义View代码

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
public class WaveView extends View {
private static final String TAG = "cj5785";
private Path path;
private Paint paint;
private int waveLen = 200;
private int dx;

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

private void init() {
path = new Path();
paint = new Paint();
paint.setColor(Color.RED);
paint.setStrokeWidth(8);
paint.setStyle(Paint.Style.FILL_AND_STROKE);
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//波形
int originY = 400;
path.reset();
path.moveTo(-waveLen + dx, originY);
for (int i = -waveLen; i < getWidth() + waveLen; i += waveLen) {
//使用quadTo
// path.quadTo(i + waveLen / 4, 600, i + waveLen / 2, originY);
// path.quadTo(i + waveLen * 3 / 4, 200, i + waveLen, originY);
// canvas.drawPath(path, paint);
//使用rQuadTo
path.rQuadTo(waveLen / 4, 200, waveLen / 2, 0);
path.rQuadTo(waveLen / 4, -200, waveLen / 2, 0);
}
//封闭空间
path.lineTo(getWidth(), getHeight());
path.lineTo(0, getHeight());
path.close();
canvas.drawPath(path, paint);
}

public void startAnimation() {
ValueAnimator animator = ValueAnimator.ofInt(0, waveLen);
animator.setDuration(1000);
animator.setRepeatCount(ValueAnimator.INFINITE);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
dx = (int)animation.getAnimatedValue();
postInvalidate();
}
});
animator.start();
}
}

PathMeasure

顾名思义,PathMeasure是一个用来测量Path的类,主要有以下方法

构造方法

方法名释义
PathMeasure()创建一个空的PathMeasure
PathMeasure(Path path, boolean forceClosed)创建PathMeasure并关联一个指定的Path(Path需要已经创建完成)
  • 无参构造函数:PathMeasure ()
    用这个构造函数可创建一个空的PathMeasure,但是使用之前需要先调用setPath方法来与Path进行关联。被关联的Path必须是已经创建好的,如果关联之后Path内容进行了更改,则需要使用setPath方法重新关联

  • 有参构造函数:PathMeasure (Path path, boolean forceClosed)
    用这个构造函数是创建一个PathMeasure并关联一个Path, 其实和创建一个空的PathMeasure后调用setPath进行关联效果是一样的,同样,被关联的Path也必须是已经创建好的,如果关联之后Path内容进行了更改,则需要使用setPath方法重新关联
    该方法有两个参数,第一个参数自然就是被关联的Path了,第二个参数是用来确保Path闭合,如果设置为true,则不论之前Path是否闭合,都会自动闭合该Path(如果Path可以闭合的话)

  • 在这里有两点需要明确

    • 不论forceClosed设置为何种状态(true或者false), 都不会影响原有Path的状态,即PathPathMeasure关联之后,之前的的Path不会有任何改变
    • forceClosed的设置状态可能会影响测量结果,如果Path未闭合但在与PathMeasure关联的时候设置forceClosedtrue时,测量结果可能会比Path实际长度稍长一点,获取到到是该Path闭合时的状态

公共方法

返回值方法名释义
voidsetPath(Path path, boolean forceClosed)关联一个Path
booleanisClosed()是否闭合
floatgetLength()获取Path的长度
booleannextContour()跳转到下一个轮廓
booleangetSegment(float startD, float stopD, Path dst, boolean startWithMoveTo)截取片段
booleangetPosTan(float distance, float[] pos, float[] tan)获取指定长度的位置坐标及该点切线值
booleangetMatrix(float distance, Matrix matrix, int flags)获取指定长度的位置坐标及该点
  • setPathPathMeasurePath关联的重要方法,效果和构造函数中两个参数的作用是一样的

  • isClosed用于判断Path是否闭合,但是如果你在关联Path的时候设置forceClosedtrue的话,这个方法的返回值则一定为true

  • getLength用于获取Path的总长度

  • getSegment用于获取Path的一个片段

    参数作用备注
    返回值(boolean)判断截取是否成功true表示截取成功,结果存入dst中,false截取失败,不会改变dst中内容
    startD开始截取位置距离Path起点的长度取值范围:0 <= startD < stopD <= Path总长度
    stopD结束截取位置距离Path起点的长度取值范围:0 <= startD < stopD <= Path总长度
    dst截取的 Path 将会添加到dst注意: 是添加,而不是替换
    startWithMoveTo起始点是否使用moveTo用于保证截取的Path第一个点位置不变

    1. 如果startDstopD的数值不在取值范围[0, getLength]内,或者startD == stopD则返回值为false,不会改变dst内容
    2. 如果在安卓4.4或者之前的版本,在默认开启硬件加速的情况下,更改dst的内容后可能绘制会出现问题,请关闭硬件加速或者给dst添加一个单个操作,例如:dst.rLineTo(0, 0)
    3. 可以用以下规则来判断startWithMoveTo的取值
    取值主要功用
    true保证截取得到的Path片段不会发生形变
    false保证存储截取片段的Path(dst)的连续性
  • nextContour
    我们知道Path可以由多条曲线构成,但不论是getLength,getgetSegment或者是其它方法,都只会在其中第一条线段上运行,而这个nextContour就是用于跳转到下一条曲线到方法,如果跳转成功,则返回true,如果跳转失败,则返回false

  • getPosTan这个方法是用于得到路径上某一长度的位置以及该位置的正切值:

    参数作用备注
    返回值(boolean)判断获取是否成功true表示成功,数据会存入postan中,false表示失败,postan不会改变
    distance距离Path起点的长度取值范围: 0 <= distance <= getLength
    pos该点的坐标值坐标值: (x==[0], y==[1])
    tan该点的正切值正切值:(x==[0], y==[1])
  • getMatrix 这个方法是用于得到路径上某一长度的位置以及该位置的正切值的矩阵

    参数作用备注
    返回值(boolean)判断获取是否成功true表示成功,数据会存入matrix中,false失败,matrix内容不会改变
    distance距离Path起点的长度取值范围: 0 <= distance <= getLength
    matrix根据falgs封装好的matrix会根据flags的设置而存入不同的内容
    flags规定哪些内容会存入到matrix可选择POSITION_MATRIX_FLAG(位置) ANGENT_MATRIX_FLAG(正切)

使用示例

  • 构造方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    path.reset();
    path.lineTo(0, 400);
    path.lineTo(400, 400);
    path.lineTo(400, 0);
    PathMeasure measure1 = new PathMeasure(path, false);
    PathMeasure measure2 = new PathMeasure(path, true);
    Log.d(TAG, "onDraw: measure1=" + measure1.getLength());
    Log.d(TAG, "onDraw: measure2=" + measure2.getLength());
    canvas.drawPath(path, paint);

打印

1
2
D/cj578: onDraw: measure1=1200.0
D/cj578: onDraw: measure2=1600.0

  • getLength()nextContour()
    多路径需要关闭硬件加速
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    path.reset();
    path.addRect(-300, -300, 300, 300, Path.Direction.CCW);
    path.addRect(-200, -200, 200, 200, Path.Direction.CCW);
    path.addRect(-100, -100, 100, 100, Path.Direction.CCW);
    canvas.drawPath(path, paint);
    PathMeasure measure = new PathMeasure(path, false);
    float len1 = measure.getLength();
    Log.d(TAG, "onDraw: len1=" + len1);
    int i = 2;
    while (measure.nextContour()) {
    float len = measure.getLength();
    Log.d(TAG, "onDraw: len" + i + "=" + len);
    i++;
    }

打印

1
2
3
D/cj578: onDraw: len1=2400.0
D/cj578: onDraw: len2=1600.0
D/cj578: onDraw: len3=800.0

  • getSegment()截取片断

    1
    2
    3
    4
    5
    6
    path.reset();
    path.addRect(-300, -300, 300, 300, Path.Direction.CCW);
    PathMeasure measure = new PathMeasure(path, false);
    Path dst = new Path();
    measure.getSegment(0, 1600, dst, true);
    canvas.drawPath(dst, paint);
  • getPosTan()获取位置和正切

    1
    2
    3
    4
    5
    6
    7
    8
    9
    path.reset();
    path.addCircle(0, 0, 300, Path.Direction.CW);
    PathMeasure measure = new PathMeasure(path, false);
    float[] pos = new float[2];
    float[] tan = new float[2];
    measure.getPosTan(measure.getLength() / 4, pos, tan);
    Log.d(TAG, "onDraw: pos-x:" + pos[0] + ",pos-y:" + pos[1]);
    Log.d(TAG, "onDraw: tan-x:" + tan[0] + ",tan-y:" + tan[1]);
    canvas.drawPath(path, paint);

打印

1
2
D/cj578: onDraw: pos-x:4.2605415E-4,pos-y:300.0
D/cj578: onDraw: tan-x:-1.0,tan-y:1.4448632E-6

Donate comment here