Android動(dòng)畫(huà)——PathMeasure

1. 了解

Path的繪制有很多種方法萌京,例如Android API,Bezier曲線或者數(shù)學(xué)函數(shù)表達(dá)式等刷钢,而高級(jí)的動(dòng)畫(huà)都會(huì)要求這個(gè)Path的坐標(biāo)點(diǎn)是可控的卤橄,這樣才能更好地?cái)U(kuò)展基于Path的動(dòng)畫(huà)。而如何確定Path點(diǎn)的坐標(biāo)辛蚊,這就用到了本次分析的工具類PathMeasure粤蝎。

  • 常用API
方法 解析
PathMeasure pathMeasure = new PathMeasure(); 創(chuàng)建PathMeasure對(duì)象
pathMeasure.setPath(path, true); 設(shè)置關(guān)聯(lián)Path
PathMeasure (Path path, boolean forceClosed) 在構(gòu)造方法里關(guān)聯(lián)Path
gentLength 獲取計(jì)算的長(zhǎng)度
getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo) 獲取路徑的片段,前兩個(gè)參數(shù)表示起止點(diǎn)坐標(biāo)嚼隘,dst表示截取path輸出結(jié)果诽里,startWithMoveTo表示是否從上一次截取的終點(diǎn)處開(kāi)始截取
getPosTan(float distance, float[] pos, float[] tan) 獲取某點(diǎn)坐標(biāo)及其切線坐標(biāo)

有幾點(diǎn)需要重視一下:
forceClosed參數(shù)對(duì)綁定的Path不會(huì)產(chǎn)生任何影響,只會(huì)對(duì)PathMeasure 的測(cè)量結(jié)果有影響飞蛹。

    Path path = new Path();

    path.lineTo(0,200);
    path.lineTo(200,200);
    path.lineTo(200,0);

    PathMeasure measure1 = new PathMeasure(path,false);
    PathMeasure measure2 = new PathMeasure(path,true);

    Log.e("TAG", "forceClosed=false---->"+measure1.getLength());
    Log.e("TAG", "forceClosed=true----->"+measure2.getLength());

    canvas.drawPath(path,mDeafultPaint);

Log如下:
25521-25521/com.blue.canvas E/TAG: forceClosed=false---->600.0
25521-25521/com.blue.canvas E/TAG: forceClosed=true----->800.0

事例.png

可以看出當(dāng)forceClosed為true,在測(cè)量path長(zhǎng)度時(shí)灸眼,會(huì)自動(dòng)補(bǔ)上使其閉合卧檐,長(zhǎng)度就為閉合的長(zhǎng)度。但是forceClosed無(wú)論true還是false焰宣,都不影響Path本身的值霉囚。

另外,getPosTan獲取切線坐標(biāo)之后匕积,可以通過(guò)下面的公式計(jì)算出某點(diǎn)的切線角度:
(Math.atan2(mTan[1], mTan[0]) * 180 / Math.PI);

2. Demo

PathTracing.gif

利用PathMeasure實(shí)現(xiàn)一個(gè)Windows樣式的加載動(dòng)畫(huà)盈罐“竦可以看出動(dòng)畫(huà)是前半部分是完整的半圓曲線,后半部分曲線的末尾加速向曲線頭部靠攏盅粪。用到了PathMeasure的getSegment方法截取一部分運(yùn)動(dòng)軌跡的操作钓葫。下面來(lái)看具體實(shí)現(xiàn)。

先初始化相關(guān)變量和操作:

    // 截取path的輸出
    private Path mDst;
    private Paint mPaint;
    // 用于繪圖的原始Path
    private Path mPath;
    // 獲取Path的長(zhǎng)度
    private float mLength;
    private float mAnimValue;
    // 測(cè)量的工具類
    private PathMeasure mPathMeasure;

在構(gòu)造器中初始化操作:

    public PathTracingView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setStrokeWidth(5);
        mPaint.setStyle(Paint.Style.STROKE);

        mPath = new Path();
        mDst = new Path();

        // 劃一個(gè)圓
        mPath.addCircle(400, 400, 100, Path.Direction.CW);
        mPathMeasure = new PathMeasure();
        // 關(guān)聯(lián)Path票顾,由于畫(huà)出的圓已經(jīng)是閉合的了础浮,所以true和false都無(wú)關(guān)緊要了
        mPathMeasure.setPath(mPath, true);
        // 獲取路徑的長(zhǎng)度
        mLength = mPathMeasure.getLength();

        // 從0到100%變化
        ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1);
        valueAnimator.setDuration(1000);
        valueAnimator.setInterpolator(new LinearInterpolator());
        valueAnimator.setRepeatCount(ValueAnimator.INFINITE);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                mAnimValue = (float) valueAnimator.getAnimatedValue();
                invalidate();
            }
        });
        valueAnimator.start();
    }

