PathMeasure之迷徑追蹤

轉(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片段耿币,從而不斷繪制完整的路徑,效果如圖所示:

image

代碼如下所示:

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圖,效果如下所示:

image

我們只需要修改下起始值的數(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而且有效的方式鄙才,效果如圖所示:

image

代碼如下所示:

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效果如圖所示:

image

只不過這里在繪制的時候矛市,使用了一些Trick,先通過canvas.translate方法將原點移動的圓心而昨,同時,通過canvas.rotate將運動趨勢的角度轉(zhuǎn)換為畫布的旋轉(zhuǎn)歌憨,這樣每次繪制切線墩衙,就只需要畫一條同樣的切線即可务嫡。

源代碼

源碼已上傳到Github:

https://github.com/xuyisheng/PathArt

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末漆改,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子去扣,更是在濱河造成了極大的恐慌,老刑警劉巖愉棱,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件哲戚,死亡現(xiàn)場離奇詭異奔滑,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)惫恼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進(jìn)店門档押,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事令宿〉鸢遥” “怎么了粒没?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長爽撒。 經(jīng)常有香客問我响蓉,道長,這世上最難降的妖魔是什么枫甲? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮粱栖,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘闹究。我一直安慰自己食店,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布叛买。 她就那樣靜靜地躺著,像睡著了一般刻伊。 火紅的嫁衣襯著肌膚如雪椒功。 梳的紋絲不亂的頭發(fā)上捶箱,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天丁屎,我揣著相機(jī)與錄音,去河邊找鬼晨川。 笑死,一個胖子當(dāng)著我的面吹牛共虑,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播妈拌,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼尘分,長吁一口氣:“原來是場噩夢啊……” “哼猜惋!你這毒婦竟也來了培愁?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤梨撞,失蹤者是張志新(化名)和其女友劉穎香罐,沒想到半個月后时肿,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體庇茫,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡旦签,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年寸宏,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片氮凝。...
    茶點故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖竿秆,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情幽钢,我是刑警寧澤傅是,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布蕾羊,位于F島的核電站帽驯,受9級特大地震影響肚豺,放射性物質(zhì)發(fā)生泄漏界拦。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一截碴、第九天 我趴在偏房一處隱蔽的房頂上張望蛉威。 院中可真熱鬧日丹,春花似錦蚯嫌、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽汪诉。三九已至谈秫,卻和暖如春扒寄,著一層夾襖步出監(jiān)牢的瞬間拟烫,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工课竣, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留喜颁,地道東北人稠氮。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓隔披,卻偏偏與公主長得像,于是被迫代替她去往敵國和親奢米。 傳聞我的和親對象是個殘疾皇子抓韩,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,901評論 2 345

推薦閱讀更多精彩內(nèi)容