PathMeasure之迷徑追蹤

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

1.gif

代碼如下所示:

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

2.gif

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

3.gif

代碼如下所示:

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

4.gif

只不過這里在繪制的時候沮协,使用了一些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閱讀 219,039評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異讹蘑,居然都是意外死亡末盔,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評論 3 395
  • 文/潘曉璐 我一進(jìn)店門座慰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來陨舱,“玉大人,你說我怎么就攤上這事版仔∮蚊ぃ” “怎么了?”我有些...
    開封第一講書人閱讀 165,417評論 0 356
  • 文/不壞的土叔 我叫張陵蛮粮,是天一觀的道長益缎。 經(jīng)常有香客問我,道長然想,這世上最難降的妖魔是什么链峭? 我笑而不...
    開封第一講書人閱讀 58,868評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮又沾,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘熙卡。我一直安慰自己杖刷,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,892評論 6 392
  • 文/花漫 我一把揭開白布驳癌。 她就那樣靜靜地躺著滑燃,像睡著了一般。 火紅的嫁衣襯著肌膚如雪颓鲜。 梳的紋絲不亂的頭發(fā)上表窘,一...
    開封第一講書人閱讀 51,692評論 1 305
  • 那天,我揣著相機(jī)與錄音甜滨,去河邊找鬼乐严。 笑死,一個胖子當(dāng)著我的面吹牛衣摩,可吹牛的內(nèi)容都是我干的昂验。 我是一名探鬼主播,決...
    沈念sama閱讀 40,416評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼既琴!你這毒婦竟也來了占婉?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,326評論 0 276
  • 序言:老撾萬榮一對情侶失蹤甫恩,失蹤者是張志新(化名)和其女友劉穎逆济,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體磺箕,經(jīng)...
    沈念sama閱讀 45,782評論 1 316
  • 正文 獨居荒郊野嶺守林人離奇死亡奖慌,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,957評論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了滞磺。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片升薯。...
    茶點故事閱讀 40,102評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖击困,靈堂內(nèi)的尸體忽然破棺而出涎劈,到底是詐尸還是另有隱情,我是刑警寧澤阅茶,帶...
    沈念sama閱讀 35,790評論 5 346
  • 正文 年R本政府宣布蛛枚,位于F島的核電站,受9級特大地震影響脸哀,放射性物質(zhì)發(fā)生泄漏蹦浦。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,442評論 3 331
  • 文/蒙蒙 一撞蜂、第九天 我趴在偏房一處隱蔽的房頂上張望盲镶。 院中可真熱鬧,春花似錦蝌诡、人聲如沸溉贿。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽宇色。三九已至,卻和暖如春颁湖,著一層夾襖步出監(jiān)牢的瞬間宣蠕,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評論 1 272
  • 我被黑心中介騙來泰國打工甥捺, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留抢蚀,地道東北人。 一個月前我還...
    沈念sama閱讀 48,332評論 3 373
  • 正文 我出身青樓镰禾,卻偏偏與公主長得像思币,于是被迫代替她去往敵國和親鹿响。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,044評論 2 355

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