美文网首页Android自定义ViewAndroid开发自定义view相关
Android 自定义View学习(十六)——PathMeasu

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

作者: 英勇青铜5 | 来源:发表于2016-10-09 22:27 被阅读1185次

    学习资料:

    徐医生,《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同学的博客 :)

    共勉 :)

    相关文章

      网友评论

      • wphper: :smile: :+1:
        英勇青铜5:@wphper :smile: :smile:
      • dodo_lihao:很详细,很棒!能否弄个github,学鸿洋大神,每个demo弄一个单独的Module
        英勇青铜5:@dodo_lihao 好的,以后也把代码上传
        dodo_lihao:@英勇青铜5 敢于面对自己每一行代码,才是真英雄。大家一起提出意见,才是真正帮助自己提高。记得原来群里面,鸿洋大神也经常会听别人比较好的意见。自己主观想法,可能比较直了一点
        英勇青铜5:@dodo_lihao 有,一直觉得代码写的烂,不想丢垃圾在上面。。。

      本文标题:Android 自定义View学习(十六)——PathMeasu

      本文链接:https://www.haomeiwen.com/subject/ubjmyttx.html