PathMeasure之迷径追踪

PathMeasure之迷径追踪

Path,不论是在自定义View还是动画,都占有举足轻重的地位。绘制Path,可以通过Android提供的API,或者是贝塞尔曲线、数学函数、图形组合等等方式,而要获取Path上每一个构成点的坐标,一般需要知道Path的函数方法,例如求解贝塞尔曲线上的点的De Casteljau算法,但对于一般的Path来说,是很难通过简单的函数方法来进行计算的,那么,如何来定位任意一个给定Path的任意一个点的坐标呢?

Android SDK提供了一个非常有用的API来帮助开发者实现这样一个Path路径点的坐标追踪,这个类就是PathMeasure,它可以认为是一个Path的坐标计算器。

初始化

PathMeasure类似一个计算器,对它进行初始化只需要new一个PathMeasure对象即可:

PathMeasure pathMeasure = new PathMeasure();

初始化PathMeasure后,可以通过PathMeasure.setPath()的方式来将Path和PathMeasure进行绑定,例如:

pathMeasure.setPath(path, true);

当然,你也可以直接使用PathMeasure的有参构造方法来进行初始化:

PathMeasure (Path path, boolean forceClosed)

这里最不容易理解的就是第二个boolean参数forceClosed。

forceClosed参数

这个参数——forceClosed,简单的说,就是Path最终是否需要闭合,如果为True的话,则不管关联的Path是否是闭合的,都会被闭合。

但是这个参数对Path和PathMeasure的影响是需要解释下的:

  • forceClosed参数对绑定的Path不会产生任何影响,例如一个折线段的Path,本身是没有闭合的,forceClosed设置为True的时候,PathMeasure计算的Path是闭合的,但Path本身绘制出来是不会闭合的。
  • forceClosed参数对PathMeasure的测量结果有影响,还是例如前面说的一个折线段的Path,本身没有闭合,forceClosed设置为True,PathMeasure的计算就会包含最后一段闭合的路径,与原来的Path不同。

API

PathMeasure的API非常容易理解,几乎都是望文生义。

getLength

PathMeasure.getLength()的使用非常广泛,其作用就是获取计算的路径长度。

getSegment

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

这个API用于截取整个Path的片段,通过参数startD和stopD来控制截取的长度,并将截取的Path保存到dst中,最后一个参数startWithMoveTo表示起始点是否使用moveTo方法,通常为True,保证每次截取的Path片段都是正常的、完整的。

如果startWithMoveTo设置为false,通常是和dst一起使用,因为dst中保存的Path是被不断添加的,而不是每次被覆盖,设置为false,则新增的片段会从上一次Path终点开始计算,这样可以保存截取的Path片段数组连续起来。

nextContour

nextContour()方法用的比较少,比较大部分情况下都只会有一个Path而不是多个,毕竟这样会增加Path的复杂度,但是如果真有一个Path,包含了多个Path,那么通过nextContour这个方法,就可以进行切换,同时,默认的API,例如getLength,获取的也是当前的这段Path所对应的长度,而不是所有的Path的长度,同时,nextContour获取Path的顺序,与Path的添加顺序是相同的。

getPosTan

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

这个API用于获取路径上某点的坐标及其切线的坐标,这个API非常强大,但是比较难理解,后面会结合例子来讲解。

简单的说,就是通过指定distance(0

硬件加速的Bug

由于硬件加速的问题,PathMeasure中的getSegment在讲Path添加到dst数组中时会被导致一些错误,需要通过mDst.lineTo(0,0)来避免这样一个Bug。

Demo

路径绘制

路径绘制是PathMeasure最常用的功能,其原理就是通过getSegment来不断截取Path片段,从而不断绘制完整的路径,效果如图所示:

代码如下所示:

package xys.com.pathart.views;

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.util.AttributeSet;
import android.view.View;

/**
 * 路径动画 PathMeasure
 * <p/>
 * Created by xuyisheng on 16/7/15.
 */
public class PathPainter extends View {

    private Path mPath;
    private Paint mPaint;
    private PathMeasure mPathMeasure;
    private float mAnimatorValue;
    private Path mDst;
    private float mLength;

    public PathPainter(Context context) {
        super(context);
    }

    public PathPainter(Context context, AttributeSet attrs) {
        super(context, attrs);
        mPathMeasure = new PathMeasure();
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(5);
        mPath = new Path();
        mPath.addCircle(400, 400, 100, Path.Direction.CW);
        mPathMeasure.setPath(mPath, true);
        mLength = mPathMeasure.getLength();
        mDst = new Path();

        final ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                mAnimatorValue = (float) valueAnimator.getAnimatedValue();
                invalidate();
            }
        });
        valueAnimator.setDuration(2000);
        valueAnimator.setRepeatCount(ValueAnimator.INFINITE);
        valueAnimator.start();
    }

    public PathPainter(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mDst.reset();
        // 硬件加速的BUG
        mDst.lineTo(0,0);
        float stop = mLength * mAnimatorValue;
        mPathMeasure.getSegment(0, stop, mDst, true);
        canvas.drawPath(mDst, mPaint);
    }
}

