【Android開源項(xiàng)目解析】仿支付寶付款成功及"天女散花"效果實(shí)現(xiàn)——看PathMeasure大展身手

話說瑞凑,在前面兩篇文章中,我們學(xué)習(xí)了BitmapShader、Path的基本使用会前,那么這一篇文章好乐,咱們接著來學(xué)習(xí)一下PathMeasure的用法。什么瓦宜,你沒聽說過PathMeasure蔚万?那你就要OUT咯~

項(xiàng)目效果圖

廢話不多說,在開始講解之前临庇,先看下最終實(shí)現(xiàn)的效果反璃。

效果一:

仿支付寶支付成功效果

效果二:

這兩個(gè)項(xiàng)目都是使用Path和PathMeature配合完成的,由其他項(xiàng)目改造而來

項(xiàng)目一是七叔寫的假夺,我對(duì)代碼進(jìn)行了大量改造淮蜈。

項(xiàng)目二是不小心搜到的,然后進(jìn)行了改造已卷,原文請(qǐng)戳這里

本文代碼請(qǐng)到這里下載

PathMeasure介紹

PathMeasure這個(gè)類確實(shí)是不太常見的梧田,關(guān)于這個(gè)類的介紹也是甚少,那么這個(gè)類是用來干嘛的呢侧蘸?主要其實(shí)是配合Path裁眯,來計(jì)算Path里面點(diǎn)的坐標(biāo)的,或者是給一個(gè)范圍讳癌,來截取Path其中的一部分的穿稳。

這么說,你肯定也迷糊晌坤,咱們先簡(jiǎn)單看一下有哪些方法逢艘,然后根據(jù)案例來進(jìn)行講解更好一些。

構(gòu)造方法有兩個(gè)骤菠,很好理解它改,不多解釋。

PathMeasure()
PathMeasure(Path path, boolean forceClosed)

重點(diǎn)看下常用方法:

  • float getLength() 返回當(dāng)前contour(解釋為輪廓不太恰當(dāng)娩怎,我覺得更像是筆畫)的長(zhǎng)度搔课,也就是這一個(gè)Path有多長(zhǎng)
  • boolean getPosTan(float distance, float[] pos, float[] tan) 傳入一個(gè)距離distance(0<=distance<=getLength()),然后會(huì)計(jì)算當(dāng)前距離的坐標(biāo)點(diǎn)和切線截亦,注意爬泥,pos會(huì)自動(dòng)填充上坐標(biāo),這個(gè)方法很重要
  • boolean getSegment(float startD, float stopD, Path dst, boolean startWithMoveTo) 傳入一個(gè)開始和結(jié)束距離崩瓤,然后會(huì)返回介于這之間的Path袍啡,在這里就是dst,他會(huì)被填充上內(nèi)容却桶,這個(gè)方法很重要
  • boolean nextContour() 移動(dòng)到下一個(gè)筆畫境输,如果你的Path是由多個(gè)筆畫組成的話蔗牡,那么就可以使用這個(gè)方法
  • void setPath(Path path, boolean forceClosed)這個(gè)方法也比較重要,用來設(shè)置新的Path對(duì)象的嗅剖,算是對(duì)第一個(gè)構(gòu)造函數(shù)的一個(gè)補(bǔ)充

仿支付寶實(shí)現(xiàn)原理解析

下面辩越,我將介紹一下如何實(shí)現(xiàn)下面的這個(gè)效果

首先分析需求:

  • 需要有三種狀態(tài):加載中,成功信粮,失敗
  • 加載中時(shí)黔攒,需要不斷更換顏色
  • 加載中狀態(tài)時(shí),圓弧要不斷的變換長(zhǎng)度和位置
  • 成功狀態(tài)和失敗狀態(tài)强缘,需要把√和×一筆一劃的畫出來

OK督惰,基本就是這些需求,那么對(duì)應(yīng)著需求旅掂,咱們看一下解決方案

  • 有三種狀態(tài)好說赏胚,用靜態(tài)常量或者是枚舉類型進(jìn)行區(qū)分
  • 不斷變換顏色也好說,只要改變Paint的顏色就可以啦
  • 不斷的變化長(zhǎng)度和位置商虐,從效果圖上可以看出來觉阅,我們需要畫一段圓弧,那就要用下面的drawArc()秘车,需要知道范圍留拾,起始角度和繪制角度,由于需要不斷的變化長(zhǎng)度鲫尊,因此就需要用Animator,具體實(shí)現(xiàn)一會(huì)詳談
