Android自定义View_Path&PathMeasure(学习笔记)

注明: 非常感谢 gcssloop 的博客,以下为我学习时的笔记记录。

更多Android学习笔记,请关注 Android-NoteBook,欢迎Star。

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

构造方法

方法名释义
PathMeasure()创建一个空的PathMeasure
PathMeasure(Path path, boolean forceClosed)创建 PathMeasure 并关联一个指定的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)获取指定长度的位置坐标及该点Matrix

PathMeasure的方法并不是很多,接下来我们就逐一讲解。


构造函数

分为有参和无参

无参

PathMeasure()

用这个构造函数可创建一个 空的 PathMeasure,但是如果使用之前需要先调用 setPath方法来与 Path进行关联。被关联的Path必须是已经创建好的,如果关联之后Path内容进行了更改,则需要使用 setPath方法 重新关联。

有参

 PathMeasure (Path path, boolean forceClosed)

用这个函数可创建一个空的PathMeasure ,但是使用之前需要先调用 setPath方法来与Path进行关联。被关联的Path必须是已经创建好的,如果关联之后Path内容进行了更改,则需要 setPath 方法重新关联。

该方法有两个参数,第一个参数是 被关联的Path,第二个参数是用来确保Path闭合,如果设置为true,则不论之前Path是否闭合,都会自动闭合该Path(如果Path可以闭合的话)。

注意

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

Demo

canvas.translate(mWidth,mHight);

Path path=new Path();
path.lineTo(0,200);
path.lineTo(200,200);
path.lineTo(200,0);

PathMeasure measure1=new PathMeasure(path,false);
PathMeasure measure2=new PathMeasure(path,true);
Toast.makeText(getContext(), measure1.getLength()+"---"+measure2.getLength(), Toast.LENGTH_SHORT).show();
canvas.drawPath(path,paint);

1561943633804

我们在上面的Dmeo所创建的Path实际上是一个边长为 200的正方形的三条边,通过Toast我们就可以验证上面的总结。

  1. 我们将Path 与两个的PathMeasure进行关联,并给 forceColsed 设置了不同的状态,之后绘制再绘制出来的Path没有任何变化,所以 Path 与 PathMeasure 进行关联并不会影响 Path 状态。
  2. 我们可以看到,设置 forceClosed 为 true 的方法比设置 为 fase的 方法测量出来的长度要长一点,这是由于Path没有闭合的缘故,多出来的距离正是 Path最后一个点与最开始一个点之间点的距离。forceColsed 为fase 测量的是当前Path状态的长度,forceClosed 为true,则不论Path 是否闭合测量的都是 Path 的闭合长度。

相应方法

setPath,isClosed 和 getLength

这三个方法都如字面意思一样,非常简单,这里就简单叙述一下,不再过多讲解。

setPath 是PathMeasure 与 Path 关联的重要方法,效果和 构造函数 中两个参数的作用是一样的。

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

getLength 用于获取 Path 的总长度。

getSegment

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

 public boolean getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo) {
        // Skia used to enforce this as part of it's API, but has since relaxed that restriction
        // so to maintain consistency in our API we enforce the preconditions here.
        }

相应的参数含义:

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

如果 startD,stopD 的数值不在取值范围[0,getLength] 内,或者 startD == stopD 则返回值为 false,不会改变 dst内容。

我们用一个Demo来演示这个方法的使用过程:

Path path=new Path();
//创建Path并添加了一个矩形
path.addRect(-200,-200,200,200,Path.Direction.CW);
canvas.drawPath(path,paint);
//创建用于存储截取后内容的 Path
Path dst=new Path();
//将Path与 PathMeasure关联,此时已经测量了path的边缘长度
PathMeasure measure=new PathMeasure(path,false);

//截取一部分存入dst中,并使用 moveTo 保持截取得到的 Path 第一个点的位置不变
measure.getSegment(200,600,dst,true);
paint.setColor(Color.RED);
//绘制dst
canvas.drawPath(dst,paint);

1561945726912

上图中的红色小块就是我们截取的图形。

PathMeasure measure=new PathMeasure(path,false);之后,已经将长度获取到了,然后moveTo使用起始点位置,开始 startD->200,也就是距离起始点200,stopD->600,距离起始点600.在上面已经用红色标记了。

但是当dst 中有内容时会怎样呢?