通过这种方式,只需要做一点点小的修改,就可以完成一个比较有意思的loading图,效果如下所示:

我们只需要修改下起始值的数字即可,关键代码如下:

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    mDst.reset();
    // 硬件加速的BUG
    mDst.lineTo(0,0);
    float stop = mLength * mAnimatorValue;
    float start = (float) (stop - ((0.5 - Math.abs(mAnimatorValue - 0.5)) * mLength));
    mPathMeasure.getSegment(start, stop, mDst, true);
    canvas.drawPath(mDst, mPaint);
}

路径绘制——另辟蹊径

关于路径绘制,View的始祖Romain Guy曾经有一篇文章讲解了一个很使用的技巧,地址如下所示:

http://www.curious-creature.com/2013/12/21/android-recipe-4-path-tracing/

Romain Guy使用DashPathEffect来实现了路径绘制。

DashPathEffect(float[] intervals, float phase)

DashPathEffect传入了一个intervals数组,用来控制实线和虚线的数组的显示,那么当实线和虚线都是整个路径的长度时,整个路径就只显示实线或者虚线了,这时候通过第二个参数phase来控制起始偏移量,就可以完成整个路径的绘制了,这的确是一个非常trick而且有效的方式,效果如图所示:

代码如下所示:

package xys.com.pathart.views;

import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.DashPathEffect;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PathEffect;
import android.graphics.PathMeasure;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.AccelerateDecelerateInterpolator;

/**
 * 路径绘制——DashPathEffect
 * <p/>
 * Created by xuyisheng on 16/7/15.
 */
public class PathPainterEffect extends View implements View.OnClickListener{

    private Paint mPaint;
    private Path mPath;
    private PathMeasure mPathMeasure;
    private PathEffect mEffect;
    private float fraction = 0;
    private ValueAnimator mAnimator;

    public PathPainterEffect(Context context) {
        super(context);
    }

    public PathPainterEffect(Context context, AttributeSet attrs) {
        super(context, attrs);
        mPath = new Path();
        mPath.reset();
        mPath.moveTo(100, 100);
        mPath.lineTo(100, 500);
        mPath.lineTo(400, 300);
        mPath.close();

        mPaint = new Paint();
        mPaint.setColor(Color.BLACK);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(5);

        mPathMeasure = new PathMeasure(mPath, false);
        final float length = mPathMeasure.getLength();

        mAnimator = ValueAnimator.ofFloat(1, 0);
        mAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
        mAnimator.setDuration(2000);
        mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                fraction = (float) valueAnimator.getAnimatedValue();
                mEffect = new DashPathEffect(new float[]{length, length}, fraction * length);
                mPaint.setPathEffect(mEffect);
                invalidate();
            }
        });
        setOnClickListener(this);
    }

    public PathPainterEffect(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawPath(mPath, mPaint);
    }

    @Override
    public void onClick(View view) {
        mAnimator.start();
    }
}

其关键代码就是在于设置:

mEffect = new DashPathEffect(new float[]{length, length}, fraction * length);

后面通过属性动画来控制路径绘制即可。

坐标与切线

PathMeasure的getPosTan()方法,可以获取路径上的坐标点和对应点的切线坐标,其中,路径上对应的点非常好理解,就是对应的点的坐标,而另一个参数tan[]数组,它用于返回当前点的运动轨迹的斜率,要理解这个API,我们首先来看下Math中的atan2这个方法:

public static double atan2 (double y, double x)

虽然atan()方法可以用于求一个反正切值,但是他传入的是一个角度,所以我们使用atan2()方法:

Math.atan2()函数返回点(x,y)和原点(0,0)之间直线的倾斜角

