自定义控件高级UI

高级UI<第三十篇>:PathMeasure详解

2020-01-09  本文已影响0人  NoBugException

Path是一个轨迹或者是一个路径,无论是动画还是自定义View中都有举足轻重的地位,Path可以绘制直线、圆形、矩形、圆角矩形、椭圆、扇形以及贝塞尔曲线,你们有没有发现,Path所能绘制的图形都有各自的公式可以精准的计算出轨迹上的任意一点的坐标。但是,在现实中,一个复杂的轨迹往往由多个Path组成,那么想要求出复杂Path上的任意一点我想并不容易。PathMeasure可以精准的获取和追踪Path的坐标,无论Path有多么复杂。

PathMeasure需要了解的知识有:

(1)构造方法
(2)获取路径长度
(3)截取Path
(4)切换到下一个Path
(5)获取路径上某点的坐标以及切点坐标

构造方法

构造方法有两种,分别是:

前者不需要传递形参,但是需要手动调用setPath(Path path, boolean forceClosed)方法设置一个路径以及是否闭合。

演示代码如下:

    PathMeasure pathMeasure = new PathMeasure();
    pathMeasure.setPath(path, true);

或者

    PathMeasure pathMeasure = new PathMeasure(path, false);

第一个参数是path,表示绑定一个路径,第二个参数是forceClosed,表示是否强制闭合。调用PathMeasureisClosed()方法可以知道Path是否闭合。

什么是路径?洗洗睡吧,路径都不知道话请出门左拐,这篇文章不适合您。

什么叫闭合路径?
答:闭合路径就是路径的首尾相连之后路径,forceClosed的意思和字面上的意思一样,即强制闭合,强制让Path的首尾相连。

获取路径长度

PathMeasure可以获取路径的长度,其方法如下:

pathMeasure.getLength()

[举例一]

图片.png

上图中,A为Path的起点,B为Path的终点,假设A到B的线段长度为100,那么请问该Path的长度时多少?

有人说,这题不是很简单吗,上图Path长度为100。

这个答案只能说不够准确,求一个Path的长度必须考虑该Path是否闭合。如果没有闭合,那么该Path的长度就是100,如果已经闭合,那么首尾相连之后长度就是200。

线段的路径的长度有点特殊,这一点需要注意。

[举例二]

图片.png

这个图形稍微复杂了点,A为Path的起点,D为Path的终点,那么闭合前的长度为:

length = AB+BC+CD

闭合后的长度为

length = AB+BC+CD+AD

[举例三]

图片.png

上图的路径更加复杂了点,A为Path的起点,E为Path的终点,闭合前的Path长度为:

length = AB+BC+CD+DE

闭合之后的长度为:

length = AB+BC+CD+DE+AE

[举例四]

图片.png

上图的曲线是一个贝塞尔曲线,A为起点,B为终点,假设该曲线的长度为600,A和B之间的线段长度为500,那么闭合前的长度为600(曲线的长度),闭合之后的长度为1100。

截取Path

public boolean getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo)

截取Path对用的API是getSegment,第一个参数startD表示截取Path的起始长度,第二个参数stopD表示截取Path的终点长度,将截取后的Path保存到dst中。参数startWithMoveTo一般为true。

另外,这个方法的返回类型是boolean类型,如果返回true,则截取成功,如果返回false,则截取失败。

下面开始举几个例子,例子举完之后再开始介绍startWithMoveTo属性。

【举例一】 绘制一个圆,并截取一段

如图:

图片.png

代码如下:

@Override
protected void onDraw(Canvas canvas) {

    path.reset();
    path.addCircle(400, 500, 100, Path.Direction.CCW);
    pathMeasure = new PathMeasure();
    pathMeasure.setPath(path, true);

    dst.reset();
    boolean success = pathMeasure.getSegment(0, pathMeasure.getLength()-100, dst, true);
    //绘制路径
    canvas.drawPath(dst, mPaint);

}

代码的意思是,通过Path绘制一个以(400,500)为圆点,半径为100,逆时针绘制一个圆形轨迹,再由PathMeasuregetSegment方法截取[0, pathMeasure.getLength()-100]区间的路径,最后将截取后的Path绘制到画布中。

Path的截取功能的强大之处不止如此,请看以下举例。

