Android 自定义View学习(十六)——PathMeasure学习

学习资料:

  • 徐医生的PathMeasure之迷径追踪
  • GcsSloop同学的安卓自定义View进阶-PathMeasure

徐医生,《Android群英传》的作者,不用多说
GcsSloop同学,今年大四,一个超级厉害的同学,个人博客超级棒


1. PathMeasure

在Android 自定义View学习(九)——Bezier贝塞尔曲线学习中学习到了使用De Casteljau 德卡斯特里奥算法利用贝塞尔曲线的起始点,控制点,终点来帮助计算曲线上任意点的坐标。在其他的Path路径中,系统提供了一个封装好的PathMeasure来帮助辅助测量

顾名思义,可以理解为用来辅助计算Path的计算器,PathMeasurepublic方法不多,一共也就7个方法


1.1 初始化,构造方法

PathMeasure构造方法有两个,一个无参,一个有参

1. public PathMeasure(){}

2. public PathMeasure(Path path, boolean forceClosed){}

使用构造方法1得到一个mPathMeasure对象后,mPathMeasure.setPath(Path path, boolean forceClosed)Path关联,setPath()方法中,也需要一个boolean forceClosed

  • boolean forceClosed
    代表测量计算时是否闭合,不关乎Path绘制,ture闭合,false不闭合。forceCloseed不会对Path有任何影响,只是对PathMeasure测量时候有影响。
private void init() {
     //画笔
     mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
     mPaint.setColor(Color.parseColor("#FF4081"));
     mPaint.setStrokeWidth(10f);
     mPaint.setStyle(Paint.Style.STROKE);
     //Path
     mPath = new Path();
     mPath.moveTo(100f,0f);
     mPath.lineTo(100f,100f);
     mPath.lineTo(200f,100f);
     mPath.lineTo(200f,0f);

     //PathMeasure
     mPathMeasure = new PathMeasure(mPath,true);
     Log.e("length","&&&&"+mPathMeasure.getLength());
}
  • ture , 400
  • false ,300

getLength(),就是获得测量计算的长度

但无论true还是false,绘制都一样

无论是通过setPath()方法还是通过构造方法2mPath关联,mPath都必须是之前创建好的。关联之后的mPath发生变化时,需要再次调用setPath()对改变后的mPath再次进行关联

PathMeasure是否闭合可以用isClosed()方法的返回值进行判断


1.2 getSegment()截取片段

getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo)
可以用来截取整个Path的某一个片段

Android 自定义View学习(十六)——PathMeasure学习_第1张图片
getSegment方法各参数含义

图截取自GcsSloop同学的安卓自定义View进阶-PathMeasure

boolean startWithMoveTo通常设置为true;
设置为false时,一般是和dst一起使用。由于截取出来的片段是添加到dst中并不是代替,所以设置为false时是将截取出来的Path的起点,移动到dst的终点,保证dst中的片段的连续性

感觉文字比较难理解,看代码比较明显

这个方法有个bug,需要考虑硬件加速问题,上面的图片最后给出了解决方案


测试使用:

public class PathLoadingView extends View {
    private Path mPath;
    private Paint mPaint, defaultPaint;
    private PathMeasure mPathMeasure;
    private Path dst;

    public PathLoadingView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    /**
     * 初始化
     */
    private void init() {
        //默认画笔 绘制辅助圆用
        defaultPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        defaultPaint.setColor(Color.CYAN);
        defaultPaint.setStrokeWidth(10f);
        defaultPaint.setStyle(Paint.Style.STROKE);
        //截取画笔
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(Color.parseColor("#FF4081"));
        mPaint.setStrokeWidth(10f);
        mPaint.setStyle(Paint.Style.STROKE);
        //Path
        mPath = new Path();
        mPath.addCircle(300f, 300f, 100f, Path.Direction.CW);//加入一个半径为100圆
        //PathMeasure
        mPathMeasure = new PathMeasure(mPath, false);
        // Path dst 用来存储截取的Path片段
        dst = new Path();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        dst.reset();
        //避免硬件加速的Bug
        dst.lineTo(0, 0);
        //截取圆的1/4
        final float stopP = (float) (Math.PI * 2 * 100 / 4);
        mPathMeasure.getSegment(0, stopP, dst, true);
        canvas.drawPath(mPath,defaultPaint);//绘制mPath辅助圆
        canvas.drawPath(dst, mPaint);//绘制截取的片段
    }
}

布局文件



    


Android 自定义View学习(十六)——PathMeasure学习_第2张图片
截取四分之一圆

红色就是截取的片段


上面的dst一开始是没有值的,下面给dst加入值

修改代码:

//避免硬件加速的Bug
dst.lineTo(0, 0);
dst.lineTo(300,300);
//截取圆的1/4

也就加入dst.lineTo(300,300),就是从控件的起点和圆心连接起来

Android 自定义View学习(十六)——PathMeasure学习_第3张图片
dst内有值

