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);
}
}</code></pre>
通過這種方式,只需要做一點點小的修改,就可以完成一個比較有意思的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();
}
}</code></pre>
其關鍵代碼就是在于設置:
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();
}
}</code></pre>
Demo效果如圖所示:

只不過這里在繪制的時候,使用了一些Trick,先通過canvas.translate方法將原點移動的圓心,同時,通過canvas.rotate將運動趨勢的角度轉換為畫布的旋轉,這樣每次繪制切線,就只需要畫一條同樣的切線即可。
來自:hhttp://www.tuicool.com/articles/j2qquuy