Canvas.drawArc(RectF oval, float startAngle, float sweepAngle, boolean useCenter,Paint paint) 
  • 需要畫出來形狀沦偎,其實(shí)就是一些線段疫向,那么就需要用Path了,但是如何能一筆一劃的效果呢豪嚎?那就要靠PathMeasure啦

下面開始講解代碼實(shí)現(xiàn)搔驼,最好參照著源代碼看下面的文章。

首先看怎么用ConfirmView呢侈询?很簡(jiǎn)單舌涨,只需要調(diào)用animatedWithState()然后傳入一個(gè)枚舉類型即可

confirmView.animatedWithState(ConfirmView.State.Progressing);

這個(gè)枚舉類型在類的內(nèi)部,代表三種狀態(tài)

public enum State {
        Success, Fail, Progressing
    }

再看構(gòu)造函數(shù)扔字,很簡(jiǎn)單囊嘉,只是進(jìn)行了變量的初始化,這些變量的具體作用革为,我將在下面用到的時(shí)候重點(diǎn)介紹

public ConfirmView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);

        mSuccessPath = new Path();
        mPathMeasure = new PathMeasure(mSuccessPath, false);
        mRenderPaths = new ArrayList<>();

        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setColor(0xFF0099CC);
        mPaint.setStrokeWidth(STROKEN_WIDTH);
        mPaint.setStrokeCap(Paint.Cap.ROUND);

        oval = new RectF();
    }

那么調(diào)用了animatedWithState()之后扭粱,進(jìn)行了什么操作呢?

public void animatedWithState(State state) {

        if (mCurrentState != state) {
            mCurrentState = state;
            if (mPhareAnimator != null && mPhareAnimator.isRunning()) {
                stopPhareAnimation();
            }
            switch (state) {
                case Fail:
                case Success:
                    updatePath();
                    if (mCircleAnimator != null && mCircleAnimator.isRunning()) {
                        mCircleAngle = (Float) mCircleAnimator.getAnimatedValue();
                        mCircleAnimator.end();
                    }

                    if ((mStartAngleAnimator == null || !mStartAngleAnimator.isRunning() || !mStartAngleAnimator.isStarted()) &&
                            (mEndAngleAnimator == null || !mEndAngleAnimator.isRunning() || !mEndAngleAnimator.isStarted())) {
                        mStartAngle = 360;
                        mEndAngle = 0;
                        startPhareAnimation();
                    }

                    break;
                case Progressing:
                    mCircleAngle = 0;
                    startCircleAnimation();
                    break;
            }
        }

    }

結(jié)合著上面的代碼震檩,我簡(jiǎn)單解釋一下琢蛤。

首先進(jìn)行重復(fù)性的判斷蜓堕,如果當(dāng)前所處的狀態(tài)與要改變的狀態(tài)相同則不進(jìn)行操作。

接下來博其,對(duì)動(dòng)畫狀態(tài)進(jìn)行了判斷套才,mPhareAnimator是用來實(shí)現(xiàn)√和×的動(dòng)畫繪制效果的,如果正在運(yùn)行慕淡,則停掉背伴。

再往下的一個(gè)switch則是開始真正的操作了,updatePath()是更新Path儡率,一會(huì)重點(diǎn)看下挂据,mCircleAnimator這個(gè)則是實(shí)現(xiàn)外部弧形的偏移量的控制的,現(xiàn)在看不明白也沒事儿普,重點(diǎn)看下下面的代碼崎逃,當(dāng)mStartAngleAnimator和mEndAngleAnimator都不在運(yùn)行狀態(tài)的時(shí)候(這兩個(gè)Animator是為了控制外部弧形的起點(diǎn)和終點(diǎn)的),會(huì)進(jìn)入下面的代碼眉孩,

mStartAngle = 360;
mEndAngle = 0;
startPhareAnimation();