【举例二】 演示一个圆的绘制轨迹

348.gif

代码如下:

@Override
protected void onDraw(Canvas canvas) {

    path.reset();
    path.addCircle(400, 500, 100, Path.Direction.CCW);
    pathMeasure = new PathMeasure();
    pathMeasure.setPath(path, true);

    dst.reset();
    boolean success = pathMeasure.getSegment(0, t, dst, true);
    if(t >= pathMeasure.getLength()){
        t = 0;
    }
    t++;
    //绘制路径
    canvas.drawPath(dst, mPaint);

    invalidate();

}

【举例三】 进度条效果

如图:(如果将View本身加上旋转动画的话那就更像一个进度条了)

349.gif

代码如下:

public class PathTest extends View {

    private Paint mPaint;
    private PathMeasure pathMeasure;
    private float scale = 0;
    private Path path = new Path();
    private Path dst = new Path();

    public PathTest(Context context) {
        this(context, null);
    }

    public PathTest(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

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

    private void init(){
        mPaint = new Paint();
        mPaint.setColor(Color.GRAY);
        mPaint.setAntiAlias(true);
        mPaint.setStrokeWidth(8);
        mPaint.setTextSize(60);
        mPaint.setStyle(Paint.Style.STROKE);
    }

    @Override
    protected void onDraw(Canvas canvas) {
     
        path.reset();
        path.addCircle(400, 500, 100, Path.Direction.CW);
        pathMeasure = new PathMeasure();
        pathMeasure.setPath(path, true);

        dst.reset();
        float stopD = pathMeasure.getLength() * scale;
        float startD = stopD - pathMeasure.getLength() * Math.abs(scale - 0.5f);
        pathMeasure.getSegment(startD, stopD, dst, true);
        if(scale >= 1){
            scale = 0.01f;
        }
        scale += 0.01f;
        //绘制路径
        canvas.drawPath(dst, mPaint);

        invalidate();

    }
}

最后,说一说startWithMoveTo属性的艺术吧,一般而言,这个属性设置为true。

startWithMoveTo分成三个单词来理解:

startWithMoveTo为true时:将画笔的位置移动到上一次的位置
startWithMoveTo为false时:画笔位置不动。

以上的理解虽然是正确的,但是往往还不能真正理解startWithMoveTo的含义,再回来看一下getSegment方法,如下:

public boolean getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo)

startWithMoveTo直接作用于dst(截取后的路径),假设将path截取,截取之后的路径存入dst中,当startWithMoveTo为true时,画笔位置就是path的画笔位置,当startWithMoveTo为false时,画笔位置就是dst的画笔位置。

【举例】

    private Path path = new Path();
    private Path dst = new Path();
        //一个圆
        path.addCircle(400, 500, 100, Path.Direction.CW);

        pathMeasure = new PathMeasure();
        pathMeasure.setPath(path, true);

        //将画笔位置调整到(100,200)
        dst.moveTo(100, 200);

        //将圆截取掉一部分()
        pathMeasure.getSegment(0, pathMeasure.getLength()-100, dst, false);

        //绘制路径
        canvas.drawPath(dst, mPaint);

看一下以上代码,path中存入一个以(400,500)为圆点,100为半径的圆,此时path画笔的位置是(0,0),dst.moveTo函数将dst的画笔的位置调整到了(100,200),最后将圆截取掉一部分,startWithMoveTo值置为false,并绘制出图形。

该代码的效果如下:

图片.png

如果将

dst.moveTo(100, 200);

换成

dst.lineTo(100, 200);

效果如下:

图片.png

由此,可以断定,如果将startWithMoveTo设置为false,当前路径的起点会和上一个路径的终点相连。(这就是startWithMoveTo的最终奥义了)

切换到下一个Path

切换到下一个Path是什么意思?多个Path的情况还可以切换?

那是当然,代码如下:

pathMeasure.nextContour()

PathMeasure有一个nextContour()方法,可以切换到下一个Path,返回true则切换成功。

我们先看下图:

图片.png

上图中,由两条直线和一个曲线组成,实现它的代码如下:

        //设置画笔位置
        path.moveTo(100, 100);

        //一条直线
        path.lineTo(200,400);

        //贝塞尔曲线
        path.quadTo(300, 100, 500, 400);

        //一条直线
        path.lineTo(600,400);

        pathMeasure = new PathMeasure();

        pathMeasure.setPath(path, false);

        //绘制路径
        canvas.drawPath(path, mPaint);

