轉(zhuǎn)自 徐醫(yī)生的簡書文章
PathMeasure之迷徑追蹤
Path耘沼,不論是在自定義View還是動畫,都占有舉足輕重的地位盅藻。繪制Path购桑,可以通過Android提供的API畅铭,或者是貝塞爾曲線、數(shù)學(xué)函數(shù)勃蜘、圖形組合等等方式硕噩,而要獲取Path上每一個構(gòu)成點的坐標(biāo),一般需要知道Path的函數(shù)方法缭贡,例如求解貝塞爾曲線上的點的De Casteljau算法炉擅,但對于一般的Path來說,是很難通過簡單的函數(shù)方法來進(jìn)行計算的阳惹,那么坑资,如何來定位任意一個給定Path的任意一個點的坐標(biāo)呢?
Android SDK提供了一個非常有用的API來幫助開發(fā)者實現(xiàn)這樣一個Path路徑點的坐標(biāo)追蹤穆端,這個類就是PathMeasure咕痛,它可以認(rèn)為是一個Path的坐標(biāo)計算器倍权。
初始化
PathMeasure類似一個計算器憎蛤,對它進(jìn)行初始化只需要new一個PathMeasure對象即可:
PathMeasure pathMeasure = new PathMeasure();
初始化PathMeasure后卦方,可以通過PathMeasure.setPath()的方式來將Path和PathMeasure進(jìn)行綁定,例如:
pathMeasure.setPath(path, true);
當(dāng)然荒勇,你也可以直接使用PathMeasure的有參構(gòu)造方法來進(jìn)行初始化:
PathMeasure (Path path, boolean forceClosed)
這里最不容易理解的就是第二個boolean參數(shù)forceClosed柒莉。
forceClosed參數(shù)
這個參數(shù)——forceClosed,簡單的說沽翔,就是Path最終是否需要閉合兢孝,如果為True的話,則不管關(guān)聯(lián)的Path是否是閉合的仅偎,都會被閉合跨蟹。
但是這個參數(shù)對Path和PathMeasure的影響是需要解釋下的:
- forceClosed參數(shù)對綁定的Path不會產(chǎn)生任何影響,例如一個折線段的Path橘沥,本身是沒有閉合的窗轩,forceClosed設(shè)置為True的時候,PathMeasure計算的Path是閉合的座咆,但Path本身繪制出來是不會閉合的痢艺。
- forceClosed參數(shù)對PathMeasure的測量結(jié)果有影響,還是例如前面說的一個折線段的Path介陶,本身沒有閉合堤舒,forceClosed設(shè)置為True,PathMeasure的計算就會包含最后一段閉合的路徑哺呜,與原來的Path不同舌缤。
API
PathMeasure的API非常容易理解,幾乎都是望文生義。
getLength
PathMeasure.getLength()的使用非常廣泛友驮,其作用就是獲取計算的路徑長度漂羊。
getSegment
boolean getSegment (float startD, float stopD, Path dst, boolean startWithMoveTo)
這個API用于截取整個Path的片段驾锰,通過參數(shù)startD和stopD來控制截取的長度卸留,并將截取的Path保存到dst中,最后一個參數(shù)startWithMoveTo表示起始點是否使用moveTo方法椭豫,通常為True耻瑟,保證每次截取的Path片段都是正常的、完整的赏酥。
如果startWithMoveTo設(shè)置為false喳整,通常是和dst一起使用,因為dst中保存的Path是被不斷添加的裸扶,而不是每次被覆蓋框都,設(shè)置為false,則新增的片段會從上一次Path終點開始計算呵晨,這樣可以保存截取的Path片段數(shù)組連續(xù)起來魏保。
nextContour
nextContour()方法用的比較少,比較大部分情況下都只會有一個Path而不是多個摸屠,畢竟這樣會增加Path的復(fù)雜度谓罗,但是如果真有一個Path,包含了多個Path季二,那么通過nextContour這個方法檩咱,就可以進(jìn)行切換,同時胯舷,默認(rèn)的API刻蚯,例如getLength,獲取的也是當(dāng)前的這段Path所對應(yīng)的長度桑嘶,而不是所有的Path的長度芦倒,同時,nextContour獲取Path的順序不翩,與Path的添加順序是相同的兵扬。
getPosTan
boolean getPosTan (float distance, float[] pos, float[] tan)
這個API用于獲取路徑上某點的坐標(biāo)及其切線的坐標(biāo),這個API非常強(qiáng)大口蝠,但是比較難理解器钟,后面會結(jié)合例子來講解。
簡單的說妙蔗,就是通過指定distance(0<distance<getLength)傲霸,來獲取坐標(biāo)點和切線的坐標(biāo),并保存到pos[]和tan[]數(shù)組中。
硬件加速的Bug
由于硬件加速的問題昙啄,PathMeasure中的getSegment在講Path添加到dst數(shù)組中時會被導(dǎo)致一些錯誤穆役,需要通過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圖,效果如下所示:
我們只需要修改下起始值的數(shù)字即可叛溢,關(guān)鍵代碼如下:
@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);
}
路徑繪制——另辟蹊徑
關(guān)于路徑繪制塑悼,View的始祖Romain Guy曾經(jīng)有一篇文章講解了一個很使用的技巧,地址如下所示:
http://www.curious-creature.com/2013/12/21/android-recipe-4-path-tracing/
Romain Guy使用DashPathEffect來實現(xiàn)了路徑繪制楷掉。
DashPathEffect(float[] intervals, float phase)
DashPathEffect傳入了一個intervals數(shù)組厢蒜,用來控制實線和虛線的數(shù)組的顯示,那么當(dāng)實線和虛線都是整個路徑的長度時烹植,整個路徑就只顯示實線或者虛線了斑鸦,這時候通過第二個參數(shù)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();
}
}
其關(guān)鍵代碼就是在于設(shè)置:
mEffect = new DashPathEffect(new float[]{length, length}, fraction * length);
后面通過屬性動畫來控制路徑繪制即可攒庵。
坐標(biāo)與切線
PathMeasure的getPosTan()方法败晴,可以獲取路徑上的坐標(biāo)點和對應(yīng)點的切線坐標(biāo)浓冒,其中,路徑上對應(yīng)的點非常好理解尖坤,就是對應(yīng)的點的坐標(biāo),而另一個參數(shù)tan[]數(shù)組慢味,它用于返回當(dāng)前點的運動軌跡的斜率,要理解這個API纯路,我們首先來看下Math中的atan2這個方法:
public static double atan2 (double y, double x)
雖然atan()方法可以用于求一個反正切值或油,但是他傳入的是一個角度,所以我們使用atan2()方法:
Math.atan2()函數(shù)返回點(x,y)和原點(0,0)之間直線的傾斜角
那么如何計算任意兩點間直線的傾斜角呢?只需要將兩點x,y坐標(biāo)分別相減得到一個新的點(x2-x1,y2-y1)驰唬。然后利用它求出角度即可——Math.atan2(y2-y1,x2-x1)顶岸。
利用這個API,通诚接叮可以獲取Path上的點坐標(biāo)和點的運動趨勢,對于運動趨勢杯拐,通常通過Math.atan2()來轉(zhuǎn)換為切線的角度,代碼如下所示:
@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);
}
根據(jù)這個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將運動趨勢的角度轉(zhuǎn)換為畫布的旋轉(zhuǎn)歌憨,這樣每次繪制切線墩衙,就只需要畫一條同樣的切線即可务嫡。
源代碼
源碼已上傳到Github: