前言
在Path在UI體系當中不論是在自定義View還是動畫,都占有舉足輕重的地位。繪制Path,可以通過Android提供的API,或者是貝塞爾曲線、數學函數、圖形組合等等方式,而要獲取Path上每一個構成點的坐標,一般需要知道Path的函數方法,例如求解貝塞爾曲線上的點的De Casteljau算法,但對于一般的Path來說,是很難通過簡單的函數方法來進行計算的,那么,今天需要了解的就是PathMeasure,關于Path測量的運用
PathMeasure
今天需要了解的API非常簡單,關于Path的測量,我們首先來看一些效果
這種load效果我們經常在項目當中遇見,那么其中有一部分效果是通過測量Path來進行實現的
那么首先我們來看到PathMeasure這個類,那么具體API詳細介紹我就列入到下面,今天最主要的核心是,掌握這個類的使用技巧,而不是死板的API,那么我們來首先先看下這個類當中的API
公共方法
返回值 方法名
void setPath(Path path, boolean forceClosed) 關聯一個Path
boolean isClosed() 是否閉合
float getLength() 獲取Path的長度
boolean nextContour() 跳轉到下一個輪廓
boolean getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo) 截取片段
boolean getPosTan(float distance, float[] pos, float[] tan) 獲取指定長度的位置坐標及該點切線值
boolean getMatrix(float distance, Matrix matrix, int flags) 獲取指定長度的位置坐標 及該點Matrix
源碼
public class PathMeasure {
private Path mPath;
/**
* Create an empty PathMeasure object. To uses this to measure the length
* of a path, and/or to find the position and tangent along it, call
* setPath.
* 創建一個空的PathMeasure
*用這個構造函數可創建一個空的 PathMeasure,
* 但是使用之前需要先調用 setPath 方法來與 Path 進行關聯。
* 被關聯的 Path 必須是已經創建好的,
* 如果關聯之后 Path 內容進行了更改,
* 則需要使用 setPath 方法重新關聯。
* Note that once a path is associated with the measure object, it is
* undefined if the path is subsequently modified and the the measure object
* is used. If the path is modified, you must call setPath with the path.
*/
public PathMeasure() {
mPath = null;
native_instance = native_create(0, false);
}
/**
* Create a PathMeasure object associated with the specified path object
* (already created and specified). The measure object can now return the
* path's length, and the position and tangent of any position along the
* path.
*
* Note that once a path is associated with the measure object, it is
* undefined if the path is subsequently modified and the the measure object
* is used. If the path is modified, you must call setPath with the path.
* 創建 PathMeasure 并關聯一個指定的Path(Path需要已經創建完成)。
* 用這個構造函數是創建一個 PathMeasure 并關聯一個 Path,
* 其實和創建一個空的 PathMeasure 后調用 setPath 進行關聯效果是一樣的,
* 同樣,被關聯的 Path 也必須是已經創建好的,如果關聯之后 Path 內容進行了更改,
* 則需要使用 setPath 方法重新關聯。
*該方法有兩個參數,第一個參數自然就是被關聯的 Path 了,
* 第二個參數是用來確保 Path 閉合,如果設置為 true,
* 則不論之前Path是否閉合,都會自動閉合該 Path(如果Path可以閉合的話)。
* 在這里有兩點需要明確:
* 1.不論 forceClosed 設置為何種狀態(true 或者 false),
* 都不會影響原有Path的狀態,即 Path 與 PathMeasure 關聯之后,之前的的 Path 不會有任何改變。
* 2.forceClosed 的設置狀態可能會影響測量結果,
* 如果 Path 未閉合但在與 PathMeasure 關聯的時候設置 forceClosed 為 true 時,
* 測量結果可能會比 Path 實際長度稍長一點,獲取到到是該 Path 閉合時的狀態。
* @param path The path that will be measured by this object 被關聯的Path
* @param forceClosed If true, then the path will be considered as "closed"
* even if its contour was not explicitly closed.
*/
public PathMeasure(Path path, boolean forceClosed) {
// The native implementation does not copy the path, prevent it from being GC'd
mPath = path;
native_instance = native_create(path != null ? path.readOnlyNI() : 0,
forceClosed);
}
/**
* Assign a new path, or null to have none.
* 關聯一個Path
*/
public void setPath(Path path, boolean forceClosed) {
mPath = path;
native_setPath(native_instance,
path != null ? path.readOnlyNI() : 0,
forceClosed);
}
/**
* Return the total length of the current contour, or 0 if no path is
* associated with this measure object.
* 返回當前輪廓的總長度,或者如果沒有路徑,則返回0。與此度量對象相關聯。
*/
public float getLength() {
return native_getLength(native_instance);
}
/**
* Pins distance to 0 <= distance <= getLength(), and then computes the
* corresponding position and tangent. Returns false if there is no path,
* or a zero-length path was specified, in which case position and tangent
* are unchanged.
* 獲取指定長度的位置坐標及該點切線值
* @param distance The distance along the current contour to sample 位置
* @param pos If not null, returns the sampled position (x==[0], y==[1]) 坐標值
* @param tan If not null, returns the sampled tangent (x==[0], y==[1]) 切線值
* @return false if there was no path associated with this measure object
*/
public boolean getPosTan(float distance, float pos[], float tan[]) {
if (pos != null && pos.length < 2 ||
tan != null && tan.length < 2) {
throw new ArrayIndexOutOfBoundsException();
}
return native_getPosTan(native_instance, distance, pos, tan);
}
public static final int POSITION_MATRIX_FLAG = 0x01; // must match flags in SkPathMeasure.h
public static final int TANGENT_MATRIX_FLAG = 0x02; // must match flags in SkPathMeasure.h
/**
* Pins distance to 0 <= distance <= getLength(), and then computes the
* corresponding matrix. Returns false if there is no path, or a zero-length
* path was specified, in which case matrix is unchanged.
*
* @param distance The distance along the associated path
* @param matrix Allocated by the caller, this is set to the transformation
* associated with the position and tangent at the specified distance
* @param flags Specified what aspects should be returned in the matrix.
*/
public boolean getMatrix(float distance, Matrix matrix, int flags) {
return native_getMatrix(native_instance, distance, matrix.native_instance, flags);
}
/**
* Given a start and stop distance, return in dst the intervening
* segment(s). If the segment is zero-length, return false, else return
* true. startD and stopD are pinned to legal values (0..getLength()).
* If startD >= stopD then return false (and leave dst untouched).
* Begin the segment with a moveTo if startWithMoveTo is true.
*
* <p>On {@link android.os.Build.VERSION_CODES#KITKAT} and earlier
* releases, the resulting path may not display on a hardware-accelerated
* Canvas. A simple workaround is to add a single operation to this path,
* such as <code>dst.rLineTo(0, 0)</code>.</p>
* 給定啟動和停止距離,
* 在DST中返回中間段。
* 如果該段為零長度,則返回false,
* 否則返回true。
* StestD和Stutd被固定到合法值(0…GigLangTh())。
* startD>=stopD,則返回false(并保持DST未被觸碰)。
* 如果有一個假設是正確的,就開始以一個模式開始。
*
* 早期版本,結果路徑可能不會在硬件加速中顯示。
* Canvas。
* 一個簡單的解決方法是在這個路徑中添加一個操作,
* 這樣的SDST. RLIN to(0, 0)
*/
public boolean getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo) {
// Skia used to enforce this as part of it's API, but has since relaxed that restriction
// so to maintain consistency in our API we enforce the preconditions here.
float length = getLength();
if (startD < 0) {
startD = 0;
}
if (stopD > length) {
stopD = length;
}
if (startD >= stopD) {
return false;
}
return native_getSegment(native_instance, startD, stopD, dst.mutateNI(), startWithMoveTo);
}
/**
* Return true if the current contour is closed()
* 是否閉合
*/
public boolean isClosed() {
return native_isClosed(native_instance);
}
/**
* Move to the next contour in the path. Return true if one exists, or
* false if we're done with the path.
*/
public boolean nextContour() {
return native_nextContour(native_instance);
}
protected void finalize() throws Throwable {
native_destroy(native_instance);
native_instance = 0; // Other finalizers can still call us.
}
private static native long native_create(long native_path, boolean forceClosed);
private static native void native_setPath(long native_instance, long native_path, boolean forceClosed);
private static native float native_getLength(long native_instance);
private static native boolean native_getPosTan(long native_instance, float distance, float pos[], float tan[]);
private static native boolean native_getMatrix(long native_instance, float distance, long native_matrix, int flags);
private static native boolean native_getSegment(long native_instance, float startD, float stopD, long native_path, boolean startWithMoveTo);
private static native boolean native_isClosed(long native_instance);
private static native boolean native_nextContour(long native_instance);
private static native void native_destroy(long native_instance);
/* package */private long native_instance;
}
從源碼上分析我們可以看得到其實這個類就是為了讓我們測量到當前Path所在的位置
API不多,那么到底怎么運用呢?
首先我們來分析這個效果
很明顯我們看到當前這里是一個圓,運用了一張圖片,讓這張圖能夠沿著當前的這個圓進行移動
那么,這個圓形是我們用Path所繪制的,那么當前Path會記錄下當前圓的所有點,而我們需要將那個箭頭圖片繪制到我們path的點上面,并且按照圓形角度來進行操控而圖形是這樣的
那么這個時候我們能夠反映過來,去得到當前圖片進行旋轉,能夠做到這一點, 但是我們如何判斷這旋轉的角度?
而測量當中提供了
/**
* Pins distance to 0 <= distance <= getLength(), and then computes the
* corresponding position and tangent. Returns false if there is no path,
* or a zero-length path was specified, in which case position and tangent
* are unchanged.
* 獲取指定長度的位置坐標及該點切線值
* @param distance The distance along the current contour to sample
PATH起點的長度取值范圍: 0 <= distance <= getLength
* @param pos If not null, returns the sampled position (x==[0], y==[1]) 坐標值
* @param tan If not null, returns the sampled tangent (x==[0], y==[1]) 切線值
* @return false if there was no path associated with this measure object
*/
public boolean getPosTan(float distance, float pos[], float tan[]) {
if (pos != null && pos.length < 2 ||
tan != null && tan.length < 2) {
throw new ArrayIndexOutOfBoundsException();
}
return native_getPosTan(native_instance, distance, pos, tan);
}
那么此時看到這個getPosTan方法其實我們就能夠很明顯了解到,通過這個方法我們可以根據path的長度值,去取得指定長度所在的XY和切線XY,見下圖
那么此時能夠看到所謂的切線,
下面掃盲,段位高跳過
幾何上,切線指的是一條剛好觸碰到曲線上某一點的直線。更準確地說,當切線經過曲線上的某點(即切點)時,切線的方向與曲線上該點的方向是相同的。平面幾何中,將和圓只有一個公共交點的直線叫做圓的切線
正切函數是直角三角形中,對邊與鄰邊的比值叫做正切。放在直角坐標系中(如圖)即 tanθ=y/x
而tan就是我們的正切值
如上圖,參考上圖
隨機選取了一個橙點(具體位置),那么切線是和橙點相交的這條線,切線角度為垂直關系,所以如下圖
實在不理解TAN的話,你們就理解為當前得到了圓心坐標,因為圓的切線是圓心《建議去復習下初中數學》
那么此時,我們拿到的getPosTan方法,能夠把當前這個點,和這個點的正切值拿到,我們可以通過反正切計算取得角度,那么橙線和X軸的夾角其實實際上應該是我們到時候顯示過去的角度,那么此時,看下圖
紅線所繪制的角度是我們當前角度,綠線繪制的是需要旋轉的角度, 那么我們現在手里擁有的資源是,當前正切值,通過正切值我們運用
公式可以計算得到當前角度
Math.tan2(tan[1], tan[0]) * 180 / PI
而反切角度的話是
Math.atan2(tan[1], tan[0]) * 180 / PI
這個就是我們的要移動的角度
那么我們當前上面這個案例就能完成
public class MyView1 extends View {
private float currentValue = 0; // 用于紀錄當前的位置,取值范圍[0,1]映射Path的整個長度
private float[] pos; // 當前點的實際位置
private float[] tan; // 當前點的tangent值,用于計算圖片所需旋轉的角度
private Bitmap mBitmap; // 箭頭圖片
private Matrix mMatrix; // 矩陣,用于對圖片進行一些操作
private Paint mDeafultPaint;
private int mViewWidth;
private int mViewHeight;
private Paint mPaint;
public MyView1(Context context) {
super(context);
init(context);
}
private void init(Context context) {
pos = new float[2];
tan = new float[2];
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 8; // 縮放圖片
mBitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.arrow, options);
mMatrix = new Matrix();
mDeafultPaint = new Paint();
mDeafultPaint.setColor(Color.RED);
mDeafultPaint.setStrokeWidth(5);
mDeafultPaint.setStyle(Paint.Style.STROKE);
mPaint = new Paint();
mPaint.setColor(Color.DKGRAY);
mPaint.setStrokeWidth(2);
mPaint.setStyle(Paint.Style.STROKE);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mViewWidth = w;
mViewHeight = h;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(Color.WHITE);
// 平移坐標系
canvas.translate(mViewWidth/2,mViewHeight/2);
// 畫坐標線
canvas.drawLine(-canvas.getWidth(),0,canvas.getWidth(),0,mPaint);
canvas.drawLine(0,-canvas.getHeight(),0,canvas.getHeight(),mPaint);
Path path = new Path(); // 創建 Path
path.addCircle(0, 0, 200, Path.Direction.CW); // 添加一個圓形
Log.i("barry","----------------------pos[0] = " + pos[0] + "pos[1] = " +pos[1]);
Log.i("barry","----------------------tan[0] = " + tan[0] + "tan[1] = " +tan[1]);
PathMeasure measure = new PathMeasure(path, false); // 創建 PathMeasure
currentValue += 0.005; // 計算當前的位置在總長度上的比例[0,1]
if (currentValue >= 1) {
currentValue = 0;
}
// 方案一
// 獲取當前位置的坐標以及趨勢
measure.getPosTan(measure.getLength() * currentValue, pos, tan);
canvas.drawCircle(tan[0],tan[1],20,mDeafultPaint);
// 重置Matrix
mMatrix.reset();
// 計算圖片旋轉角度
float degrees = (float) (Math.atan2(tan[1], tan[0]) * 180.0 / Math.PI);
// 旋轉圖片
mMatrix.postRotate(degrees, mBitmap.getWidth() / 2, mBitmap.getHeight() / 2);
// 將圖片繪制中心調整到與當前點重合
mMatrix.postTranslate(pos[0] - mBitmap.getWidth() / 2, pos[1] - mBitmap.getHeight() / 2);
// 方案二
// 獲取當前位置的坐標以及趨勢的矩陣
//measure.getMatrix(measure.getLength() * currentValue, mMatrix,
//PathMeasure.TANGENT_MATRIX_FLAG | PathMeasure.POSITION_MATRIX_FLAG);
// 將圖片繪制中心調整到與當前點重合(注意:此處是前乘pre)
//mMatrix.preTranslate(-mBitmap.getWidth() / 2, -mBitmap.getHeight() / 2);
canvas.drawPath(path, mDeafultPaint);
canvas.drawBitmap(mBitmap, mMatrix, mDeafultPaint);
invalidate();
}
}
那么其他API大家可以參考網上的代碼,以及文檔資料,時間上來不及,這幾天生病休息,就先發給大家進行預習,事后如果有時間在進行補全
鏈接:https://pan.baidu.com/s/19PzRMTugbQEKFVrRKM_eWQ 密碼:3p5c
預習其他資料上傳網盤,可以參考