mStartAngle和mEndAngle分別代表起點(diǎn)轉(zhuǎn)過的角度和終點(diǎn)轉(zhuǎn)過的角度个绍,然后就startPhareAnimation(),這個(gè)時(shí)候浪汪,真正的繪制√和×的動(dòng)畫才開始執(zhí)行巴柿。

如果是Progressing呢,則執(zhí)行下面的代碼死遭,重置mCircleAngle广恢,startCircleAnimation()這個(gè)方法是繪制外部的弧形的動(dòng)畫

mCircleAngle = 0;
startCircleAnimation();

至此,咱們知道了傳入不同狀態(tài)的枚舉類型會(huì)進(jìn)行什么操作呀潭,下面钉迷,開始看真正的操作。

咱先看一個(gè)簡(jiǎn)單的钠署,就是startCircleAnimation()到底做了什么糠聪。

前面說過,這個(gè)方法是為了繪制加載中狀態(tài)時(shí)谐鼎,外部不斷變化的彩色弧形的舰蟆,下面是代碼實(shí)現(xiàn)

public void startCircleAnimation() {
        if (mCircleAnimator == null || mStartAngleAnimator == null || mEndAngleAnimator == null) {
            initAngleAnimation();
        }
        mStartAngleAnimator.setDuration(NORMAL_ANGLE_ANIMATION_DURATION);
    mEndAngleAnimator.setDuration(NORMAL_ANGLE_ANIMATION_DURATION);
        mCircleAnimator.setDuration(NORMAL_CIRCLE_ANIMATION_DURATION);
        mStartAngleAnimator.start();
        mEndAngleAnimator.start();
        mCircleAnimator.start();
    }

首先前面的if語句是為空判斷,從而進(jìn)行初始化的操作狸棍,后面則是簡(jiǎn)單的設(shè)置動(dòng)畫的持續(xù)時(shí)間和開啟動(dòng)畫身害。這里一共出現(xiàn)了三個(gè)動(dòng)畫,完成外部弧形的效果控制

  • mStartAngleAnimator 控制圓弧起點(diǎn)
  • mEndAngleAnimator 控制圓弧終點(diǎn)
  • mCircleAnimator 控制圓弧的整體偏移量

這么說隔缀,你可能還是不很明白题造,沒關(guān)系,咱們一點(diǎn)點(diǎn)的看代碼猾瘸,首先界赔,咱們看在初始化的時(shí)候丢习,到底做了什么操作,也就是initAngleAnimation()淮悼。

 private void initAngleAnimation() {

        mStartAngleAnimator = ValueAnimator.ofFloat(0.0F, 1.0F);
        mEndAngleAnimator = ValueAnimator.ofFloat(0.0F, 1.0F);
        mCircleAnimator = ValueAnimator.ofFloat(0.0F, 1.0F);

        mStartAngleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float value = (Float) animation.getAnimatedValue();
                setStartAngle(value);
            }
        });
        mEndAngleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float value = (Float) animation.getAnimatedValue();
                setEndAngle(value);
            }
        });

        mStartAngleAnimator.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {

                if (mCurrentState == State.Progressing) {
                    if (mEndAngleAnimator != null) {
                        new Handler().postDelayed(new Runnable() {
                            @Override
                            public void run() {
                                mEndAngleAnimator.start();
                            }
                        }, 400L);
                    }
                }
            }

            @Override
            public void onAnimationEnd(Animator animation) {
                if (mCurrentState != State.Progressing && mEndAngleAnimator != null && !mEndAngleAnimator.isRunning() && !mEndAngleAnimator.isStarted()) {
                    startPhareAnimation();
                }
            }

            @Override
            public void onAnimationCancel(Animator animation) {
            }

            @Override
            public void onAnimationRepeat(Animator animation) {
            }
        });

        mEndAngleAnimator.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {

            }

            @Override
            public void onAnimationEnd(Animator animation) {
                if (mStartAngleAnimator != null) {
                    if (mCurrentState != State.Progressing) {
                        mStartAngleAnimator.setDuration(NORMAL_ANIMATION_DURATION);
                    }
                    colorCursor++;
                    if (colorCursor >= colors.length) colorCursor = 0;
                    mPaint.setColor(colors[colorCursor]);
                    mStartAngleAnimator.start();
                }
            }

            @Override
            public void onAnimationCancel(Animator animation) {
            }

            @Override
            public void onAnimationRepeat(Animator animation) {
            }
        });

        mCircleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float value = (float) animation.getAnimatedValue();
                setCircleAngle(value);
            }
        });


        mStartAngleAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
        mEndAngleAnimator.setInterpolator(new AccelerateDecelerateInterpolator());


        mCircleAnimator.setInterpolator(new LinearInterpolator());
        mCircleAnimator.setRepeatCount(-1);
    }

這段代碼雖然長(zhǎng)咐低,但是也沒有太大的難度,無非就是進(jìn)行了初始化操作袜腥,ValueAnimator的范圍是0-1见擦,這個(gè)在后面將用于計(jì)算角度。在值不斷的更新的過程中羹令,分別調(diào)用了下面這三個(gè)方法鲤屡,更新一些值

setStartAngle(value);
setEndAngle(value);
setCircleAngle(value);

在這三個(gè)方法里面,都對(duì)成員變量進(jìn)行了更新福侈,并且酒来!調(diào)用了invalidate()!看到這里是不是激動(dòng)了肪凛,改變一次就重繪一次堰汉,這三個(gè)值肯定和弧形的動(dòng)畫效果有關(guān)啊伟墙!

   private void setStartAngle(float startAngle) {
        this.mStartAngle = startAngle;
        invalidate();
    }

    private void setEndAngle(float endAngle) {
        this.mEndAngle = endAngle;
        invalidate();
    }

    private void setCircleAngle(float circleAngle) {
        this.mCircleAngle = circleAngle;
        invalidate();
    }

咱知道了這個(gè)翘鸭,先不著急去看onDraw(),仔細(xì)看下動(dòng)畫的執(zhí)行順序戳葵。

在mStartAngleAnimator執(zhí)行之后就乓,調(diào)用了下面的方法,這當(dāng)然很簡(jiǎn)單拱烁,就是說档址,mStartAngleAnimator執(zhí)行了400毫秒之后,mEndAngleAnimator才會(huì)執(zhí)行,而且插值器設(shè)置的是AccelerateDecelerateInterpolator,為啥呢站粟?很簡(jiǎn)單呢灶,因?yàn)橹挥羞@樣,才能做出弧形長(zhǎng)度先長(zhǎng)后短的效果呀~

new Handler().postDelayed(new Runnable() {
       @Override
        public void run() {
            mEndAngleAnimator.start();
        }
       }, 400L);

而在mEndAngleAnimator執(zhí)行結(jié)束之后见芹,會(huì)調(diào)用下面的代碼

if (mStartAngleAnimator != null) {
    if (mCurrentState != State.Progressing) {
        mStartAngleAnimator.setDuration(NORMAL_ANIMATION_DURATION);
    }
    colorCursor++;
    if (colorCursor >= colors.length) colorCursor = 0;
    mPaint.setColor(colors[colorCursor]);
    mStartAngleAnimator.start();
}

在這個(gè)設(shè)置mStartAngleAnimator的動(dòng)畫時(shí)間剂娄,是為了畫√或者是×的時(shí)候快一些效果更流暢。下面的代碼很簡(jiǎn)單了吧玄呛,改變畫筆顏色阅懦,然后mStartAngleAnimator又開啟啦!這就是為啥一直轉(zhuǎn)啊轉(zhuǎn)的原因徘铝。

但是說到這里耳胎,咱們還沒看onDraw()做了什么呢惯吕!

 @Override
    public void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        switch (mCurrentState) {
            case Fail:
                for (int i = 0; i < PATH_SIZE_TWO; i++) {
                    Path p = mRenderPaths.get(i);
                    if (p != null) {
                        canvas.drawPath(p, mPaint);
                    }
                }
                drawCircle(canvas);
                break;
            case Success:
                Path p = mRenderPaths.get(0);
                if (p != null) {
                    canvas.drawPath(p, mPaint);
                }
                drawCircle(canvas);
                break;
            case Progressing:
                drawCircle(canvas);
                break;
        }

    }