那么,请问,这个图形有有几个Path?

答:仅有一个,上图中,两条直线和一条贝塞尔曲线是连续的,所以只能算一个Path,我们可以使用nextContour方法遍历来验证。

        while (pathMeasure.nextContour()){
            Log.d("aaa", "路径长度:"+pathMeasure.getLength());
        }

发现日志只打印了一次。

我们再来看一张图。

图片.png

实现代码如下:

        //设置画笔位置
        path.moveTo(100, 100);

        //一条直线
        path.lineTo(200,400);

        //贝塞尔曲线
        path.quadTo(300, 100, 500, 400);

        //设置画笔位置
        path.moveTo(500, 500);

        //一条直线
        path.lineTo(600,400);

        pathMeasure = new PathMeasure();

        pathMeasure.setPath(path, false);

        //绘制路径
        canvas.drawPath(path, mPaint);

上图只有两条Path,左边直线和曲线是连续的,所以算一条Path,右边的直线和左边的不连续,所以右边的直线单独算是一条Path,总共两条Path。同样可以通过nextContour遍历验证。

        while (pathMeasure.nextContour()){
            Log.d("aaa", "路径长度:"+pathMeasure.getLength());
        }

获取路径上某点的坐标以及切点坐标

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

以上方法可以获取路径上某点坐标,以及切点坐标。

当我们得到坐标和切点坐标时,可以实现下图效果

350.gif

直接贴出代码:

    private Paint mPaint;
    private PathMeasure pathMeasure;
    private Path path = new Path();
    private Bitmap airplaneBitmap;
    private Context mContext;
    private float distance = 0;
    private float pos[];//坐标
    private float tan[];//切点坐标

    public PathTest(Context context) {
        this(context, null);
    }

    public PathTest(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

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

    private void init(){
        mPaint = new Paint();
        mPaint.setColor(Color.BLACK);
        mPaint.setAntiAlias(true);
        mPaint.setStrokeWidth(80);
        mPaint.setTextSize(60);
        mPaint.setStyle(Paint.Style.STROKE);

        pos = new float[2];
        tan = new float[2];
    }

    @Override
    protected void onDraw(Canvas canvas) {

        path.reset();

        path.addCircle(300, 500, 200, Path.Direction.CW);

        pathMeasure = new PathMeasure();

        pathMeasure.setPath(path, false);

        //绘制路径
        canvas.drawPath(path, mPaint);


        canvas.save();

        if (Build.VERSION.SDK_INT>Build.VERSION_CODES.LOLLIPOP){
            Drawable vectorDrawable = mContext.getDrawable(R.drawable.airplane);
            airplaneBitmap = Bitmap.createBitmap(vectorDrawable.getIntrinsicWidth(), vectorDrawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
            Canvas ca = new Canvas(airplaneBitmap);
            vectorDrawable.setBounds(0, 0, ca.getWidth(), ca.getHeight());
            vectorDrawable.draw(ca);
        }else {
            airplaneBitmap = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.airplane);
        }

        pathMeasure.getPosTan(distance, pos, tan);
        float degrees = (float) (Math.atan2(tan[1], tan[0]) * 180.0 / Math.PI);
        Matrix matrix=new Matrix();
        matrix.postRotate(degrees);
        matrix.preTranslate(-airplaneBitmap.getWidth()/2,-airplaneBitmap.getHeight()/2);
        matrix.postTranslate(pos[0], pos[1]);
        canvas.drawBitmap(airplaneBitmap, matrix, mPaint);

        if(distance >= pathMeasure.getLength()){
            distance = 0;
        }

        distance = distance + 4;

        invalidate();

    }
}

核心只需要了解两点:

(1)根据路径坐标点调整飞机的位置

    //偏移量
    matrix.preTranslate(-airplaneBitmap.getWidth()/2,-airplaneBitmap.getHeight()/2);
    //调整位置
    matrix.postTranslate(pos[0], pos[1]);

(2)根据切点求出角度

float degrees = (float) (Math.atan2(tan[1], tan[0]) * 180.0 / Math.PI);