此时mPathMeasure.getSegment(0, stopP, dst, true)startWithMoveTo值为true,截取的片段的起点并没有改变,将startWithMoveTo设为false

Android 自定义View学习(十六)——PathMeasure学习_第4张图片
startWithMoveTo设为false

此时,截取的片段就和 dst连接了起来,并且截取的片段形态也发生了改变


利用这个方法可以做出一个类似Material Design风格的圆形进度条

public class PathLoadingView extends View {
    private Path mPath;
    private Paint mPaint;
    private PathMeasure mPathMeasure;
    private Path dst;
    private float mLength;
    private float mAnimatorValue;

    public PathLoadingView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    /**
     * 初始化
     */
    private void init() {
        //画笔
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(Color.parseColor("#FF4081"));
        mPaint.setStrokeWidth(10f);
        mPaint.setStyle(Paint.Style.STROKE);
        //Path
        mPath = new Path();
        mPath.addCircle(300f, 300f, 100f, Path.Direction.CW);//加入一个半径为100圆
        //PathMeasure
        mPathMeasure = new PathMeasure(mPath, false);
        mLength = mPathMeasure.getLength();//此时为圆的周长
        // Path dst 用来存储截取的Path片段
        dst = new Path();
        //属性动画
        final ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1);
        //设置动画过程的监听
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mAnimatorValue = (float) animation.getAnimatedValue();
                invalidate();
            }
        });
        valueAnimator.setDuration(2000);
        valueAnimator.setRepeatCount(ValueAnimator.INFINITE);//无限循环
        valueAnimator.start();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        dst.reset();
        //避免硬件加速的Bug
        dst.lineTo(0, 0);
        //截取片段
        float stop = mLength * mAnimatorValue;
        float start = (float) (stop - ((0.5 - Math.abs(mAnimatorValue - 0.5)) * mLength));
        mPathMeasure.getSegment(start, stop, dst, true);
        canvas.drawPath(dst, mPaint);//绘制截取的片段
    }
}
Android 自定义View学习(十六)——PathMeasure学习_第5张图片
PathLoadingView

代码最关键的地方就是利用属性动画得到的mAnimatorValue值计算开始和结束截取点


1.3 getPosTan() 获取一点坐标及点的正切值

  • boolean getPosTan(float distance, float pos[], float tan[])
    可以获取路径上一个点的坐标以及该点的正切值
Android 自定义View学习(十六)——PathMeasure学习_第6张图片
getPosTan()各参数含义

代码:

public class PathLoadingView extends View {
    private Path mPath;
    private Paint mPaint;
    private PathMeasure mPathMeasure;

    private float mAnimatorValue;
    private float[] pos;
    private float[] tan;
    private float mLength;
    public PathLoadingView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    /**
     * 初始化
     */
    private void init() {
        pos = new float[2];//点的坐标
        tan = new float[2];//直角三角形两个的直角边
        //画笔
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(Color.parseColor("#FF4081"));
        mPaint.setStrokeWidth(10f);
        mPaint.setStyle(Paint.Style.STROKE);
        //Path
        mPath = new Path();
        mPath.addCircle(0f, 0f, 200f, Path.Direction.CW);//加入一个半径为100圆
        //PathMeasure
        mPathMeasure = new PathMeasure(mPath, false);
        mLength = mPathMeasure.getLength();
        //属性动画
        final ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1);
        //设置动画过程的监听
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mAnimatorValue = (float) animation.getAnimatedValue();
                invalidate();
            }
        });
        valueAnimator.setDuration(2000);
        valueAnimator.setRepeatCount(ValueAnimator.INFINITE);//无限循环
        valueAnimator.start();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //获取在动画某一个时刻点的坐标及正切值
        mPathMeasure.getPosTan(mLength * mAnimatorValue,pos,tan);
        float degrees = (float) (Math.atan2(tan[1], tan[0]) * 180.0 / Math.PI);
        Log.e("degrees","&&&"+degrees+"--->"+Math.atan2(tan[1], tan[0])+"--->tan[1]= "+tan[1]+"---tan[0]= "+tan[0]+"---pos[0] ="+pos[0]+"---pos[1] ="+pos[1]);
        canvas.save();
        canvas.translate(getWidth()/2, getHeight()/2);//将坐标系移动到控件的中心位置
        canvas.drawPath(mPath, mPaint);
        canvas.drawCircle(pos[0], pos[1], 10, mPaint);//在路径的点上绘制一个小圆
        canvas.rotate(degrees);//将画布旋转 此时坐标系也跟着旋转
        canvas.drawLine(0, -200, 100, -200, mPaint);//绘制一段长度为100的正切线 200是圆的半径
        canvas.restore();
    }
}

运行后效果

切线

这段代码的效果看起来是切线在圆上滑动,实际是画布旋转的效果,切线是同一条,根据动画的时间,计算出对应旋转的角度,将画布进行旋转

