前言
像我這樣迷茫的人,像我這樣尋找的人,像我這樣碌碌無為的人,你還見過多少人 ...
詮釋了我的內心真實想法
PathMeasure之直播間送愛心
Path(路徑),在繪制自定義控件中占著舉足輕重的地位。稍微復雜點的控件都會有著它的身影。是不是有時候還在為求控件某點的坐標而犯愁呢?反正當時我還在傻傻的計算控件路徑的公式去求坐標。那么,如何來定位任意一個給定Path的任意一個點的坐標呢?
Android SDK提供了一個非常有用的API來幫助開發(fā)者解決以上的難題,這個類就是 PathMeasure ,它的中文意思路徑測量,英文學得不好,只能翻譯這個淺顯的含義。
先來揪一揪最終實現的效果圖:
簡單控制了愛心的縮放,速率以及透明度。源碼也非常的簡單易懂,若有什么疑問請留言。
文章結尾會附上源碼,如果對你有所幫助,還望動手點一點star
那下面重點來看一看 PathMeasure
PathMeasure的那些事
首先來看下 PathMeasure 的 API:
PathMeasure 的 API 非常簡單,基本都是望文生義,紅框圈住是比較常用的方法。接著我們挨著來揪一揪各個方法的用法以及含義。
初始化
PathMeasure 的初始化可以直接 new 一個 PathMeasure
pathMeasure = new PathMeasure();
初始化 PathMeasure 后,可以通過以下的方式將 Path 和 PathMeasure 進行綁定:
pathMeasure.setPath(path, false);
當然還可以將上面兩步結合在一起完成有參的構造方法來進行初始化:
PathMeasure(Path path, boolean forceClosed)
參數 path 就是需要計算,測量的路徑;那么 forceClosed 又代表什么含義呢,字面上強制關閉,接下來通過一個案例來加深對它的理解。
forceClosed參數
先看下面一段代碼,繪制了兩條線段,分別改變 forceClosed 的值來獲取 PathMeasure.getLength() 的值:
mPathMeasure = new PathMeasure();
mPath = new Path();
//水平繪制長為600的線段
mPath.moveTo(800, 200);
mPath.lineTo(200, 200);
//繪制長為800的字段
mPath.lineTo(200, 1000);
mPathMeasure.setPath(mPath, false);
forceClosed 為 false,true 的效果圖如下:
可以得出以下的結論,forceClosed 為 false 為兩條線段的長度(600+800),forceClosed 為 ture 為路徑閉合的總長度(600+800+1000)。簡單的說,forceClosed 就是 Path 最終是否需要閉合,如果為 ture 的話,則不管關聯的 Path 是否是閉合的,計算的時候都會按閉合來計算。
但是這個參數對 Path 和 PathMeasure 的影響是需要解釋下的:
forceClosed 參數對綁定的 Path 不會產生任何影響,例如一個折線段的 Path,本身是沒有閉合的,forceClosed 設置為 ture 的時候, PathMeasure 計算的 Path 是閉合的,但 Path 本身繪制出來是不會閉合的。
forceClosed 參數對 PathMeasure 的測量結果有影響,還是例如前面說的一個折線段的 Path,本身沒有閉合,forceClosed 設置為 ture , PathMeasure 的計算就會包含最后一段閉合的路徑,與原來的 Path 不同。
還有一點需要注意一下,如果你繪制的路徑是直線路徑,則設置 forceClosed 失效。
getLength
PathMeasure.getLength() 的使用非常廣泛,其作用就是獲取計算的路徑長度。
getSegment
getSegment 用于獲取 Path 的一個片段,方法如下:
getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo)
| 參數 | 作用 | 備注 |
|---|---|---|
| 返回值(boolean) | 判斷截取是否成功 | true 表示截取成功,結果存入dst中,false 截取失敗,不會改變dst中內容 |
| startD | 開始截取位置距離 Path 起點的長度 | 取值范圍: 0~getLength |
| stopD | 結束截取位置距離 Path 起點的長度 | 取值范圍: 0~getLength |
| dst | 截取的 Path 將會添加到 dst 中 | 注意: 是添加,而不是替換 |
| startWithMoveTo | 起始點是否使用 moveTo | 用于保證截取的 Path 第一個點位置不變 |
-
4.4或者之前的版本,在默認開啟硬件加速的情況下,更改 dst 的內容后可能繪制會出現問題,請關閉硬件加速或者給 dst 添加一個單個操作,例如:
dst.lineTo(0, 0)
通過以下案例來加深對 getSegment 的理解,效果圖如下:
其原理就是通過 getSegment 來不斷截取 Path 片段,從而不斷繪制完整的路徑,代碼如下:
public class CircleView extends View {
Paint mPaint;
Path mPath;
Path mDstPath;
PathMeasure mPathMeasure;
float mPathLength;
float mAnimatedValue;
public CircleView(Context context) {
this(context, null);
}
public CircleView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public CircleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(2);
mPaint.setColor(Color.RED);
mPathMeasure = new PathMeasure();
mPath = new Path();
mDstPath = new Path();
//圓弧路徑
mPath.addArc(new RectF(200, 200, 600, 600), 0, 359.6f);
//繪制五角星
for (int i = 1; i < 6; i++) {
Point p = getPoint(200, -144 * i);
mPath.lineTo(400 + p.x, 400 + p.y);
}
mPath.close();
mPathMeasure.setPath(mPath, false);
mPathLength = mPathMeasure.getLength();
ValueAnimator animator = ValueAnimator.ofFloat(0f, 1.0f);
animator.setInterpolator(new AccelerateDecelerateInterpolator());
animator.setDuration(3000);
animator.setRepeatCount(-1);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
mAnimatedValue = (float) valueAnimator.getAnimatedValue();
invalidate();
}
});
animator.start();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mDstPath.reset();
// 硬件加速的BUG
mDstPath.lineTo(0, 0);
//獲取到路徑片段
mPathMeasure.getSegment(0, mAnimatedValue * mPathLength, mDstPath, true); //注意startWithMoveTo一般使用true,如果使用false所有的軌跡會連接上原點。
canvas.drawPath(mDstPath, mPaint);
}
private Point getPoint(float radius, float angle) {
float x = (float) ((radius) * Math.cos(angle * Math.PI / 180f));
float y = (float) ((radius) * Math.sin(angle * Math.PI / 180f));
Point p = new Point(x, y);
return p;
}
private class Point {
private float x;
private float y;
private Point(float x, float y) {
this.x = x;
this.y = y;
}
}
}
代碼中有相應的注釋加以說明
nextContour
nextContour() 方法用的比較少,比較大部分情況下都只會有一個 Path 而不是多個,畢竟這樣會增加 Path 的復雜度,但是如果真有一個 Path,包含了多個 Path,那么通過 nextContour 這個方法,就可以進行切換,同時,默認的 API,例如 getLength,獲取的也是當前的這段 Path 所對應的長度,而不是所有的 Path 的長度,同時,nextContou r獲取 Path 的順序,與 Path 的添加順序是相同的,這里就不再舉例說明了。
getPosTan
getPosTan(float distance, float[] pos, float[] tan)
這個 API 非常強大,直播愛心的實現就用到了該方法。意思就是獲取路徑上某點的坐標及其切線的坐標
各個參數的含義如下表:
| 參數 | 作用 | 備注 |
|---|---|---|
| 返回值(boolean) | 判斷截取是否成功 | 數據會存入 pos 和 tan 中,false 表示失敗,pos 和 tan 不會改變 |
| distance | 距離 Path 起點的長度 | 取值范圍: 0~getLength |
| pos | 保存該點的坐標值 | 坐標值: (x, y) |
| tan | 保存該點的正切值 | 正切值: (x, y) |
接著來看以下案例,效果圖如下:
通常我們按照以下的方式來轉換切線的角度:
float degrees = (float) (Math.atan2(tan[1], tan[0]) * 180.0 / Math.PI);
以下是模擬圓上箭頭圍繞圓的運動趨勢代碼:
public class ArrowView extends View {
Paint mPaint;
Path mPath;
PathMeasure mPathMeasure;
float mPathLength;
float mAnimatedValue;
float[] pos = new float[2];
float[] tan = new float[2];
public ArrowView(Context context) {
this(context, null);
}
public ArrowView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public ArrowView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(6);
mPaint.setColor(Color.RED);
mPathMeasure = new PathMeasure();
mPath = new Path();
mPath.addCircle(0, 0, 200, Path.Direction.CW);
mPathMeasure.setPath(mPath, false);
mPathLength = mPathMeasure.getLength();
ValueAnimator animator = ValueAnimator.ofFloat(0f, 1.0f);
animator.setInterpolator(new AccelerateDecelerateInterpolator());
animator.setDuration(3000);
animator.setRepeatCount(-1);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
mAnimatedValue = (float) valueAnimator.getAnimatedValue();
invalidate();
}
});
animator.start();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mPathMeasure.getPosTan(mAnimatedValue * mPathLength, pos, tan);
float degrees = (float) (Math.atan2(tan[1], tan[0]) * 180.0 / Math.PI);
canvas.save();
canvas.translate(400, 400);
mPaint.setColor(Color.RED);
canvas.drawPath(mPath, mPaint);
//繪制箭頭
Path path = new Path(); //不建議在onDraw中直接new一個對象
path.moveTo(40 * (float) Math.cos(-30 * Math.PI / 180f), 200 + 40 * (float) Math.sin(-30 * Math.PI / 180f));
path.lineTo(0, 200);
path.lineTo(40 * (float) Math.cos(30 * Math.PI / 180f), 200 + 40 * (float) Math.sin(30 * Math.PI / 180f));
canvas.rotate(degrees);
mPaint.setColor(Color.GREEN);
canvas.drawPath(path, mPaint);
canvas.restore();
}
}
接著我們來看一看后一個 API
getMatrix
getMatrix 方法用于獲取路徑上某一長度的位置以及位置的正切值的矩陣,方法體如下:
getMatrix(float distance, Matrix matrix, int flags)
| 參數 | 作用 | 備注 |
|---|---|---|
| 返回值(boolean) | 判斷獲取是否成功 true表示成功 | 數據會存入matrix中,false 失敗,matrix內容不會改變 |
| distance | 距離 Path 起點的長度 | 取值范圍: 0~getLength |
| matrix | 根據 falgs 標記轉換成matrix | 會根據 flags 的設置而存入不同的矩陣 |
| flags | 規(guī)定哪些內容會存入到matrix中 | POSITION_MATRIX_FLAG(位置) TANGENT_MATRIX_FLAG(正切) |
參數的含義:
| 參數 | 作用 | 備注 |
|---|---|---|
| 返回值(boolean) | 判斷獲取是否成功 true表示成功 | 數據會存入matrix中,false 失敗,matrix內容不會改變 |
| distance | 距離 Path 起點的長度 | 取值范圍: 0~getLength |
| matrix | 根據 falgs 標記轉換成matrix | 會根據 flags 的設置而存入不同的矩陣 |
| flags | 規(guī)定哪些內容會存入到matrix中 | POSITION_MATRIX_FLAG(位置) TANGENT_MATRIX_FLAG(正切) |
flags 選項可以選擇 位置 或者 正切 ,如果我們兩個選項都想選擇怎么辦?
可以將兩個選項之間用 | 連接起來,如下:
pathMeasure.getMatrix(distance, matrix, PathMeasure.TANGENT_MATRIX_FLAG| PathMeasure.POSITION_MATRIX_FLAG);
以下是內切圓的案例,效果圖如下:
代碼如下:
public class MatrixView extends View {
Paint mPaint;
Path mPath;
PathMeasure mPathMeasure;
Matrix mMatrix;
float mPathLength;
float mAnimatedValue;
Bitmap mBitmap;
public MatrixView(Context context) {
this(context, null);
}
public MatrixView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public MatrixView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(2);
mPaint.setColor(Color.RED);
mPathMeasure = new PathMeasure();
mPath = new Path();
mMatrix = new Matrix();
mBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.ic_matrix);
mPath.addCircle(0, 0, 200, Path.Direction.CW);
mPathMeasure.setPath(mPath, false);
mPathLength = mPathMeasure.getLength();
ValueAnimator animator = ValueAnimator.ofFloat(0f, 1.0f);
animator.setInterpolator(new AccelerateDecelerateInterpolator());
animator.setDuration(3000);
animator.setRepeatCount(-1);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
mAnimatedValue = (float) valueAnimator.getAnimatedValue();
invalidate();
}
});
animator.start();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
mMatrix.reset();
mPathMeasure.getMatrix(mAnimatedValue * mPathLength, mMatrix,
PathMeasure.POSITION_MATRIX_FLAG | PathMeasure.TANGENT_MATRIX_FLAG);
canvas.translate(400, 400);
canvas.drawBitmap(mBitmap, mMatrix, mPaint);
canvas.drawPath(mPath, mPaint);
}
}
相信 PathMeasure 的強大會讓你愛不釋手。
PathMeasure送愛心
熟悉了 PathMeasure 相關的 API ,那么理解以下的代碼就非常容易了。原理非常簡單繪制三階貝塞爾曲線根據 getPosTan 獲取曲線上坐標,最后根據坐標繪制愛心 Bitmap,具體代碼如下:
public class HeartView extends View {
private SparseArray<Bitmap> mBitmapSparseArray = new SparseArray<>();
private SparseArray<Heart> mHeartSparseArray;
private Paint mPaint;
private int mWidth;
private int mHeight;
private Matrix mMatrix;
//動畫時長
private int mDuration;
//最大速率
private int mMaxRate;
//是否控制速率
private boolean mRateEnable;
//是否控制透明度速率
private boolean mAlphaEnable;
//是否控制縮放
private boolean mScaleEnable;
//動畫時長
private final static int DURATION_TIME = 3000;
//最大速率
private final static int MAX_RATE = 2000;
public HeartView(Context context) {
this(context, null);
}
public HeartView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public HeartView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
private void init(Context context) {
mPaint = new Paint();
mPaint.setAntiAlias(true);
mHeartSparseArray = new SparseArray<>();
mMatrix = new Matrix();
mDuration = DURATION_TIME;
mMaxRate = MAX_RATE;
mRateEnable = true;
mAlphaEnable = true;
mScaleEnable = true;
initBitmap(context);
}
private void initBitmap(Context context) {
Bitmap bitmap1 = BitmapFactory.decodeResource(context.getResources(), R.mipmap.live_heart1);
Bitmap bitmap2 = BitmapFactory.decodeResource(context.getResources(), R.mipmap.live_heart2);
Bitmap bitmap3 = BitmapFactory.decodeResource(context.getResources(), R.mipmap.live_heart3);
Bitmap bitmap4 = BitmapFactory.decodeResource(context.getResources(), R.mipmap.live_heart4);
Bitmap bitmap5 = BitmapFactory.decodeResource(context.getResources(), R.mipmap.live_heart5);
Bitmap bitmap6 = BitmapFactory.decodeResource(context.getResources(), R.mipmap.live_heart6);
Bitmap bitmap7 = BitmapFactory.decodeResource(context.getResources(), R.mipmap.live_heart7);
mBitmapSparseArray.put(HeartType.BLUE, bitmap1);
mBitmapSparseArray.put(HeartType.GREEN, bitmap2);
mBitmapSparseArray.put(HeartType.YELLOW, bitmap3);
mBitmapSparseArray.put(HeartType.PINK, bitmap4);
mBitmapSparseArray.put(HeartType.BROWN, bitmap5);
mBitmapSparseArray.put(HeartType.PURPLE, bitmap6);
mBitmapSparseArray.put(HeartType.RED, bitmap7);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
//wrap_content情況-默認高度為200寬度100
int defaultWidth = (int) dp2px(100);
if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(defaultWidth, defaultWidth * 3);
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(defaultWidth, heightSpecSize);
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpecSize, defaultWidth * 3);
} else {
setMeasuredDimension(widthSpecSize, heightSpecSize);
}
}
public void addHeart(int arrayIndex) {
if (arrayIndex < 0 || arrayIndex > (mBitmapSparseArray.size() - 1)) return;
Path path = new Path();
final PathMeasure pathMeasure = new PathMeasure();
final float pathLength;
final Heart heart = new Heart();
final int bitmapIndex = arrayIndex;
//繪制三階貝塞爾曲線
PointF start = new PointF();//起點位置
PointF control1 = new PointF(); //貝塞爾控制點
PointF control2 = new PointF(); //貝塞爾控制點
PointF end = new PointF(); //貝塞爾結束點
initStartAndEnd(start, end);
initControl(control1, control2);
path.moveTo(start.x, start.y);
path.cubicTo(control1.x, control1.y, control2.x, control2.y, end.x, end.y);
pathMeasure.setPath(path, false);
pathLength = pathMeasure.getLength();
ValueAnimator animator = ValueAnimator.ofFloat(0f, 1.0f);
//先加速后減速
animator.setInterpolator(new AccelerateDecelerateInterpolator());
//動畫的長短來控制速率
animator.setDuration(mDuration + (mRateEnable ? (int) (Math.random() * mMaxRate) : 0));
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
float fraction = valueAnimator.getAnimatedFraction();
float[] pos = new float[2];
pathMeasure.getPosTan(fraction * pathLength, pos, new float[2]);
heart.setX(pos[0]);
heart.setY(pos[1]);
heart.setProgress(fraction);
invalidate();
}
});
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
mHeartSparseArray.remove(heart.hashCode());
}
@Override
public void onAnimationStart(Animator animation) {
super.onAnimationStart(animation);
mHeartSparseArray.put(heart.hashCode(), heart);
heart.setIndex(bitmapIndex);
}
});
animator.start();
}
public void addHeart() {
addHeart(new Random().nextInt(mBitmapSparseArray.size() - 1));
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mHeartSparseArray == null || mHeartSparseArray.size() == 0) return;
canvasHeart(canvas);
}
private void canvasHeart(Canvas canvas) {
for (int i = 0; i < mHeartSparseArray.size(); i++) {
Heart heart = mHeartSparseArray.valueAt(i);
//設置畫筆透明度
mPaint.setAlpha(mAlphaEnable ? (int) (255 * (1.0f - heart.getProgress())) : 255);
//會覆蓋掉之前的x,y數值
mMatrix.setTranslate(0, 0);
//位移到x,y
mMatrix.postTranslate(heart.getX(), heart.getY());
mMatrix.postScale(dealToScale(heart.getProgress()), dealToScale(heart.getProgress()),
mWidth / 2, mHeight);
if (heart != null) {
canvas.drawBitmap(mBitmapSparseArray.get(heart.getIndex()), mMatrix, mPaint);
}
}
}
private float dealToScale(float fraction) {
if (fraction < 0.1f && mScaleEnable) {
return 0.5f + fraction / 0.1f * 0.5f;
}
return 1.0f;
}
public void initControl(PointF control1, PointF control2) {
control1.x = (float) (Math.random() * mWidth);
control1.y = (float) (Math.random() * mHeight);
control2.x = (float) (Math.random() * mWidth);
control2.y = (float) (Math.random() * mHeight);
if (control1.x == control2.x && control1.y == control2.y) {
initControl(control1, control2);
}
}
public void initStartAndEnd(PointF start, PointF end) {
start.x = mWidth / 2;
start.y = mHeight;
end.x = mWidth / 2;
end.y = 0;
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mWidth = w;
mHeight = h;
}
@Override
protected void onDetachedFromWindow() {
cancel();
super.onDetachedFromWindow();
}
/**
* 取消已有動畫,釋放資源
*/
public void cancel() {
//回收bitmap
for (int i = 0; i < mBitmapSparseArray.size(); i++) {
if (mBitmapSparseArray.valueAt(i) != null) {
mBitmapSparseArray.valueAt(i).recycle();
}
}
}
public int getDuration() {
return mDuration;
}
public void setDuration(int duration) {
mDuration = duration;
}
public int getMaxRate() {
return mMaxRate;
}
public void setMaxRate(int maxRate) {
mMaxRate = maxRate;
}
public boolean getRateEnable() {
return mRateEnable;
}
public void setRateEnable(boolean rateEnable) {
mRateEnable = rateEnable;
}
public boolean getAlphaEnable() {
return mAlphaEnable;
}
public void setAlphaEnable(boolean alphaEnable) {
mAlphaEnable = alphaEnable;
}
public boolean getScaleEnable() {
return mScaleEnable;
}
public void setScaleEnable(boolean scaleEnable) {
mScaleEnable = scaleEnable;
}
private float dp2px(float dp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics());
}
}
源代碼
源碼已上傳到Github