1. 了解
Path的繪制有很多種方法萌京,例如Android API,Bezier曲線或者數(shù)學(xué)函數(shù)表達(dá)式等刷钢,而高級(jí)的動(dòng)畫(huà)都會(huì)要求這個(gè)Path的坐標(biāo)點(diǎn)是可控的卤橄,這樣才能更好地?cái)U(kuò)展基于Path的動(dòng)畫(huà)。而如何確定Path點(diǎn)的坐標(biāo)辛蚊,這就用到了本次分析的工具類PathMeasure粤蝎。
- 常用API
方法 | 解析 |
---|---|
PathMeasure pathMeasure = new PathMeasure(); | 創(chuàng)建PathMeasure對(duì)象 |
pathMeasure.setPath(path, true); | 設(shè)置關(guān)聯(lián)Path |
PathMeasure (Path path, boolean forceClosed) | 在構(gòu)造方法里關(guān)聯(lián)Path |
gentLength | 獲取計(jì)算的長(zhǎng)度 |
getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo) | 獲取路徑的片段,前兩個(gè)參數(shù)表示起止點(diǎn)坐標(biāo)嚼隘,dst表示截取path輸出結(jié)果诽里,startWithMoveTo表示是否從上一次截取的終點(diǎn)處開(kāi)始截取 |
getPosTan(float distance, float[] pos, float[] tan) | 獲取某點(diǎn)坐標(biāo)及其切線坐標(biāo) |
有幾點(diǎn)需要重視一下:
forceClosed參數(shù)對(duì)綁定的Path不會(huì)產(chǎn)生任何影響,只會(huì)對(duì)PathMeasure 的測(cè)量結(jié)果有影響飞蛹。
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.e("TAG", "forceClosed=false---->"+measure1.getLength());
Log.e("TAG", "forceClosed=true----->"+measure2.getLength());
canvas.drawPath(path,mDeafultPaint);
Log如下:
25521-25521/com.blue.canvas E/TAG: forceClosed=false---->600.0
25521-25521/com.blue.canvas E/TAG: forceClosed=true----->800.0
可以看出當(dāng)forceClosed為true,在測(cè)量path長(zhǎng)度時(shí)灸眼,會(huì)自動(dòng)補(bǔ)上使其閉合卧檐,長(zhǎng)度就為閉合的長(zhǎng)度。但是forceClosed無(wú)論true還是false焰宣,都不影響Path本身的值霉囚。
另外,getPosTan獲取切線坐標(biāo)之后匕积,可以通過(guò)下面的公式計(jì)算出某點(diǎn)的切線角度:
(Math.atan2(mTan[1], mTan[0]) * 180 / Math.PI);
2. Demo
利用PathMeasure實(shí)現(xiàn)一個(gè)Windows樣式的加載動(dòng)畫(huà)盈罐“竦可以看出動(dòng)畫(huà)是前半部分是完整的半圓曲線,后半部分曲線的末尾加速向曲線頭部靠攏盅粪。用到了PathMeasure的getSegment方法截取一部分運(yùn)動(dòng)軌跡的操作钓葫。下面來(lái)看具體實(shí)現(xiàn)。
先初始化相關(guān)變量和操作:
// 截取path的輸出
private Path mDst;
private Paint mPaint;
// 用于繪圖的原始Path
private Path mPath;
// 獲取Path的長(zhǎng)度
private float mLength;
private float mAnimValue;
// 測(cè)量的工具類
private PathMeasure mPathMeasure;
在構(gòu)造器中初始化操作:
public PathTracingView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setStrokeWidth(5);
mPaint.setStyle(Paint.Style.STROKE);
mPath = new Path();
mDst = new Path();
// 劃一個(gè)圓
mPath.addCircle(400, 400, 100, Path.Direction.CW);
mPathMeasure = new PathMeasure();
// 關(guān)聯(lián)Path票顾,由于畫(huà)出的圓已經(jīng)是閉合的了础浮,所以true和false都無(wú)關(guān)緊要了
mPathMeasure.setPath(mPath, true);
// 獲取路徑的長(zhǎng)度
mLength = mPathMeasure.getLength();
// 從0到100%變化
ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1);
valueAnimator.setDuration(1000);
valueAnimator.setInterpolator(new LinearInterpolator());
valueAnimator.setRepeatCount(ValueAnimator.INFINITE);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
mAnimValue = (float) valueAnimator.getAnimatedValue();
invalidate();
}
});
valueAnimator.start();
}
在onDraw方法繪制動(dòng)畫(huà):
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mDst.reset();
// 避免Android上硬件加速的bug,在調(diào)用getSegment方法時(shí)奠骄,對(duì)mDst進(jìn)行l(wèi)ineTo操作
mDst.lineTo(0, 0);
// 終點(diǎn)坐標(biāo)從0到100%變化
float stop = mLength * mAnimValue;
// 在前半段start為0豆同,后半段快速向stop靠攏
float start = (float) (stop - ((0.5 - Math.abs(mAnimValue - 0.5)) * mLength));
// 獲取截取片段
mPathMeasure.getSegment(start, stop, mDst, true);
canvas.drawPath(mDst, mPaint);
}
運(yùn)行之后就能出現(xiàn)上面的動(dòng)畫(huà)效果。
3. 進(jìn)階
3.1 Dash樣式
對(duì)于Android繪制動(dòng)畫(huà)的畫(huà)筆來(lái)說(shuō)含鳞,有如上幾種表現(xiàn)形式影锈,其中Dash表示實(shí)線、虛線的結(jié)合蝉绷。通過(guò)畫(huà)筆的Dash樣式精居,也可以用來(lái)實(shí)現(xiàn)路徑的變換動(dòng)畫(huà)——將虛線/實(shí)線填充整個(gè)路徑,然后改變偏移量的值潜必,讓實(shí)線/虛線不斷地填充靴姿,以達(dá)到實(shí)線虛線相互交替。
下面來(lái)看如何實(shí)現(xiàn)磁滚。
創(chuàng)建自定義View佛吓,然后實(shí)現(xiàn)構(gòu)造方法:
public PathPaintView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setStrokeWidth(5);
mPaint.setStyle(Paint.Style.STROKE);
mPath = new Path();
// 繪制一個(gè)三角形
mPath.moveTo(100, 100);
mPath.lineTo(100, 500);
mPath.lineTo(400, 300);
mPath.close();
mPathMeasure = new PathMeasure();
mPathMeasure.setPath(mPath, true);
// 取出具體長(zhǎng)度
mLength = mPathMeasure.getLength();
ValueAnimator valueAnimator = ValueAnimator.ofFloat(1, 0);
valueAnimator.setDuration(2000);
valueAnimator.setInterpolator(new LinearInterpolator());
valueAnimator.setRepeatCount(ValueAnimator.INFINITE);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
mAnimValue = (float) valueAnimator.getAnimatedValue();
// 設(shè)置畫(huà)筆風(fēng)格樣式Dash
// 將實(shí)線和虛線都設(shè)置為整個(gè)路徑的長(zhǎng)度,第二個(gè)參數(shù)是偏移量垂攘,從0到100%
// 這樣實(shí)線或者虛線會(huì)一點(diǎn)一點(diǎn)地?cái)D開(kāi)
mPathEffect = new DashPathEffect(new float[]{mLength, mLength}, mLength * mAnimValue);
mPaint.setPathEffect(mPathEffect);
invalidate();
}
});
valueAnimator.start();
}
對(duì)于這種方式實(shí)現(xiàn)動(dòng)畫(huà)维雇,就不用截取路徑了,在onDraw中直接繪制即可:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawPath(mPath, mPaint);
}
實(shí)現(xiàn)效果如下圖:
3.2 getPosTan
對(duì)于高級(jí)動(dòng)畫(huà)效果來(lái)說(shuō)晒他,對(duì)于運(yùn)動(dòng)軌跡上的點(diǎn)的控制是必要的吱型,因?yàn)榭梢愿鶕?jù)點(diǎn)的坐標(biāo),做一些動(dòng)態(tài)地改變陨仅。
PathMeasure.getPosTan方法就是獲取運(yùn)動(dòng)軌跡點(diǎn)的坐標(biāo)和切線方向的津滞。下面來(lái)使用這個(gè)API。
創(chuàng)建一個(gè)自定義View灼伤,初始化操作:
private Path mPath;
// 存放取出點(diǎn)的具體坐標(biāo)
private float[] mPos;
// 當(dāng)前曲線的運(yùn)動(dòng)趨勢(shì)即橫縱坐標(biāo)
private float[] mTan;
private Paint mPaint;
private PathMeasure mPathMeasure;
private ValueAnimator mValueAnim;
private float mCurrentValue;
public PathPosTanView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
mPath = new Path();
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(5);
// 繪制一個(gè)圓形
mPath.addCircle(0, 0, 200, Path.Direction.CW);
mPathMeasure = new PathMeasure();
mPathMeasure.setPath(mPath, false);
// 初始化數(shù)組触徐,橫縱坐標(biāo)一共兩個(gè)
mPos = new float[2];
mTan = new float[2];
setOnClickListener(this);
mValueAnim = ValueAnimator.ofFloat(0, 1);
mValueAnim.setDuration(3000);
mValueAnim.setInterpolator(new LinearInterpolator());
mValueAnim.setRepeatCount(ValueAnimator.INFINITE);
mValueAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
mCurrentValue = (float) valueAnimator.getAnimatedValue();
invalidate();
}
});
}
接下來(lái)在onDraw上繪制,圓形運(yùn)動(dòng)軌跡狐赡、軌跡上運(yùn)動(dòng)的小圓形和運(yùn)動(dòng)時(shí)切線的方向:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 第一個(gè)參數(shù)是運(yùn)動(dòng)的軌跡長(zhǎng)度撞鹉,后面兩個(gè)參數(shù)接收獲取的值
mPathMeasure.getPosTan(mCurrentValue * mPathMeasure.getLength(), mPos, mTan);
// 獲取路徑上點(diǎn)的切線角度
float degree = (float) (Math.atan2(mTan[1], mTan[0]) * 180 / Math.PI);
// 將畫(huà)布鎖定
canvas.save();
// 移動(dòng)畫(huà)布
canvas.translate(400, 400);
// 繪制路線
canvas.drawPath(mPath, mPaint);
// 繪制在運(yùn)動(dòng)軌跡上的圓
canvas.drawCircle(mPos[0], mPos[1], 10, mPaint);
// 旋轉(zhuǎn)畫(huà)布角度
canvas.rotate(degree);
// 繪制切線
canvas.drawLine(0, -200, 300, -200, mPaint);
// 畫(huà)布釋放
canvas.restore();
}
需要注意的一點(diǎn)是,沒(méi)有必要為了每個(gè)點(diǎn)繪制對(duì)應(yīng)的切線,這樣會(huì)十分麻煩鸟雏,因?yàn)槔L制線條需要坐標(biāo)參數(shù)享郊。上面的做法是只繪制切線的初始位置,然后根據(jù)切線的角度移動(dòng)畫(huà)布孝鹊,在效果上使得切線也隨之轉(zhuǎn)動(dòng)炊琉,避免了重復(fù)繪制切線的操作。
運(yùn)行效果如下圖: