自定义View-第十步:PathMeasure

前言

根据Gcssloop所学习的自定义View整理与笔记。

一. 简单了解PathMeasure

** PathMeasure是一个用来测量Path的类**
1. 相关方法

  • 构造方法
方法名 作用
PathMeasure() 创建一个空的PathMeasure
PathMeasure(Path path, boolean forceClosed) 创建PathMeasure并关联一个指定的Path(Path)需要已经创建完成
  • 公共方法
返回值 方法名 作用
void setPath(Path path, boolean forceClosed) 关联一个path
boolean isClosed() 是否关闭
float getLength() 获取path的长度
boolean nextContour() 跳转到下一个轮廓
boolean getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo) 截取片段
boolean getPosTan(float distance,floast[] pos,float[] tan) 获取指定长度的位置坐标及该点切线值
boolean getMatrix(float distance, Matrix matrix, int flags) 获取指定长度的位置坐标及该点Matrix

二.详细介绍各个方法

1.构造函数

  • PathMeasure()

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

  • PathMeasure (Path path, boolean forceClosed)
  • path:被关联的path,其实和创建一个空的 PathMeasure再调用 setPath 方法来与 Path 进行关联效果是一样的
  • forceClosed:用来确保path闭合,如果设置为true,则不论之前path是否闭合,都会自动闭合该path(如果path可以闭合的话),但是不会影响之前path的状态。
    注意:forceClosed 的设置状态可能会影响测量结果,如果 Path 未闭合但在与 PathMeasure 关联的时候设置 forceClosed 为 true 时,测量结果可能会比 Path 实际长度稍长一点,获取到到是该 Path 闭合时的状态。

demo:

       paint.setStyle(Paint.Style.STROKE);
       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);

       Log.d("PathMeasure", "forceClosed=false---->" + measure1.getLength());//600
       Log.d("PathMeasure", "forcedClosed=true----->" + measure2.getLength());//800

       canvas.drawPath(path, paint);

2.setPath、 isClosed 和 getLength

  • setPath: 和构造函数中的path作用是一样的,就是设置关联的path
  • isClosed: 用于判断 Path 是否闭合,但是如果你在关联 Path 的时候设置 forceClosed 为 true 的话,这个方法的返回值则一定为true。
  • getLength:用于获取path的长度

3.boolean getSegment (float startD, float stopD, Path dst, boolean startWithMoveTo)

  • 返回值 boolean:判断截取是否成功,true 表示截取成功,结果添加到dst中,false 截取失败,不会改变dst中内容
  • startD:开始截取位置距离Path起点的长度,取值范围:0<=startD<stopD<=Path的总长度
  • stopD:结束截取位置距离Path起点的长度,取值范围:0<=startD<stopD<=Path的总长度
  • dst: 截取的Path将会添加到dst中,注意:是添加,不是替换
  • startWithMoveTo:起始点是否使用moveTo,用于保证截取的Path第一个点位置不变,如果不使用,则会改变截取的path的起始点。
    如果不清楚的话,木关系,下边会有demo的哦
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeWidth(10);
        canvas.translate(400, 400);

        Path path = new Path();
        //创建一个顺时针的矩形
        path.addRect(-200, -200, 200, 200, Path.Direction.CW);

        // 将 Path 与 PathMeasure 关联
        PathMeasure measure = new PathMeasure(path, false);
        Path dst = new Path();
        dst.lineTo(-300, -300);
        // 截取矩形的一部分,添加到dst中,并使用 moveTo 保持截取得到的 Path 第一个点的位置不变
        measure.getSegment(200, 600, dst, true);
        canvas.drawPath(dst, paint);
startWithMoveTo=true效果图
//仅仅将上边的demo的startWithMoveTo设为false
        measure.getSegment(200, 600, dst, false);
startWithMoveTo=false效果图

如果在安卓4.4或者之前的版本,在默认开启硬件加速的情况下,更改 dst 的内容后可能绘制会出现问题,请关闭硬件加速或者给 dst 添加一个单个操作,例如: dst.rLineTo(0, 0)

4. nextContour
nextContour 用于跳转到下一条曲线,true表示成功,false表示失败。
例如下边的图,便是内外两条曲线:

来自于http://www.gcssloop.com/customview/Path_PathMeasure