除了pathMeasure.getPosTan(distance, pos, tan)可以实现以上效果,pathMeasure.getMatrix也是可以实现以上效果的,代码如下:

public class PathTest extends View {

    private Paint mPaint;
    private PathMeasure pathMeasure;
    private Path path = new Path();
    private Bitmap airplaneBitmap;
    private Context mContext;
    private float distance = 0;

    public PathTest(Context context) {
        this(context, null);
    }

    public PathTest(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

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

    private void init(){
        mPaint = new Paint();
        mPaint.setColor(Color.BLACK);
        mPaint.setAntiAlias(true);
        mPaint.setStrokeWidth(80);
        mPaint.setTextSize(60);
        mPaint.setStyle(Paint.Style.STROKE);
    }

    @Override
    protected void onDraw(Canvas canvas) {

        path.reset();

        path.addCircle(300, 500, 200, Path.Direction.CW);

        pathMeasure = new PathMeasure();

        pathMeasure.setPath(path, false);

        //绘制路径
        canvas.drawPath(path, mPaint);


        if (Build.VERSION.SDK_INT>Build.VERSION_CODES.LOLLIPOP){
            Drawable vectorDrawable = mContext.getDrawable(R.drawable.airplane);
            airplaneBitmap = Bitmap.createBitmap(vectorDrawable.getIntrinsicWidth(), vectorDrawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
            Canvas ca = new Canvas(airplaneBitmap);
            vectorDrawable.setBounds(0, 0, ca.getWidth(), ca.getHeight());
            vectorDrawable.draw(ca);
        }else {
            airplaneBitmap = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.airplane);
        }

        Matrix matrix=new Matrix();
        pathMeasure.getMatrix(distance,matrix,PathMeasure.POSITION_MATRIX_FLAG | PathMeasure.TANGENT_MATRIX_FLAG);
        matrix.preTranslate(-airplaneBitmap.getWidth()/2,-airplaneBitmap.getHeight()/2);
        canvas.drawBitmap(airplaneBitmap,matrix,mPaint);

        if(distance >= pathMeasure.getLength()){
            distance = 0;
        }

        distance = distance + 4;

        invalidate();

    }
}

这里需要了解两个Flag,分别是:

(1)PathMeasure.POSITION_MATRIX_FLAG:位置信息
(2)PathMeasure.TANGENT_MATRIX_FLAG:切线信息

最后贴出飞机的矢量图代码

airplane.xml

<vector
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="60dp"
    android:height="40dp"
    android:viewportHeight="1024"
    android:viewportWidth="1024">
    <path android:fillColor="#4DA476" android:pathData="M404.64,353.27c88.97,0 178.25,-0.78 267.19,0.05L476.67,54.78c-8.49,-12.95 -22.59,-18.97 -38.02,-18.97l-135,-0.01 100.99,317.47z"/>
    <path android:fillColor="#4DA476" android:pathData="M1019.92,607.23c0,52.59 -42.62,95.25 -95.24,95.25l-244.05,-0L476.62,969.22c-9.36,12.27 -22.58,18.99 -38.02,18.99l-135,-0.01 95.64,-299.21L85.23,591.48c-16.98,-5.27 -28.81,-19 -31.71,-36.57L4.44,254.17c-1.19,-7.18 0.51,-13.84 5.24,-19.39 4.65,-5.61 10.91,-8.5 18.21,-8.5l54.76,-0.01c15.56,0 29.84,6.12 38.22,19.21l89.01,139.52c92.39,0 315.48,0 492.58,0 222.23,-0 317.46,114.41 317.46,222.23v-0.01h0zM511.98,448.5v63.5h-63.49v63.5h63.49v63.48h63.5v-63.48L638.97,575.5v-63.5h-63.49v-63.5h-63.49zM899.96,480.25h-57.06c-4.05,0 -7.58,1.6 -10.24,4.7 -2.7,3.05 -3.77,6.74 -3.21,10.79l3.38,24.52c1.91,13.57 13.29,23.5 26.94,23.5l98.42,-0.01c-10.2,-19.89 -24.92,-37.49 -42.94,-52.17a200.1,200.1 0,0 0,-15.28 -11.31l0,-0.01z"/>
</vector>

[本章完...]

上一篇下一篇

猜你喜欢

热点阅读