咱先看Progressing分支里面的drawCircle(canvas),其他的先不要管

private void drawCircle(Canvas canvas) {
        float offsetAngle = mCircleAngle * 360;
        float startAngle = mEndAngle * 360;
        float sweepAngle = mStartAngle * 360;

        if (startAngle == 360)
            startAngle = 0;
        sweepAngle = sweepAngle - startAngle;
        startAngle += offsetAngle;

        if (sweepAngle < 0)
            sweepAngle = 1;

        canvas.drawArc(oval, startAngle, sweepAngle, false, mPaint);
    }

是的怕午,上面這段代碼就是繪制不斷變幻的環(huán)的代碼咯

float startAngle = mEndAngle * 360;是計(jì)算終點(diǎn)的位置废登,有人會(huì)感到奇怪,為啥終點(diǎn)的位置叫startAngle坝粝А堡距!因?yàn)榻K點(diǎn)的位置就是開始繪制的位置,所以不要奇怪了兆蕉。

sweepAngle = sweepAngle - startAngle;則是計(jì)算要畫多少角度的弧線羽戒,因?yàn)槠瘘c(diǎn)先跑到前面的,所以減去終點(diǎn)的位置虎韵,就是旋轉(zhuǎn)角度易稠。

startAngle += offsetAngle;那么這句是干嘛的?這個(gè)就是所謂的偏移量劝术,為了要實(shí)現(xiàn)更隨性的從非固定點(diǎn)開始結(jié)束的效果缩多。沒聽懂?我給你去掉你看下效果养晋!


    private void drawCircle(Canvas canvas) {
        float offsetAngle = mCircleAngle * 360;
        float startAngle = mEndAngle * 360;
        float sweepAngle = mStartAngle * 360;

        if (startAngle == 360)
            startAngle = 0;
        sweepAngle = sweepAngle - startAngle;
//        startAngle += offsetAngle;

        if (sweepAngle < 0)
            sweepAngle = 1;

        canvas.drawArc(oval, startAngle, sweepAngle, false, mPaint);
    }

這下子明白了吧衬吆,去掉漂移量效果就沒有之前那么隨性了~


ok,關(guān)于弧線的問題就說這么多绳泉,下面就要說咱們今天的主角PathMeasure了逊抡。

在前面的代碼中,我們提到零酪,成功和失敗狀態(tài)會(huì)執(zhí)行updatePath()和startPhareAnimation()冒嫡,那么到底做了些什么呢?

private void updatePath() {

        int offset = (int) (mSignRadius * 0.15F);
        mRenderPaths.clear();

        switch (mCurrentState) {
            case Success:
                mSuccessPath.reset();
                mSuccessPath.moveTo(mCenterX - mSignRadius, mCenterY + offset);
                mSuccessPath.lineTo(mCenterX - offset, mCenterY + mSignRadius - offset);
                mSuccessPath.lineTo(mCenterX + mSignRadius, mCenterY - mSignRadius + offset);
                mRenderPaths.add(new Path());
                break;
            case Fail:
                mSuccessPath.reset();
                float failRadius = mSignRadius * 0.8F;
                mSuccessPath.moveTo(mCenterX - failRadius, mCenterY - failRadius);
                mSuccessPath.lineTo(mCenterX + failRadius, mCenterY + failRadius);
                mSuccessPath.moveTo(mCenterX + failRadius, mCenterY - failRadius);
                mSuccessPath.lineTo(mCenterX - failRadius, mCenterY + failRadius);
                for (int i = 0; i < PATH_SIZE_TWO; i++) {
                    mRenderPaths.add(new Path());
                }
                break;
            default:
                mSuccessPath.reset();
        }

        mPathMeasure.setPath(mSuccessPath, false);

    }

在updatePath()我們可以很清楚的看到四苇,在這里初始化了mSuccessPath孝凌,通過moveTo()和lineTo()?首先勾勒除了√和×的形狀,至于這個(gè)坐標(biāo)是怎么確定的月腋,這個(gè)可以自己想法來蟀架,我就不介紹了。還要需要注意的是榆骚,Success中最后在mRenderPaths中添加了一個(gè)Path對(duì)象片拍,而在Fail則添加了兩個(gè)對(duì)象,這個(gè)其實(shí)是和要繪制的圖形的筆畫數(shù)有關(guān)的妓肢,×是兩筆捌省,所以是兩個(gè),這里添加的Path議會(huì)將用來紀(jì)錄每一筆畫的形狀碉钠。