那么如何计算任意两点间直线的倾斜角呢?只需要将两点x,y坐标分别相减得到一个新的点(x2-x1,y2-y1)。然后利用它求出角度即可——Math.atan2(y2-y1,x2-x1)。

利用这个API,通常可以获取Path上的点坐标和点的运动趋势,对于运动趋势,通常通过Math.atan2()来转换为切线的角度,代码如下所示:

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    mMeasure.getPosTan(mMeasure.getLength() * currentValue, pos, tan);
    float degrees = (float) (Math.atan2(tan[1], tan[0]) * 180.0 / Math.PI);
}

根据这个API,我们可以模拟一个圆上的点和点的运动趋势,代码如下:

package xys.com.pathart.views;

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.util.AttributeSet;
import android.view.View;

/**
 * 曲线上切点
 * <p/>
 * Created by xuyisheng on 16/7/15.
 */
public class PathTan extends View implements View.OnClickListener {

    private Path mPath;
    private float[] pos;
    private float[] tan;
    private Paint mPaint;
    float currentValue = 0;
    private PathMeasure mMeasure;

    public PathTan(Context context) {
        super(context);
    }

    public PathTan(Context context, AttributeSet attrs) {
        super(context, attrs);
        mPath = new Path();
        mPaint = new Paint();
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(4);
        mMeasure = new PathMeasure();
        mPath.addCircle(0, 0, 200, Path.Direction.CW);
        mMeasure.setPath(mPath, false);
        pos = new float[2];
        tan = new float[2];
        setOnClickListener(this);
    }

    public PathTan(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mMeasure.getPosTan(mMeasure.getLength() * currentValue, pos, tan);
        float degrees = (float) (Math.atan2(tan[1], tan[0]) * 180.0 / Math.PI);

        canvas.save();
        canvas.translate(400, 400);
        canvas.drawPath(mPath, mPaint);
        canvas.drawCircle(pos[0], pos[1], 10, mPaint);
        canvas.rotate(degrees);
        canvas.drawLine(0, -200, 300, -200, mPaint);
        canvas.restore();
    }

    @Override
    public void onClick(View view) {
        ValueAnimator animator = ValueAnimator.ofFloat(0, 1);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                currentValue = (float) valueAnimator.getAnimatedValue();
                invalidate();
            }
        });
        animator.setDuration(3000);
        animator.setRepeatCount(ValueAnimator.INFINITE);
        animator.start();
    }
}

Demo效果如图所示:

只不过这里在绘制的时候,使用了一些Trick,先通过canvas.translate方法将原点移动的圆心,同时,通过canvas.rotate将运动趋势的角度转换为画布的旋转,这样每次绘制切线,就只需要画一条同样的切线即可。

源代码

源码已上传到Github:

https://github.com/xuyisheng/PathArt

时间: 2024-11-20 17:34:13

PathMeasure之迷径追踪的相关文章

【聚划算 Android 技术周刊 第十三期- 20161118】

我们是聚划算无线Android团队,目前负责聚划算客户端.手淘天猫聚划算插件.淘抢购插件.俪人购客户端等,欢迎交流同时欢迎优秀的人才转岗或者加盟 再没有什么事情比"收快递"更让人感到愉悦了! 要说有,那就是"收很多快递"! Android 热点资讯 谷歌最新发布:照片扫描App 谷歌拥有强大的机器学习能力,使得在摄影和图像处理这个领域,谷歌推出的"照片扫描" App 从中获益 http://www.pixcn.cn/article-5091-1.

35句细节 35句句句都点出了隐藏的信息_励志篇

