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

学习资料:

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


1. PathMeasure <p>

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

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


1.1 初始化,构造方法 <p>

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()截取片段 <p>

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

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);//绘制截取的片段
    }
}

布局文件

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/activity_path_measure"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.szlk.customview.custom.PathLoadingView
        android:layout_width="200dp"
        android:layout_height="200dp"
        android:layout_centerInParent="true" />

</RelativeLayout>
截取四分之一圆

红色就是截取的片段


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

修改代码:

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

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

dst内有值

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

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);//绘制截取的片段
    }
}
PathLoadingView

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


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

  • boolean getPosTan(float distance, float pos[], float tan[])
    可以获取路径上一个点的坐标以及该点的正切值
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轴坐标,邻边的边长
tan值取自单位圆上对应角度的坐标

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

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

注意X,Y值顺序

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

角度 = 弧度 * 180 / π

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


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

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. 最后 <p>

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

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

共勉 :)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容