最后纲缓,咱們的主角終于現(xiàn)身了

 mPathMeasure.setPath(mSuccessPath, false);

調(diào)用完這個(gè)方法卷拘,會(huì)馬上調(diào)用下面的方法

public void startPhareAnimation() {
        if (mPhareAnimator == null) {
            mPhareAnimator = ValueAnimator.ofFloat(0.0F, 1.0F);
            mPhareAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    float value = (Float) animation.getAnimatedValue();
                    setPhare(value);
                }
            });

            mPhareAnimator.setDuration(NORMAL_ANIMATION_DURATION);
            mPhareAnimator.setInterpolator(new LinearInterpolator());
        }
        mPhare = 0;
        mPhareAnimator.start();
    }

其實(shí)也很簡(jiǎn)單,初始化了mPhareAnimator色徘,然后開啟動(dòng)畫恭金,不斷調(diào)用setPhare(value),

private void setPhare(float phare) {
        mPhare = phare;
        updatePhare();
        invalidate();
    }

在這里updatePhare()褂策,然后重繪界面横腿,那么玄機(jī)應(yīng)該都在updatePhare()了吧!

private void updatePhare() {

        if (mSuccessPath != null) {
            switch (mCurrentState) {
                case Success: {
                    if (mPathMeasure.getSegment(0, mPhare * mPathMeasure.getLength(), mRenderPaths.get(0), true)) {
                        mRenderPaths.get(0).rLineTo(0, 0);
                    }
                }
                break;
                case Fail: {
                    //i = 0,畫一半斤寂,i=1,畫另一半
                    float seg = 1.0F / PATH_SIZE_TWO;

                    for (int i = 0; i < PATH_SIZE_TWO; i++) {
                        float offset = mPhare - seg * i;
                        offset = offset < 0 ? 0 : offset;
                        offset *= PATH_SIZE_TWO;
                        Log.d("i:" + i + ",seg:" + seg, "offset:" + offset + ", mPhare:" + mPhare + ", size:" + PATH_SIZE_TWO);
                        boolean success = mPathMeasure.getSegment(0, offset * mPathMeasure.getLength(), mRenderPaths.get(i), true);

                        if (success) {
                            mRenderPaths.get(i).rLineTo(0, 0);
                        }
                        mPathMeasure.nextContour();
                    }
                    mPathMeasure.setPath(mSuccessPath, false);
                }
                break;
            }
        }
    }

在這里耿焊,一個(gè)很重要的方法調(diào)用了,那就是mPathMeasure.getSegment()

當(dāng)Success的時(shí)候遍搞,會(huì)執(zhí)行下面的代碼罗侯。mPhare就是動(dòng)畫的百分比,從0到1溪猿,那么钩杰,下面的這段代碼就很好理解了,這是為了根據(jù)動(dòng)畫的百分比诊县,獲取畫出√的整個(gè)Path的一部分讲弄,然后把這部分,填充到了mRenderPaths.get(0)里面依痊,這里面存放的就是在上面方法中添加進(jìn)去的一個(gè)Path對(duì)象避除。mPhare不斷的變化,我們就能獲取到畫整個(gè)√形狀所需的所有Path對(duì)象胸嘁,還記得這個(gè)方法之后是什么嗎瓶摆?invalidate()!所以性宏,現(xiàn)在在onDraw()里面肯定用這Path對(duì)象群井,畫出√的一部分,不斷的更新從mPhare毫胜,不斷繪制蝌借,從無到有,而出現(xiàn)了動(dòng)畫效果指蚁。

mRenderPaths.get(0).rLineTo(0, 0);這個(gè)代碼則是為了在4.4以下不能繪制出圖形BUG的解決方法,不要在意自晰。

if (mPathMeasure.getSegment(0, mPhare * mPathMeasure.getLength(), mRenderPaths.get(0), true)) {
     mRenderPaths.get(0).rLineTo(0, 0);
}