Path path=new Path();
//创建Path并添加了一个矩形
path.addRect(-200,-200,200,200,Path.Direction.CW);
canvas.drawPath(path,paint);

Path dst=new Path();
dst.lineTo(-300,-300);

//创建用于存储截取后内容的 Path
//将Path与 PathMeasure关联
PathMeasure measure=new PathMeasure(path,false);
//截取一部分存入dst中,并使用 moveTo 保持截取得到的 Path 第一个点的位置不变
measure.getSegment(200,600,dst,true);
paint.setColor(Color.RED);
//绘制dst
canvas.drawPath(dst,paint);

1561949080991

可以发现,我们Path里面的线还在。并没有被替换,而且因为使用了 moveTo(方法里面true),所以第一个点位置并没有变。

现在我们将 startWithMoveTo 改为false,看看效果会怎样

Path path=new Path();
//创建Path并添加了一个矩形
path.addRect(-200,-200,200,200,Path.Direction.CW);
canvas.drawPath(path,paint);

Path dst=new Path();
dst.lineTo(-300,-300);

//创建用于存储截取后内容的 Path
//将Path与 PathMeasure关联
PathMeasure measure=new PathMeasure(path,false);
//截取一部分存入dst中,起始点不使用 moveTo
measure.getSegment(200,600,dst,false);
paint.setColor(Color.RED);
//绘制dst
canvas.drawPath(dst,paint);

1561949317287

从上面我们可以得知:如果startWithMoveTo 为 true,则被截取出来到Path 片段保持原状,如果 startWithmoveTo 为false ,则会将截取出来的 Path片段的起始点移动到dst的最后一个点,以保证 dst的连续性。

所以,也可以用下面的规则来判断 startWithMoveTo的取值:

取值主要功用
true保证截取得到的 Path 片段不会发生形变
false保证存储截取片段的 Path(dst) 的连续性

nextContour

我们知道Path 可以由多条曲线构成,但不论是 getLength,getSegment 或者是其他方法,都只会在其中第一条线段上运行,而这个 nextContour 就是用于跳转到下一条曲线到方法,如果跳转成功,则返回true,如果跳转失败,则返回 false.

我们用下面这个Demo来演示一下效果:

Path path=new Path();
//添加小矩形
path.addRect(-100,-100,100,100,Path.Direction.CW);
//添加大矩形
path.addRect(-200,-200,200,200,Path.Direction.CW);
//绘制Path
canvas.drawPath(path,paint);
//Path与 PathMeasure 关联
PathMeasure measure=new PathMeasure(path,false);
//获取第一条路径的长度
float len1=measure.getLength();
//跳转到下一条路径
measure.nextContour();
//获得第二条路径的长度
float len2=measure.getLength();
Toast.makeText(getContext(), "第一条"+len1+"---第二条"+len2, Toast.LENGTH_SHORT).show();

1561950865514

我们观察结果,可以发现:

  1. 曲线的顺序和 Path 中添加的顺序有关。
  2. getLength 获取到是当前一条曲线分长度,而不是整个Path 的长度。
  3. getLength 等方法是针对当前的曲线。

getPosTan

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

boolean getPosTan(float distance,float[] pos,float[] tan)

方法各个参数解释

参数作用备注
返回值(boolean)判断获取是否成功true表示成功,数据会存入 pos 和 tan 中, false 表示失败,pos 和 tan 不会改变
distance距离 Path 起点的长度取值范围: 0 <= distance <= getLength
pos该点的坐标值当前点在画布上的位置,有两个数值,分别为x,y坐标。
tan该点的正切值当前点在曲线上的方向,使用 Math.atan2(tan[1], tan[0]) 获取到正切角的弧度值。

这个方法也不难理解,除了其中的 tan,这是干啥的呢?

tan 是用来判断Path 上趋势的,即在这个位置上曲线的走向。

我们以下面这个Demo来看:

箭头图片

jiantou

效果图:

img

public class CanvasView extends View {
    
    //用于记录当前的位置,取值范围[0,1]映射Path的整个长度
    private float currentValue = 0;
    //当前点的实际位置
    private float[] pos;
    //当前点的tangent值,用于计算图片所需旋转的角度
    private float[] tan;
    //箭头图片
    private Bitmap mBitmap;
    //矩阵,用于对图片进行一些操作
    private Matrix matrix;

    private int mWidth;
    private int mHight;
    private Paint paint;