举个栗子:

       paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeWidth(10);
        canvas.translate(400, 400);
        Path path = new Path();
        // 添加小矩形
        path.addRect(-100, -100, 100, 100, Path.Direction.CCW);
        // 添加大矩形
        path.addRect(-200, -200, 200, 200, Path.Direction.CW);
        // 绘制 Path
        canvas.drawPath(path, paint);
        // 将Path与PathMeasure关联
        PathMeasure measure = new PathMeasure(path, true);
        // 获得第一条路径的长度
        float len1 = measure.getLength();
        // 跳转到下一条路径
        measure.nextContour();
        // 获得第二条路径的长度
        float len2 = measure.getLength();
        // 输出两条路径的长度
        Log.i("LEN", "len1=" + len1);  //结果 800
        Log.i("LEN", "len2=" + len2);   //结果 1600

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

5.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[0]是邻边边长,tan[1]是对边边长,tanΘ=对边/ 邻边=tan[1]/tan[0]。
    例如,我们需要计算旋转角度,则可以这样
 //tan[1],tan[0]千万千万不要反了
float degrees = (float) (Math.atan2(tan[1], tan[0]) * 180.0 / Math.PI);

举个栗子吧,注意箭头的方向哦O(∩_∩)O~


沿着路径移动的箭头
   private void init() {
        paint = new Paint();
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeWidth(10);
        
        matrix = new Matrix();
        pos = new float[2];
        tan = new float[2];
        
        BitmapFactory.Options options = new BitmapFactory.Options();
        //缩放图片
        options.inSampleSize = 2;
        bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.row, options);

    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        //路径
        Path path = new Path();
        path.lineTo(200, 200);
        path.lineTo(400, 200);
        path.lineTo(300, 500);
        path.lineTo(0, 700);

        //进度
        currentValue += 0.05;
        if (currentValue > 1) {
            currentValue = 0;
        }

        PathMeasure pathMeasure = new PathMeasure(path, false);
        int length = (int) (pathMeasure.getLength() * currentValue);
        pathMeasure.getPosTan(length, pos, tan);

        //获取旋转角度
        float degree = (float) (Math.atan2(tan[1], tan[0]) * 180 / Math.PI);

        //设置图片旋转角度和偏移量,matrix的方法可以类比canvas的操作方法
        matrix.reset();
        matrix.postRotate(degree, bitmap.getWidth() / 2, bitmap.getHeight() / 2);
        matrix.postTranslate(pos[0] - bitmap.getWidth() / 2, pos[1] - bitmap.getHeight() / 2);

        canvas.drawPath(path, paint);
        canvas.drawBitmap(bitmap, matrix, paint);

        try {
            Thread.sleep(300);
        }
        catch (InterruptedException e) {
            e.printStackTrace();
        }
    //最好使用 线程 或者 ValueAnimator 来控制界面的刷新
        invalidate();
    }

大家也可以试试这样子让箭头根据圆来旋转移动


http://www.gcssloop.com/customview/Path_PathMeasure

6. boolean getMatrix (float distance, Matrix matrix, int flags)
用于得到路径上某一长度的位置以及该位置的正切值的矩阵

  • 返回值boolean: 判断获取是否成功 ,true成功,数据会存入matrix中,false失败,matrix内容不变
  • distance: 距离path起点的长度,取值范围0<=distance<=getLength
  • matrix: 根据flags封装好的matrix,根据flags的设置而存入不同的内容
  • flags: 规定哪些内容会存入到matrix中,可选择POSITION_MATRIX_FLAG(位置) 、ANGENT_MATRIX_FLAG(正切),如果两个选项都想选择,可以将两个选项之间用 | 连接起来
   @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        //路径
        Path path = new Path();
        path.lineTo(200, 200);
        path.lineTo(400, 200);
        path.lineTo(300, 500);
        path.lineTo(0, 700);

        //进度
        currentValue += 0.05;
        if (currentValue > 1) {
            currentValue = 0;
        }

        PathMeasure pathMeasure = new PathMeasure(path, false);
        float length =pathMeasure.getLength() * currentValue;

       //---------改变下边的内容------------------------------
         pathMeasure.getMatrix(length,matrix,PathMeasure.POSITION_MATRIX_FLAG|PathMeasure.TANGENT_MATRIX_FLAG);
       // 将图片绘制中心调整到与当前点重合(注意:此处是前乘pre)
         matrix.preTranslate(-bitmap.getWidth()/2,-bitmap.getHeight()/2);
       //---------改变的内容结束------------------------------
        canvas.drawPath(path, paint);
        canvas.drawBitmap(bitmap, matrix, paint);

        try {
            Thread.sleep(300);
        }
        catch (InterruptedException e) {
            e.printStackTrace();
        }
        invalidate();
    }