getPosTan(mLength * mAnimatorValue,pos,tan)会将拿到的坐标及正切值存入pos,tan两个数组中

  • pos[0],就是点x轴坐标
  • pos[1],就是点y轴坐标

tan值不好理解,值是取自半径为1的单位圆上的坐标

  • tan[0],单位圆上点x轴坐标,其实就是角对边的边长
  • tan[1],单位圆上点y轴坐标,邻边的边长
Android 自定义View学习(十六)——PathMeasure学习_第7张图片
tan值取自单位圆上对应角度的坐标

图从GcsSloop同学博客盗来的,源自维基百科

double radian = Math.atan2(double y ,double x);
  • yy轴值
  • xx轴值

注意X,Y值顺序

得到的结果radian并不是角度,而是是弧度,取值范围(-π,π),弧度转角度公式:

角度 = 弧度 * 180 / π

得到角度后,就可以根据需要进行操作


1.4 getMatrix() 得到点位置及正切值矩阵

getMatrix(float distance, Matrix matrix, int flags)

  • distance,距离起点的距离
  • matrix,用来位置或者正切值的矩阵
  • flags,矩阵的类型,有两种,PathMeasure.TANGENT_MATRIX_FLAG正切,PathMeasure.TANGENT_MATRIX_FLAG位置

代码:

public class PathLoadingView extends View {
    private Path mPath;
    private Paint mPaint;
    private PathMeasure mPathMeasure;

    private float mAnimatorValue;
    private float mLength;
    private Matrix mMatrix;
    private Bitmap mBitmap;

    public PathLoadingView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    /**
     * 初始化
     */
    private void init() {
        mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.fly);
        //矩阵
        mMatrix = new Matrix();
        //画笔
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(Color.parseColor("#FF4081"));
        mPaint.setStrokeWidth(10f);
        mPaint.setStyle(Paint.Style.STROKE);
        //Path
        mPath = new Path();
        mPath.addCircle(0f, 0f, 200f, Path.Direction.CW);//加入一个半径为100圆
        //PathMeasure
        mPathMeasure = new PathMeasure(mPath, false);
        mLength = mPathMeasure.getLength();
        //属性动画
        final ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1);
        //设置动画过程的监听
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                mAnimatorValue = (float) animation.getAnimatedValue();
                invalidate();
            }
        });
        valueAnimator.setDuration(2000);
        valueAnimator.setRepeatCount(ValueAnimator.INFINITE);//无限循环
        valueAnimator.start();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //得到矩阵
        mPathMeasure.getMatrix(mLength * mAnimatorValue, mMatrix, PathMeasure.POSITION_MATRIX_FLAG);
        canvas.translate(getWidth() / 2, getHeight() / 2);//将坐标系移动到控件的中心位置
        canvas.drawPath(mPath, mPaint);
        //绘制小三角形
        canvas.drawBitmap(mBitmap, mMatrix, null);
    }
}

运行后效果

此时在圆上围绕坐标系原点旋转

因为使用canvas.translate()将坐标系进行了调整,圆心处其实就是坐标系原点(0,0),此时小飞机有两个问题

  1. 朝向并不是正切线方向
  2. 小飞机自身中心不在圆上

此时代码并没有使用正切矩阵


修改代码,加入正切矩阵:

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    //得到矩阵正切和位置矩阵
    mPathMeasure.getMatrix(mLength * mAnimatorValue, mMatrix, PathMeasure.POSITION_MATRIX_FLAG|PathMeasure.TANGENT_MATRIX_FLAG );
    canvas.translate(getWidth() / 2, getHeight() / 2);//将坐标系移动到控件的中心位置
    canvas.drawPath(mPath, mPaint);
    //绘制小三角形
    mMatrix.preRotate(270);//调整朝向,朝着正切线的方向
    canvas.drawBitmap(mBitmap, mMatrix, null);
}

**注意:对Matrix的操作应该放在getMatrix()之后,getMatrix()会将之前的操作重置掉 **

  1. 先加上正切矩阵PathMeasure.TANGENT_MATRIX_FLAG,但由于正切矩阵的影响,小飞机的角度需要调整
  2. 然后,再mMatrix.preRotate(270),这里旋转的角度需要根据自己的图片来修改
小飞机不在圆上

第一个问题解决后,第二个问题也就好解决了,只需要利用前乘平移,将小飞机的中心朝左上方移动,移动到圆上就好了

//绘制小三角形
mMatrix.preRotate(270);//调整朝向,朝着正切线的方向
mMatrix.preTranslate(-mBitmap.getWidth() / 2, -mBitmap.getHeight() / 2);//将小飞机移动到圆上
canvas.drawBitmap(mBitmap, mMatrix, null);

最终效果

终于比较正常了

PathMeasure的方法差不多学习完了


2. 最后

PathMeasurePath在自定义View使用的比较多,需要再多学习。

本篇的学习主要就是抄袭徐医生和GcsSloop同学的博客 :)

共勉 :)

你可能感兴趣的:(Android 自定义View学习(十六)——PathMeasure学习)