    public CanvasView(Context context) {
        super(context);
    }

    public CanvasView(Context context, AttributeSet attrs) {
        super(context, attrs);
        paint = new Paint();
        paint.setTextSize(25f);
        paint.setStrokeWidth(10f);
        paint.setStyle(Paint.Style.STROKE);
        init(context);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mWidth = w / 2;
        mHight = h / 2;
    }

    @RequiresApi(api = Build.VERSION_CODES.KITKAT)
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.translate(mWidth, mHight);
        //创建Path
        Path path=new Path();
        path.addCircle(0,0,200,Path.Direction.CW);
        PathMeasure measure=new PathMeasure(path,false);

        currentValue+=0.005;
        if (currentValue>=1){
            currentValue=0;
        }
        measure.getPosTan(measure.getLength()*currentValue,pos,tan);
        matrix.reset();
        //计算图片旋转角度
        float degrees= (float) (Math.atan2(tan[1],tan[0])*180/Math.PI);
        matrix.postRotate(degrees,mBitmap.getWidth()/2,mBitmap.getHeight()/2);
        matrix.postTranslate(pos[0]-mBitmap.getWidth()/2,pos[1]-mBitmap.getHeight()/2);

        canvas.drawPath(path,paint);
        canvas.drawBitmap(mBitmap,matrix,paint);
        invalidate();
    }
    private void init(Context context){
        pos=new float[2];
        tan=new float[2];
        BitmapFactory.Options options=new BitmapFactory.Options();
        options.inSampleSize=10;     //缩放图片
        mBitmap=BitmapFactory.decodeResource(context.getResources(),R.drawable.jiantou,options);
        matrix=new Matrix();
    }
}

核心要点:

  1. 通过 tan 的值计算出图片旋转的角度,tan是tagent的 缩写,即中学中常见的正切,其中 tan[0] 是邻边边长,tan[1]是对边边长,而Math 中 atan2 方法时根据正切的数值计算出该角度的大小,得到的单位是弧度(取值范围是-pi到pi),所以上面又将弧度转成了角度。
  2. 通过 Matrix 来设置图片对旋转角度和位移,这里使用的方法与前面讲过的 canvas操作类似。

关于tan这个参数有很多人理解,特此拉出来详述一下,tan 在数学中被称为正切,在直角三角形中,一个锐角的正切定义为它的对边(Opposite side)与邻边(Adjacent side)的比值(来自维基百科):

img

我们此处用 tan 来描述 Path 上某一点的切线方向,主要用了两个数值 tan[0] 和 tan[1] 来描述这个切线的方向(切线方向与x轴夹角) ,看上面公式可知 tan 既可以用 对边/邻边 来表述,也可以用 sin/cos 来表述,此处用两种理解方式均可以(注意下面等价关系):

tan[0] = cos = 邻边(单位圆x坐标) tan[1] = sin = 对边(单位圆y坐标)

以 sin/cos理解:

img

在圆上最右侧点的切线方向向下(动图中小飞机朝向和切线朝向一致),切线角度为90度. sin90 = 1,cos90 = 0 tan[0] = cos = 0 tan[1] = sin = 1

以 对边/邻边 理解(单位圆上坐标):

按照这种理解方式需要借助一个单位圆,单位圆上任意一点到圆心到距离均为 1,以下图30度为例:

img

tan30 = 对边/邻边 = AB/OA = B点y坐标/B点x坐标

另外根据单位圆性质同样可以证得: sin30 = 对边/斜边 = AB/OB = AB = B点y坐标 (单位圆边上任意一点距离圆心距离均为1,故OB = 1) cos30 = 邻边/斜边 = OA/OB = OA = B点x坐标

化为通用公式即为: sin = 该角度在单位圆上对应点的y坐标 cos = 该角度在单位圆上对应点的x坐标

即 tan = sin/cos = y/x tan[0] = x tan[1] = y

另外注意,这个单位圆与小飞机路径没有半毛钱关系,例如上一个例子中的90度切线,不要在单位圆上找对应位置,要找对应角度的位置,90度对应的位置是(0,1),所以: tan[0] = x = 0 tan[1] = y = 1

其实绕来绕去全是等价的 (╯°Д°)╯︵ ┻━┻

PS: 使用 Math.atan2(tan[1], tan[0]) 将 tan 转化为角(单位为弧度)的时候要注意参数顺序。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值