高級UI---LSN-8-1-Path測量

前言
在Path在UI體系當中不論是在自定義View還是動畫,都占有舉足輕重的地位。繪制Path,可以通過Android提供的API,或者是貝塞爾曲線、數學函數、圖形組合等等方式,而要獲取Path上每一個構成點的坐標,一般需要知道Path的函數方法,例如求解貝塞爾曲線上的點的De Casteljau算法,但對于一般的Path來說,是很難通過簡單的函數方法來進行計算的,那么,今天需要了解的就是PathMeasure,關于Path測量的運用

PathMeasure

今天需要了解的API非常簡單,關于Path的測量,我們首先來看一些效果


[圖片上傳中...(005Xtdi2jw1f4fp2myqo4g308c05k75k.gif-ee6867-1530211885892-0)]
005Xtdi2jw1f4fp2myqo4g308c05k75k.gif

這種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不多,那么到底怎么運用呢?
首先我們來分析這個效果


[圖片上傳中...(005Xtdi2jw1f4fp2myqo4g308c05k75k.gif-ee6867-1530211885892-0)]

很明顯我們看到當前這里是一個圓,運用了一張圖片,讓這張圖能夠沿著當前的這個圓進行移動
那么,這個圓形是我們用Path所繪制的,那么當前Path會記錄下當前圓的所有點,而我們需要將那個箭頭圖片繪制到我們path的點上面,并且按照圓形角度來進行操控而圖形是這樣的

arrow.jpg

那么這個時候我們能夠反映過來,去得到當前圖片進行旋轉,能夠做到這一點, 但是我們如何判斷這旋轉的角度?

而測量當中提供了

/**
 * 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.png

那么此時能夠看到所謂的切線,
下面掃盲,段位高跳過
幾何上,切線指的是一條剛好觸碰到曲線上某一點的直線。更準確地說,當切線經過曲線上的某點(即切點)時,切線的方向與曲線上該點的方向是相同的。平面幾何中,將和圓只有一個公共交點的直線叫做圓的切線
正切函數直角三角形中,對邊與鄰邊的比值叫做正切。放在直角坐標系中(如圖)即 tanθ=y/x
而tan就是我們的正切值
如上圖,參考上圖
隨機選取了一個橙點(具體位置),那么切線是和橙點相交的這條線,切線角度為垂直關系,所以如下圖
實在不理解TAN的話,你們就理解為當前得到了圓心坐標,因為圓的切線是圓心《建議去復習下初中數學》

image.png

那么此時,我們拿到的getPosTan方法,能夠把當前這個點,和這個點的正切值拿到,我們可以通過反正切計算取得角度,那么橙線和X軸的夾角其實實際上應該是我們到時候顯示過去的角度,那么此時,看下圖


tan.png

紅線所繪制的角度是我們當前角度,綠線繪制的是需要旋轉的角度, 那么我們現在手里擁有的資源是,當前正切值,通過正切值我們運用
公式可以計算得到當前角度


d52a2834349b033b36892f7417ce36d3d539bd1f.jpg

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
預習其他資料上傳網盤,可以參考

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,990評論 2 374

推薦閱讀更多精彩內容