不信咱們看下onDraw()凝化,是不是!那么現(xiàn)在你應(yīng)該知道×是怎么畫出來的吧酬荞?

case Success:
Path p = mRenderPaths.get(0);
if (p != null) {
    canvas.drawPath(p, mPaint);
}
drawCircle(canvas);
break;

來來來搓劫,咱們看下代碼瞧哟!

 case Fail: {
   //i = 0,畫一半,i=1,畫另一半
     float seg = 1.0F / PATH_SIZE_TWO;
     for (int i = 0; i < PATH_SIZE_TWO; i++) {
         float offset = mPhare - seg * i;
         offset = offset < 0 ? 0 : offset;
         offset *= PATH_SIZE_TWO;
         Log.d("i:" + i + ",seg:" + seg, "offset:" + offset + ", mPhare:" + mPhare + ", size:" + PATH_SIZE_TWO);
         boolean success = mPathMeasure.getSegment(0, offset * mPathMeasure.getLength(), mRenderPaths.get(i), true);

         if (success) {
             mRenderPaths.get(i).rLineTo(0, 0);
         }

         mPathMeasure.nextContour();
     }
     mPathMeasure.setPath(mSuccessPath, false);
 }
 break;

與繪制√相比枪向,因?yàn)椤潦莾晒P勤揩,所以有些小復(fù)雜,但是也不難秘蛔,offset *= PATH_SIZE_TWO;是為了保證在mPhare從0-0.5過程中控制第一筆畫陨亡,0.5-1則控制第二條筆畫,你仔細(xì)看下代碼深员,這樣可以實(shí)現(xiàn)offset從0-1兩次负蠕。由于×是兩筆畫,所以在i=0取到第一筆畫的Path部分倦畅,存儲(chǔ)在mRenderPaths的第一個(gè)Path之后遮糖,調(diào)用了mPathMeasure.nextContour();切換到下一筆畫,再次完成相同的操作叠赐。

而由于PathMeasure只能往下找Contour欲账,所以最后 mPathMeasure.setPath(mSuccessPath, false);回復(fù)到最初狀態(tài),然后我們看下onDraw()

  for (int i = 0; i < PATH_SIZE_TWO; i++) {
                    Path p = mRenderPaths.get(i);
                    if (p != null) {
                        canvas.drawPath(p, mPaint);
                    }
                }
                drawCircle(canvas);

其實(shí)和Success差不多的芭概,只不過是兩個(gè)Path赛不,畫出兩筆。

OK谈山,到這里俄删,這個(gè)效果就算是全部實(shí)現(xiàn)了,累死我了

"天女散花"實(shí)現(xiàn)效果解析

其實(shí)這個(gè)我并不打算詳細(xì)講奏路,因?yàn)橐煌ò偻ǔ胍嗾f無益,更多的東西需要你自己研究代碼吸收鸽粉,咱們就重點(diǎn)看下PathMeasure的用法斜脂。

其實(shí)這種效果實(shí)現(xiàn)的真相是這樣滴

YES!就是一些Bitmap對(duì)象沿著Path路徑移動(dòng)!

那么和PathMeasure有啥關(guān)系呢触机?

看下onDraw()帚戳!

 @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        drawFllower(canvas, fllowers1);
        drawFllower(canvas, fllowers2);
        drawFllower(canvas, fllowers3);
    }

OK,再看下drawFllower()

 private void drawFllower(Canvas canvas, List<Fllower> fllowers) {
        for (Fllower fllower : fllowers) {
            float[] pos = new float[2];
            canvas.drawPath(fllower.getPath(), mPaint);
            pathMeasure.setPath(fllower.getPath(), false);
            pathMeasure.getPosTan(height * fllower.getValue(), pos, null);
            canvas.drawBitmap(mBitmap, pos[0], pos[1] - top, null);
        }
    }

首先儡首,遍歷一個(gè)Fllower集合片任,然后把每個(gè)Fllower所屬的Path畫出來,就是上面藍(lán)色的曲線蔬胯,然后很眼熟了吧对供,給PathMeasure設(shè)置Path對(duì)象,然后呢,就是重點(diǎn)啦产场!height是屏幕的高度鹅髓,fllower.getValue()也是一個(gè)百分比,從0-1京景,和前面的Animator作用相同窿冯,這句代碼就是說,我要距離為height * fllower.getValue()處的點(diǎn)的坐標(biāo)确徙,給我放在pos里面醒串!