效果图和上边一样哦

1.对 matrix 的操作必须要在 getMatrix 之后进行,否则会被 getMatrix 重置而导致无效。
2.矩阵对旋转角度默认为图片的左上角,我们此处需要使用 preTranslate 调整为图片中心。
3.使用pre,越靠后越先执行,即后调用的pre操作先执行。会在后续Matrix章节详细讲解

三. postInvalidate()和Invalidate()区别

这两个函数的作用都是用来重绘控件的,但区别是Invalidate()一定要在UI线程执行,如果不是在UI线程就会报错。而postInvalidate()则没有那么多讲究,它可以在任何线程中执行,而不必一定要是主线程。其实在postInvalidate()就是利用handler给主线程发送刷新界面的消息来实现的,所以它是可以在任何线程中执行,而不会出错。而正是因为它是通过发消息来实现的,所以它的界面刷新可能没有直接调Invalidate()刷的那么快。
所以在我们确定当前线程是主线程的情况下,还是以invalide()函数为主。当我们不确定当前要刷新页面的位置所处的线程是不是主线程的时候,还是用postInvalidate为好;

四.一个动画撒

动画

代码如下:

package com.xiaohongchun.redlips.view;

import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PathMeasure;
import android.graphics.RectF;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;

import com.orhanobut.Logger;
import com.xiaohongchun.redlips.R;
import com.xiaohongchun.redlips.record.Util;

public class SuccessIcon extends View {

    private Paint paint;
    private Path path, dst;
    private PathMeasure pathMeasure;
    private float value;
    private float radius;

    private int size;
    private ValueAnimator valueAnimator;

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

    public SuccessIcon(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);

    }


    public void startAnimater() {
        Logger.d("pngpng","size="+size);
        size = Math.min(getMeasuredWidth(), getMeasuredHeight());
        radius = size / 2 - Util.dipToPX(getContext(), 2);
        initPathPaint();
        initAnimater();
    }

    private void initPathPaint() {
        paint = new Paint();
        paint.setColor(Color.RED);
        paint.setAntiAlias(true);
        paint.setStrokeCap(Paint.Cap.ROUND);
        paint.setStrokeWidth(Util.dipToPX(getContext(), 1));
        paint.setStyle(Paint.Style.STROKE);
        path = new Path();
        dst = new Path();
        float start = (float) -Math.cos(Math.toRadians(45)) * radius / 2;//开始点
        path.moveTo(start, start);
        path.lineTo(0, 0);
        float end = (float) Math.cos(Math.toRadians(45)) * radius;//线的结束点
        path.rLineTo(end, -end);
        path.arcTo(new RectF(-1 * radius, -1 * radius, radius, radius), -45, -350);
        pathMeasure = new PathMeasure();
        pathMeasure.setPath(path, false);
    }


    private void initAnimater() {
        if (valueAnimator == null) {
            valueAnimator = ValueAnimator.ofFloat(0, 1);
            valueAnimator.setDuration(600);
            valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    value = (float) animation.getAnimatedValue();
                    postInvalidate();
                }
            });

            valueAnimator.start();
        }
    }


    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (size > 0) {
            canvas.translate(size / 2, size / 2);
            pathMeasure.getSegment(0, pathMeasure.getLength() * value, dst, true);
            dst.rLineTo(0, 0);//如果没有关闭硬件加速,则加上这句话,关闭了,则不需要
            canvas.drawPath(dst, paint);
        }
    }


}


       <com.xiaohongchun.redlips.view.SuccessIcon
                    android:id="@+id/activity_goods_pay_success_icon"
                    android:layout_width="30dp"
                    android:layout_height="30dp"
                    />
 icon = (SuccessIcon) findViewById(R.id.success_icon);
        icon.post(new Runnable() {
            @Override
            public void run() {
                icon.startAnimater();
            }
        });

后记

动画

这个是http://www.gcssloop.com/customview/Path_PathMeasure 所实现的动画,
源码是https://github.com/GcsSloop/AndroidNote/blob/master/CustomView/Advance/Code/SearchView.java
主要利用了getSegment进行绘制,这里就不讲解了

参考网站

自定义控件三部曲之绘图篇

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

推荐阅读更多精彩内容