1.有识有胆,有胆有识,知识与胆量是互相促进的 2.体育锻炼可以(有时可以迅速)使人乐观(科学实验证明) 3.勤奋,机会,乐观是成功的三要素.(注意:传统观念认为勤奋和机会是成功的要素,但是经过统计学和成功人士的分析得出,乐观是成功的第三要素) 4.自信是人格的核心 5.获得的成功越大,就越令人高兴(野心是使人勤奋的原因,节制使人枯萎) 6.热爱你所拥有的--列夫·托尔斯泰 7.(一般情况下)不想三年以后的事,只想现在的事(现在有成就,以后才能更辉煌) 8.把问题看宽广些,没有解决不了的事(真理

受挫时背背激励语录

据说是可以激励人的一生----             1.不问收获,只问耕耘.(如同种树,先有根茎,再有枝叶,尔后花实,好好劳动,不要想太多,那样只会使人胆小.懒惰)   2.体育锻炼可以(有时可以迅速)使人乐观(科学实验证明).   3.勤奋,机会,乐观是成功的三要素.(注意:传统观念认为勤奋和机会是成功的要素,但是经过统计学和成功人士的分析得出,乐观是成功的第三要素)   4.自信是人格的核心.   5.获得的成功越大,就越令人高兴.(野心是使人勤奋的原因,节制使人枯萎)   6.热爱你所

大数据迷潮下的教育研究及其想象力

"大数据"似乎已经成为一种潮流,甚至是一种时尚.人们还没来得及知道它"是什么",就已经置身其中并迷狂不已,更别提仔细思考"为什么"了.所以,用"迷潮"来形容一点也不为过.产业.金融.物流等各界人士摩拳擦掌.跃跃欲试,热烈讨论这种新技术和新资源所带来的巨大福利,生怕落下新时代疾驰而过的"幸福列车".尚在努力向"小数据"靠拢的教育研究,似乎也按捺不住转型的冲动,宣称"传统数据研究无

大部分数码迷们每天的日程表

腾讯数码讯(编译:肖夏雯)对iPhone的跟进已经不是什么特别的事情,这几乎成为大部分数码迷们每天的日程表,和吃饭睡觉一般平常.随着iPhone 6发布日的邻近,猜测与传闻简直如春天的柳絮般漫天飞舞,在这里,我们将到目前为止有关iPhone 6的传闻进行汇总,包括外形设计.参数配置.价格和发布日期,让各位能好好地理理思绪,也好在iPhone 6被官方发布之前有个大致轮廓.虽然不能确定它们是真实可靠的信息,不过也能给各位果迷们解解渴.好朋友们,你们准备好了么?走起. 设计 如之前iPhone 5c

微软“魔法钟”家人动向全追踪

据新华社电 令"哈利·波特"迷倾心不已.能指示家庭成员行踪的"魔法钟"不再是哈利密友罗恩·韦斯利一家的"专利".微软公司最新推出一款"定位钟",通过追踪家庭成员手机信号,即可实时掌握他们的行踪. 这款"定位钟"外形与普通钟表类似.用户需先在手机上下载特定系统软件,将手机移动网络划分为"工厂"."学校"或"健身房"等不同区域.这些区域会显示在&quo

微软推出“定位钟”可追踪家庭成员手机信号

令"哈利·波特"迷倾心不已.能指示家庭成员行踪的"魔法钟"不再是哈利密友罗恩·韦斯利一家的"专利".美国微软公司最新推出一款"定位钟",通过追踪家庭成员手机信号,即可实时掌握他们的行踪. 这款"定位钟"外形与普通钟表类似.用户需先在手机上下载特定系统软件,将手机移动网络划分为"工厂"."学校"或"健身房"等不同区域.这些区域会显示在"定位

追踪“洛楷恋”日后出路:梁洛施有望重拾电影

<盗墓迷城3>中梁洛施造型 梁洛施在<盗墓迷城3>中表现抢眼 梁洛施(右)曾为电影苦练舞蹈 追踪"洛楷恋" 日后出路 新快报讯 梁洛施在26日晚的分手声明中表示"藉此声明,望能结束外界对我无休止的猜想,臆度",但显然这只是她一厢情愿的想法,连日来关于她和李泽楷分手内幕的传闻和猜测不断.尽管到目前为止没有人知道梁洛施和李泽楷为什么分手,分手费多少,但是恢复自由身的梁洛施竟一下子成为影坛抢手货,看来再闯好莱坞重拾电影梦并非难事. 电影人大赞有潜质

走秀网甩单事件追踪:已与两位消费者和解

中介交易 http://www.aliyun.com/zixun/aggregation/6858.html">SEO诊断淘宝客 站长团购 云主机 技术大厅 新闻追踪 上次报道索引 时间:2012年7月3日 版次:A20<深圳新闻> 题目:<苦等一月 等来一条退款短信> 本报讯 (记者 仇日红) 本报昨日报道走秀网甩单事件之后,引起了走秀网官方的重视. 昨天,该网站公关部的工作人员告诉记者,已经主动与报道中提到的两位消费者沟通,给出的和解方案也得到了消费者的接受.