好了,點(diǎn)的坐標(biāo)都有了米愿,剩下的還需要說么...

不行了厦凤,再不回家,就真回不去了育苟,拜拜较鼓,同學(xué)們

更多參考資料

尊重原創(chuàng),轉(zhuǎn)載請(qǐng)注明:From 凱子哥(http://blog.csdn.net/zhaokaiqiang1992) 侵權(quán)必究违柏!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末博烂,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子漱竖,更是在濱河造成了極大的恐慌禽篱,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,406評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件馍惹,死亡現(xiàn)場(chǎng)離奇詭異躺率,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)万矾,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門悼吱,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人良狈,你說我怎么就攤上這事后添。” “怎么了薪丁?”我有些...
    開封第一講書人閱讀 163,711評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵遇西,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我严嗜,道長(zhǎng)粱檀,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,380評(píng)論 1 293
  • 正文 為了忘掉前任漫玄,我火速辦了婚禮茄蚯,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己第队,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,432評(píng)論 6 392
  • 文/花漫 我一把揭開白布刨秆。 她就那樣靜靜地躺著凳谦,像睡著了一般。 火紅的嫁衣襯著肌膚如雪衡未。 梳的紋絲不亂的頭發(fā)上尸执,一...
    開封第一講書人閱讀 51,301評(píng)論 1 301
  • 那天,我揣著相機(jī)與錄音缓醋,去河邊找鬼如失。 笑死,一個(gè)胖子當(dāng)著我的面吹牛送粱,可吹牛的內(nèi)容都是我干的褪贵。 我是一名探鬼主播,決...
    沈念sama閱讀 40,145評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼抗俄,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼脆丁!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起动雹,我...
    開封第一講書人閱讀 39,008評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤槽卫,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后胰蝠,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體歼培,經(jīng)...
    沈念sama閱讀 45,443評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,649評(píng)論 3 334
  • 正文 我和宋清朗相戀三年茸塞,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了躲庄。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,795評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡翔横,死狀恐怖读跷,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情禾唁,我是刑警寧澤效览,帶...
    沈念sama閱讀 35,501評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站荡短,受9級(jí)特大地震影響丐枉,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜掘托,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,119評(píng)論 3 328
  • 文/蒙蒙 一瘦锹、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦弯院、人聲如沸辱士。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽颂碘。三九已至,卻和暖如春椅挣,著一層夾襖步出監(jiān)牢的瞬間头岔,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工鼠证, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留峡竣,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,899評(píng)論 2 370
  • 正文 我出身青樓量九,卻偏偏與公主長(zhǎng)得像适掰,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子娩鹉,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,724評(píng)論 2 354

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

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,106評(píng)論 25 707
  • 在iOS中隨處都可以看到絢麗的動(dòng)畫效果攻谁,實(shí)現(xiàn)這些動(dòng)畫的過程并不復(fù)雜,今天將帶大家一窺ios動(dòng)畫全貌弯予。在這里你可以看...
    每天刷兩次牙閱讀 8,488評(píng)論 6 30
  • 有一個(gè)人際關(guān)系和金錢后盾都很雄厚的父親固然好戚宦,自己可以省去很多麻煩,少走不少?gòu)澛沸饽郏约赫嬲L(zhǎng)遠(yuǎn)的人生路還是要自己...
    李翊洺赫閱讀 272評(píng)論 0 0
  • 下了60多公里的高速路段受楼,公交車在這城區(qū)繞了一圈,才開往終點(diǎn)汽車站呼寸。 我正想著這樣的繞城有些爛尾艳汽,作為連接北京城和...
    果二閱讀 472評(píng)論 0 0
  • 花枝搖曳風(fēng)來嬌, 東風(fēng)吹開西風(fēng)搖对雪。 若生枝頭有心肺河狐, 識(shí)得東南和北西。
    行者無痕閱讀 236評(píng)論 0 2