在onDraw方法繪制動(dòng)畫(huà):

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mDst.reset();
        // 避免Android上硬件加速的bug,在調(diào)用getSegment方法時(shí)奠骄,對(duì)mDst進(jìn)行l(wèi)ineTo操作
        mDst.lineTo(0, 0);

        // 終點(diǎn)坐標(biāo)從0到100%變化
        float stop = mLength * mAnimValue;
        // 在前半段start為0豆同,后半段快速向stop靠攏
        float start = (float) (stop - ((0.5 - Math.abs(mAnimValue - 0.5)) * mLength));

        // 獲取截取片段
        mPathMeasure.getSegment(start, stop, mDst, true);
        canvas.drawPath(mDst, mPaint);
    }

運(yùn)行之后就能出現(xiàn)上面的動(dòng)畫(huà)效果。

3. 進(jìn)階

3.1 Dash樣式

paint.png

對(duì)于Android繪制動(dòng)畫(huà)的畫(huà)筆來(lái)說(shuō)含鳞,有如上幾種表現(xiàn)形式影锈,其中Dash表示實(shí)線、虛線的結(jié)合蝉绷。通過(guò)畫(huà)筆的Dash樣式精居,也可以用來(lái)實(shí)現(xiàn)路徑的變換動(dòng)畫(huà)——將虛線/實(shí)線填充整個(gè)路徑,然后改變偏移量的值潜必,讓實(shí)線/虛線不斷地填充靴姿,以達(dá)到實(shí)線虛線相互交替。

下面來(lái)看如何實(shí)現(xiàn)磁滚。

創(chuàng)建自定義View佛吓,然后實(shí)現(xiàn)構(gòu)造方法:

    public PathPaintView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setStrokeWidth(5);
        mPaint.setStyle(Paint.Style.STROKE);

        mPath = new Path();

        // 繪制一個(gè)三角形
        mPath.moveTo(100, 100);
        mPath.lineTo(100, 500);
        mPath.lineTo(400, 300);
        mPath.close();

        mPathMeasure = new PathMeasure();
        mPathMeasure.setPath(mPath, true);
        // 取出具體長(zhǎng)度
        mLength = mPathMeasure.getLength();

        ValueAnimator valueAnimator = ValueAnimator.ofFloat(1, 0);
        valueAnimator.setDuration(2000);
        valueAnimator.setInterpolator(new LinearInterpolator());
        valueAnimator.setRepeatCount(ValueAnimator.INFINITE);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                mAnimValue = (float) valueAnimator.getAnimatedValue();
                // 設(shè)置畫(huà)筆風(fēng)格樣式Dash
                // 將實(shí)線和虛線都設(shè)置為整個(gè)路徑的長(zhǎng)度,第二個(gè)參數(shù)是偏移量垂攘,從0到100%
                // 這樣實(shí)線或者虛線會(huì)一點(diǎn)一點(diǎn)地?cái)D開(kāi)
                mPathEffect = new DashPathEffect(new float[]{mLength, mLength}, mLength * mAnimValue);
                mPaint.setPathEffect(mPathEffect);
                invalidate();
            }
        });
        valueAnimator.start();
    }

對(duì)于這種方式實(shí)現(xiàn)動(dòng)畫(huà)维雇,就不用截取路徑了,在onDraw中直接繪制即可:

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawPath(mPath, mPaint);
    }

實(shí)現(xiàn)效果如下圖:


PathDash.gif

3.2 getPosTan

對(duì)于高級(jí)動(dòng)畫(huà)效果來(lái)說(shuō)晒他,對(duì)于運(yùn)動(dòng)軌跡上的點(diǎn)的控制是必要的吱型,因?yàn)榭梢愿鶕?jù)點(diǎn)的坐標(biāo),做一些動(dòng)態(tài)地改變陨仅。

PathMeasure.getPosTan方法就是獲取運(yùn)動(dòng)軌跡點(diǎn)的坐標(biāo)和切線方向的津滞。下面來(lái)使用這個(gè)API。

創(chuàng)建一個(gè)自定義View灼伤,初始化操作:

    private Path mPath;
    // 存放取出點(diǎn)的具體坐標(biāo)
    private float[] mPos;
    // 當(dāng)前曲線的運(yùn)動(dòng)趨勢(shì)即橫縱坐標(biāo)
    private float[] mTan;
    private Paint mPaint;
    private PathMeasure mPathMeasure;
    private ValueAnimator mValueAnim;
    private float mCurrentValue;
    public PathPosTanView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        mPath = new Path();
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setStrokeWidth(5);
        // 繪制一個(gè)圓形
        mPath.addCircle(0, 0, 200, Path.Direction.CW);

        mPathMeasure = new PathMeasure();
        mPathMeasure.setPath(mPath, false);
        // 初始化數(shù)組触徐,橫縱坐標(biāo)一共兩個(gè)
        mPos = new float[2];
        mTan = new float[2];

        setOnClickListener(this);

        mValueAnim = ValueAnimator.ofFloat(0, 1);
        mValueAnim.setDuration(3000);
        mValueAnim.setInterpolator(new LinearInterpolator());
        mValueAnim.setRepeatCount(ValueAnimator.INFINITE);
        mValueAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                mCurrentValue = (float) valueAnimator.getAnimatedValue();
                invalidate();
            }
        });
    }

