高级UI<第三十篇>:PathMeasure详解
Path
是一个轨迹
或者是一个路径
,无论是动画还是自定义View中都有举足轻重的地位,Path可以绘制直线、圆形、矩形、圆角矩形、椭圆、扇形以及贝塞尔曲线,你们有没有发现,Path所能绘制的图形都有各自的公式
可以精准的计算出轨迹
上的任意一点的坐标。但是,在现实中,一个复杂的轨迹
往往由多个Path组成,那么想要求出复杂Path上的任意一点我想并不容易。PathMeasure
可以精准的获取和追踪Path的坐标,无论Path有多么复杂。
PathMeasure
需要了解的知识有:
(1)构造方法
(2)获取路径长度
(3)截取Path
(4)切换到下一个Path
(5)获取路径上某点的坐标以及切点坐标
构造方法
构造方法有两种,分别是:
- public PathMeasure()
- PathMeasure(Path path, boolean forceClosed)
前者不需要传递形参,但是需要手动调用setPath(Path path, boolean forceClosed)
方法设置一个路径以及是否闭合。
演示代码如下:
PathMeasure pathMeasure = new PathMeasure();
pathMeasure.setPath(path, true);
或者
PathMeasure pathMeasure = new PathMeasure(path, false);
第一个参数是path,表示绑定一个路径,第二个参数是forceClosed,表示是否强制闭合。调用PathMeasure
的isClosed()
方法可以知道Path是否闭合。
什么是路径?洗洗睡吧,路径都不知道话请出门左拐,这篇文章不适合您。
什么叫闭合路径?
答:闭合路径就是路径的首尾相连之后路径,forceClosed的意思和字面上的意思一样,即强制闭合,强制让Path的首尾相连。
获取路径长度
PathMeasure
可以获取路径的长度,其方法如下:
pathMeasure.getLength()
[举例一]
上图中,A为Path的起点,B为Path的终点,假设A到B的线段长度为100,那么请问该Path的长度时多少?
有人说,这题不是很简单吗,上图Path长度为100。
这个答案只能说不够准确,求一个Path的长度必须考虑该Path是否闭合。如果没有闭合,那么该Path的长度就是100,如果已经闭合,那么首尾相连之后长度就是200。
线段的路径的长度有点特殊,这一点需要注意。
[举例二]
这个图形稍微复杂了点,A为Path的起点,D为Path的终点,那么闭合前的长度为:
length = AB+BC+CD
闭合后的长度为
length = AB+BC+CD+AD
[举例三]
上图的路径更加复杂了点,A为Path的起点,E为Path的终点,闭合前的Path长度为:
length = AB+BC+CD+DE
闭合之后的长度为:
length = AB+BC+CD+DE+AE
[举例四]
上图的曲线是一个贝塞尔曲线,A为起点,B为终点,假设该曲线的长度为600,A和B之间的线段长度为500,那么闭合前的长度为600(曲线的长度),闭合之后的长度为1100。
截取Path
public boolean getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo)
截取Path对用的API是getSegment
,第一个参数startD表示截取Path的起始长度,第二个参数stopD表示截取Path的终点长度,将截取后的Path保存到dst中。参数startWithMoveTo一般为true。
另外,这个方法的返回类型是boolean类型,如果返回true,则截取成功,如果返回false,则截取失败。
下面开始举几个例子,例子举完之后再开始介绍startWithMoveTo
属性。
【举例一】
绘制一个圆,并截取一段
如图:
图片.png代码如下:
@Override
protected void onDraw(Canvas canvas) {
path.reset();
path.addCircle(400, 500, 100, Path.Direction.CCW);
pathMeasure = new PathMeasure();
pathMeasure.setPath(path, true);
dst.reset();
boolean success = pathMeasure.getSegment(0, pathMeasure.getLength()-100, dst, true);
//绘制路径
canvas.drawPath(dst, mPaint);
}
代码的意思是,通过Path绘制一个以(400,500)为圆点,半径为100,逆时针绘制一个圆形轨迹,再由PathMeasure
的getSegment
方法截取[0, pathMeasure.getLength()-100]区间的路径,最后将截取后的Path绘制到画布中。
Path的截取功能的强大之处不止如此,请看以下举例。
【举例二】
演示一个圆的绘制轨迹
代码如下:
@Override
protected void onDraw(Canvas canvas) {
path.reset();
path.addCircle(400, 500, 100, Path.Direction.CCW);
pathMeasure = new PathMeasure();
pathMeasure.setPath(path, true);
dst.reset();
boolean success = pathMeasure.getSegment(0, t, dst, true);
if(t >= pathMeasure.getLength()){
t = 0;
}
t++;
//绘制路径
canvas.drawPath(dst, mPaint);
invalidate();
}
【举例三】
进度条效果
如图:(如果将View本身加上旋转动画的话那就更像一个进度条了)
349.gif代码如下:
public class PathTest extends View {
private Paint mPaint;
private PathMeasure pathMeasure;
private float scale = 0;
private Path path = new Path();
private Path dst = new Path();
public PathTest(Context context) {
this(context, null);
}
public PathTest(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public PathTest(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init(){
mPaint = new Paint();
mPaint.setColor(Color.GRAY);
mPaint.setAntiAlias(true);
mPaint.setStrokeWidth(8);
mPaint.setTextSize(60);
mPaint.setStyle(Paint.Style.STROKE);
}
@Override
protected void onDraw(Canvas canvas) {
path.reset();
path.addCircle(400, 500, 100, Path.Direction.CW);
pathMeasure = new PathMeasure();
pathMeasure.setPath(path, true);
dst.reset();
float stopD = pathMeasure.getLength() * scale;
float startD = stopD - pathMeasure.getLength() * Math.abs(scale - 0.5f);
pathMeasure.getSegment(startD, stopD, dst, true);
if(scale >= 1){
scale = 0.01f;
}
scale += 0.01f;
//绘制路径
canvas.drawPath(dst, mPaint);
invalidate();
}
}
最后,说一说startWithMoveTo
属性的艺术吧,一般而言,这个属性设置为true。
将startWithMoveTo
分成三个单词来理解:
- start 开始,即开始绘制的时候
- With:带着,伴随着
- MoveTo:移动到,即将画笔移动到
整体的意思就是:将画笔的位置移动到起始点
,即将画笔的位置移动到上一次的位置
。
当startWithMoveTo
为true时:将画笔的位置移动到上一次的位置
当startWithMoveTo
为false时:画笔位置不动。
以上的理解虽然是正确的,但是往往还不能真正理解startWithMoveTo
的含义,再回来看一下getSegment
方法,如下:
public boolean getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo)
startWithMoveTo
直接作用于dst(截取后的路径),假设将path截取,截取之后的路径存入dst中,当startWithMoveTo
为true时,画笔位置就是path的画笔位置,当startWithMoveTo
为false时,画笔位置就是dst的画笔位置。
【举例】
private Path path = new Path();
private Path dst = new Path();
//一个圆
path.addCircle(400, 500, 100, Path.Direction.CW);
pathMeasure = new PathMeasure();
pathMeasure.setPath(path, true);
//将画笔位置调整到(100,200)
dst.moveTo(100, 200);
//将圆截取掉一部分()
pathMeasure.getSegment(0, pathMeasure.getLength()-100, dst, false);
//绘制路径
canvas.drawPath(dst, mPaint);
看一下以上代码,path中存入一个以(400,500)为圆点,100为半径的圆,此时path画笔的位置是(0,0),dst.moveTo
函数将dst的画笔的位置调整到了(100,200),最后将圆截取掉一部分,startWithMoveTo
值置为false,并绘制出图形。
该代码的效果如下:
图片.png如果将
dst.moveTo(100, 200);
换成
dst.lineTo(100, 200);
效果如下:
图片.png由此,可以断定,如果将startWithMoveTo
设置为false,当前路径的起点会和上一个路径的终点相连。(这就是startWithMoveTo
的最终奥义了)
切换到下一个Path
切换到下一个Path是什么意思?多个Path的情况还可以切换?
那是当然,代码如下:
pathMeasure.nextContour()
PathMeasure有一个nextContour()
方法,可以切换到下一个Path,返回true则切换成功。
我们先看下图:
图片.png上图中,由两条直线和一个曲线组成,实现它的代码如下:
//设置画笔位置
path.moveTo(100, 100);
//一条直线
path.lineTo(200,400);
//贝塞尔曲线
path.quadTo(300, 100, 500, 400);
//一条直线
path.lineTo(600,400);
pathMeasure = new PathMeasure();
pathMeasure.setPath(path, false);
//绘制路径
canvas.drawPath(path, mPaint);
那么,请问,这个图形有有几个Path?
答:仅有一个,上图中,两条直线和一条贝塞尔曲线是连续的,所以只能算一个Path,我们可以使用nextContour方法遍历来验证。
while (pathMeasure.nextContour()){
Log.d("aaa", "路径长度:"+pathMeasure.getLength());
}
发现日志只打印了一次。
我们再来看一张图。
图片.png实现代码如下:
//设置画笔位置
path.moveTo(100, 100);
//一条直线
path.lineTo(200,400);
//贝塞尔曲线
path.quadTo(300, 100, 500, 400);
//设置画笔位置
path.moveTo(500, 500);
//一条直线
path.lineTo(600,400);
pathMeasure = new PathMeasure();
pathMeasure.setPath(path, false);
//绘制路径
canvas.drawPath(path, mPaint);
上图只有两条Path,左边直线和曲线是连续的,所以算一条Path,右边的直线和左边的不连续,所以右边的直线单独算是一条Path,总共两条Path。同样可以通过nextContour遍历验证。
while (pathMeasure.nextContour()){
Log.d("aaa", "路径长度:"+pathMeasure.getLength());
}
获取路径上某点的坐标以及切点坐标
public boolean getPosTan(float distance, float pos[], float tan[])
以上方法可以获取路径上某点坐标,以及切点坐标。
当我们得到坐标和切点坐标时,可以实现下图效果
350.gif直接贴出代码:
private Paint mPaint;
private PathMeasure pathMeasure;
private Path path = new Path();
private Bitmap airplaneBitmap;
private Context mContext;
private float distance = 0;
private float pos[];//坐标
private float tan[];//切点坐标
public PathTest(Context context) {
this(context, null);
}
public PathTest(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public PathTest(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mContext = context;
init();
}
private void init(){
mPaint = new Paint();
mPaint.setColor(Color.BLACK);
mPaint.setAntiAlias(true);
mPaint.setStrokeWidth(80);
mPaint.setTextSize(60);
mPaint.setStyle(Paint.Style.STROKE);
pos = new float[2];
tan = new float[2];
}
@Override
protected void onDraw(Canvas canvas) {
path.reset();
path.addCircle(300, 500, 200, Path.Direction.CW);
pathMeasure = new PathMeasure();
pathMeasure.setPath(path, false);
//绘制路径
canvas.drawPath(path, mPaint);
canvas.save();
if (Build.VERSION.SDK_INT>Build.VERSION_CODES.LOLLIPOP){
Drawable vectorDrawable = mContext.getDrawable(R.drawable.airplane);
airplaneBitmap = Bitmap.createBitmap(vectorDrawable.getIntrinsicWidth(), vectorDrawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
Canvas ca = new Canvas(airplaneBitmap);
vectorDrawable.setBounds(0, 0, ca.getWidth(), ca.getHeight());
vectorDrawable.draw(ca);
}else {
airplaneBitmap = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.airplane);
}
pathMeasure.getPosTan(distance, pos, tan);
float degrees = (float) (Math.atan2(tan[1], tan[0]) * 180.0 / Math.PI);
Matrix matrix=new Matrix();
matrix.postRotate(degrees);
matrix.preTranslate(-airplaneBitmap.getWidth()/2,-airplaneBitmap.getHeight()/2);
matrix.postTranslate(pos[0], pos[1]);
canvas.drawBitmap(airplaneBitmap, matrix, mPaint);
if(distance >= pathMeasure.getLength()){
distance = 0;
}
distance = distance + 4;
invalidate();
}
}
核心只需要了解两点:
(1)根据路径坐标点调整飞机的位置
//偏移量
matrix.preTranslate(-airplaneBitmap.getWidth()/2,-airplaneBitmap.getHeight()/2);
//调整位置
matrix.postTranslate(pos[0], pos[1]);
(2)根据切点求出角度
float degrees = (float) (Math.atan2(tan[1], tan[0]) * 180.0 / Math.PI);
除了pathMeasure.getPosTan(distance, pos, tan)
可以实现以上效果,pathMeasure.getMatrix
也是可以实现以上效果的,代码如下:
public class PathTest extends View {
private Paint mPaint;
private PathMeasure pathMeasure;
private Path path = new Path();
private Bitmap airplaneBitmap;
private Context mContext;
private float distance = 0;
public PathTest(Context context) {
this(context, null);
}
public PathTest(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public PathTest(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mContext = context;
init();
}
private void init(){
mPaint = new Paint();
mPaint.setColor(Color.BLACK);
mPaint.setAntiAlias(true);
mPaint.setStrokeWidth(80);
mPaint.setTextSize(60);
mPaint.setStyle(Paint.Style.STROKE);
}
@Override
protected void onDraw(Canvas canvas) {
path.reset();
path.addCircle(300, 500, 200, Path.Direction.CW);
pathMeasure = new PathMeasure();
pathMeasure.setPath(path, false);
//绘制路径
canvas.drawPath(path, mPaint);
if (Build.VERSION.SDK_INT>Build.VERSION_CODES.LOLLIPOP){
Drawable vectorDrawable = mContext.getDrawable(R.drawable.airplane);
airplaneBitmap = Bitmap.createBitmap(vectorDrawable.getIntrinsicWidth(), vectorDrawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
Canvas ca = new Canvas(airplaneBitmap);
vectorDrawable.setBounds(0, 0, ca.getWidth(), ca.getHeight());
vectorDrawable.draw(ca);
}else {
airplaneBitmap = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.airplane);
}
Matrix matrix=new Matrix();
pathMeasure.getMatrix(distance,matrix,PathMeasure.POSITION_MATRIX_FLAG | PathMeasure.TANGENT_MATRIX_FLAG);
matrix.preTranslate(-airplaneBitmap.getWidth()/2,-airplaneBitmap.getHeight()/2);
canvas.drawBitmap(airplaneBitmap,matrix,mPaint);
if(distance >= pathMeasure.getLength()){
distance = 0;
}
distance = distance + 4;
invalidate();
}
}
这里需要了解两个Flag,分别是:
(1)PathMeasure.POSITION_MATRIX_FLAG:位置信息
(2)PathMeasure.TANGENT_MATRIX_FLAG:切线信息
最后贴出飞机的矢量图代码
airplane.xml
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="60dp"
android:height="40dp"
android:viewportHeight="1024"
android:viewportWidth="1024">
<path android:fillColor="#4DA476" android:pathData="M404.64,353.27c88.97,0 178.25,-0.78 267.19,0.05L476.67,54.78c-8.49,-12.95 -22.59,-18.97 -38.02,-18.97l-135,-0.01 100.99,317.47z"/>
<path android:fillColor="#4DA476" android:pathData="M1019.92,607.23c0,52.59 -42.62,95.25 -95.24,95.25l-244.05,-0L476.62,969.22c-9.36,12.27 -22.58,18.99 -38.02,18.99l-135,-0.01 95.64,-299.21L85.23,591.48c-16.98,-5.27 -28.81,-19 -31.71,-36.57L4.44,254.17c-1.19,-7.18 0.51,-13.84 5.24,-19.39 4.65,-5.61 10.91,-8.5 18.21,-8.5l54.76,-0.01c15.56,0 29.84,6.12 38.22,19.21l89.01,139.52c92.39,0 315.48,0 492.58,0 222.23,-0 317.46,114.41 317.46,222.23v-0.01h0zM511.98,448.5v63.5h-63.49v63.5h63.49v63.48h63.5v-63.48L638.97,575.5v-63.5h-63.49v-63.5h-63.49zM899.96,480.25h-57.06c-4.05,0 -7.58,1.6 -10.24,4.7 -2.7,3.05 -3.77,6.74 -3.21,10.79l3.38,24.52c1.91,13.57 13.29,23.5 26.94,23.5l98.42,-0.01c-10.2,-19.89 -24.92,-37.49 -42.94,-52.17a200.1,200.1 0,0 0,-15.28 -11.31l0,-0.01z"/>
</vector>
[本章完...]