接下來(lái)在onDraw上繪制,圓形運(yùn)動(dòng)軌跡狐赡、軌跡上運(yùn)動(dòng)的小圓形和運(yùn)動(dòng)時(shí)切線的方向:

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 第一個(gè)參數(shù)是運(yùn)動(dòng)的軌跡長(zhǎng)度撞鹉,后面兩個(gè)參數(shù)接收獲取的值
        mPathMeasure.getPosTan(mCurrentValue * mPathMeasure.getLength(), mPos, mTan);
        // 獲取路徑上點(diǎn)的切線角度
        float degree = (float) (Math.atan2(mTan[1], mTan[0]) * 180 / Math.PI);
        // 將畫(huà)布鎖定
        canvas.save();
        // 移動(dòng)畫(huà)布
        canvas.translate(400, 400);
        // 繪制路線
        canvas.drawPath(mPath, mPaint);
        // 繪制在運(yùn)動(dòng)軌跡上的圓
        canvas.drawCircle(mPos[0], mPos[1], 10, mPaint);
        // 旋轉(zhuǎn)畫(huà)布角度
        canvas.rotate(degree);
        // 繪制切線
        canvas.drawLine(0, -200, 300, -200, mPaint);
        // 畫(huà)布釋放
        canvas.restore();
    }

需要注意的一點(diǎn)是,沒(méi)有必要為了每個(gè)點(diǎn)繪制對(duì)應(yīng)的切線,這樣會(huì)十分麻煩鸟雏,因?yàn)槔L制線條需要坐標(biāo)參數(shù)享郊。上面的做法是只繪制切線的初始位置,然后根據(jù)切線的角度移動(dòng)畫(huà)布孝鹊,在效果上使得切線也隨之轉(zhuǎn)動(dòng)炊琉,避免了重復(fù)繪制切線的操作。

運(yùn)行效果如下圖:


PathPosTan.gif
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末惶室,一起剝皮案震驚了整個(gè)濱河市温自,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌皇钞,老刑警劉巖悼泌,帶你破解...
    沈念sama閱讀 217,907評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異夹界,居然都是意外死亡馆里,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,987評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門可柿,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)鸠踪,“玉大人,你說(shuō)我怎么就攤上這事复斥∮埽” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,298評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵目锭,是天一觀的道長(zhǎng)评汰。 經(jīng)常有香客問(wèn)我,道長(zhǎng)痢虹,這世上最難降的妖魔是什么被去? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,586評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮奖唯,結(jié)果婚禮上惨缆,老公的妹妹穿的比我還像新娘。我一直安慰自己丰捷,他們只是感情好坯墨,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,633評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著瓢阴,像睡著了一般畅蹂。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上荣恐,一...
    開(kāi)封第一講書(shū)人閱讀 51,488評(píng)論 1 302
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼叠穆。 笑死少漆,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的硼被。 我是一名探鬼主播示损,決...
    沈念sama閱讀 40,275評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼嚷硫!你這毒婦竟也來(lái)了检访?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,176評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤仔掸,失蹤者是張志新(化名)和其女友劉穎脆贵,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體起暮,經(jīng)...
    沈念sama閱讀 45,619評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡卖氨,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,819評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了负懦。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片筒捺。...
    茶點(diǎn)故事閱讀 39,932評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖纸厉,靈堂內(nèi)的尸體忽然破棺而出系吭,到底是詐尸還是另有隱情,我是刑警寧澤颗品,帶...
    沈念sama閱讀 35,655評(píng)論 5 346
  • 正文 年R本政府宣布肯尺,位于F島的核電站,受9級(jí)特大地震影響抛猫,放射性物質(zhì)發(fā)生泄漏蟆盹。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,265評(píng)論 3 329
  • 文/蒙蒙 一闺金、第九天 我趴在偏房一處隱蔽的房頂上張望逾滥。 院中可真熱鬧,春花似錦败匹、人聲如沸寨昙。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,871評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)舔哪。三九已至,卻和暖如春槽棍,著一層夾襖步出監(jiān)牢的瞬間捉蚤,已是汗流浹背抬驴。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,994評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留缆巧,地道東北人布持。 一個(gè)月前我還...
    沈念sama閱讀 48,095評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像陕悬,于是被迫代替她去往敵國(guó)和親题暖。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,884評(píng)論 2 354

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