仿360手機助手下載按鈕

代碼地址以下:
http://www.demodashi.com/demo/12833.htmlhtml

最近在學習android的高級view的繪製,再結合值動畫的數據上的改變,本身擼了個360手機助手的下載按鈕。先看下原版的360手機助手的下載按鈕是長啥樣子吧:android

360下載按鈕效果圖.gif

1、運行效果:

再來看看本身demo吧,大家盡情的吐槽吧,哈哈:
360downSimple.gifcanvas

裏面的細節問題還會不斷地更改的,gif的動態圖是有些快的,這是由於簡書要求gif的大小了,這個也冒得辦法啊 。因此想看真是效果的筒子們,能夠去看demo哈。api

完善後的效果圖.gif

細心的朋友可能發現loading狀態下左邊幾個運動圓的最高點和最低點都越界了,這是由於在規定正弦函數的最高點時沒考慮圓的半徑的長度,所以近兩天作了點修改了,效果圖以下:app

修改loading狀態下的運動點最高點和最低點.gif

2、實現細節分析步驟圖:

我們的整個過程能夠分爲這麼幾個狀態,在這裏我用枚舉類進行了概括:ide

public enum Status {
        Normal, Start, Pre, Expand, Load, Complete;
 }

Normal(還沒進行開始的狀態,也就是咱們的默認狀態,也就是咱們還沒執行onTouch的時候了):函數

normal狀態.png

Start(點擊onTouch改變爲該狀態):學習

@Override
public boolean onTouchEvent(MotionEvent event) {
    final int action = MotionEventCompat.getActionMasked(event);
    //擡起的時候去改變status
    if (action == MotionEvent.ACTION_UP) {
        status = Status.Start;
        startAnimation(collectAnimator);
    }
    return true;
}

那我們再來看看collectAnimator作了些什麼呢:動畫

collectAnimator = new Animation() {
    @Override
    protected void applyTransformation(float interpolatedTime, Transformation t) {
        currentLength = (int) (width - width * interpolatedTime);
        if (currentLength <= height) {
            currentLength = height;
            clearAnimation();
            status = Status.Pre;
            angleAnimator.start();
        }
        invalidate();
    }
};
collectAnimator.setInterpolator(new LinearInterpolator());
collectAnimator.setDuration(collectSpeed);

其實核心的就是在這個過程當中改變了全局變量currentLength而已,此時咱們回到onDraw裏面吧,看看在Start狀態下currentLength都作了些什麼:ui

@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    if (status == Status.Normal || status == Status.Start) {
        float start = (float) (width * 1.0 / 2 - currentLength * 1.0 / 2);
        canvas.drawRoundRect(start, 0, (float) (width * 1.0 / 2 + currentLength * 1.0 / 2), height, 90, 90, bgPaint);
        Paint.FontMetrics fontMetrics = textPaint.getFontMetrics();
        float allHeight = fontMetrics.descent - fontMetrics.ascent;
        if (status == Status.Normal) {
            canvas.drawText("下載", (float) (width * 1.0 / 2), (float) (height * 1.0 / 2 - allHeight / 2 - fontMetrics.ascent), textPaint);
        }
    } else if (status == Status.Pre) {
        canvas.drawCircle((float) (width * 1.0 / 2), (float) (height * 1.0 / 2), (float) (height * 1.0 / 2), bgPaint);
        canvas.save();
        canvas.rotate(angle, (float) (width * 1.0 / 2), (float) (height * 1.0 / 2));
        canvas.drawCircle((float) (width * 1.0 / 2), (float) (height * 1.0 / 2), 25, textPaint);
        canvas.drawCircle((float) (width * 1.0 / 2), (float) (height * 1.0 / 2) - 24, 15, textPaint);
        canvas.drawCircle((float) (width * 1.0 / 2 - 22), (float) ((height * 1.0 / 2) + 18 * 0.866), 15, textPaint);
        canvas.drawCircle((float) (width * 1.0 / 2 + 22), (float) ((height * 1.0 / 2) + 18 * 0.866), 15, textPaint);
        canvas.restore();
    } else if (status == Status.Expand) {
        float start = (float) (width * 1.0 / 2 - currentLength * 1.0 / 2);
        canvas.drawRoundRect(start, 0, (float) (width * 1.0 / 2 + currentLength * 1.0 / 2), height, 90, 90, bgPaint);

        canvas.save();
        canvas.translate(translateX, 0);
        canvas.drawCircle((float) (width * 1.0 / 2), (float) (height * 1.0 / 2), 25, textPaint);
        canvas.drawCircle((float) (width * 1.0 / 2), (float) (height * 1.0 / 2) - 24, 15, textPaint);
        canvas.drawCircle((float) (width * 1.0 / 2 - 22), (float) ((height * 1.0 / 2) + 18 * 0.866), 15, textPaint);
        canvas.drawCircle((float) (width * 1.0 / 2 + 22), (float) ((height * 1.0 / 2) + 18 * 0.866), 15, textPaint);
        canvas.restore();
    } else if (status == Status.Load || status == Status.Complete) {

        float start = (float) (width * 1.0 / 2 - currentLength * 1.0 / 2);
        bgPaint.setColor(progressColor);
        canvas.drawRoundRect(start, 0, (float) (width * 1.0 / 2 + currentLength * 1.0 / 2), height, 90, 90, bgPaint);
        if (progress != 100) {
            //畫中間的幾個loading的點的狀況哈
            if (fourMovePoint[0].isDraw)
                canvas.drawCircle(fourMovePoint[0].moveX, fourMovePoint[0].moveY, fourMovePoint[0].radius, textPaint);
            if (fourMovePoint[1].isDraw)
                canvas.drawCircle(fourMovePoint[1].moveX, fourMovePoint[1].moveY, fourMovePoint[1].radius, textPaint);
            if (fourMovePoint[2].isDraw)
                canvas.drawCircle(fourMovePoint[2].moveX, fourMovePoint[2].moveY, fourMovePoint[2].radius, textPaint);
            if (fourMovePoint[3].isDraw)
                canvas.drawCircle(fourMovePoint[3].moveX, fourMovePoint[3].moveY, fourMovePoint[3].radius, textPaint);
        }

        float progressRight = (float) (progress * width * 1.0 / 100);
        //在最上面畫進度
        bgPaint.setColor(bgColor);

        canvas.save();
        canvas.clipRect(0, 0, progressRight, height);
        canvas.drawRoundRect(start, 0, (float) (width * 1.0 / 2 + currentLength * 1.0 / 2), height, 90, 90, bgPaint);
        canvas.restore();

        if (progress != 100) {
            bgPaint.setColor(bgColor);
            canvas.drawCircle((float) (width - height * 1.0 / 2), (float) (height * 1.0 / 2), (float) (height * 1.0 / 2), bgPaint);
            canvas.save();
            canvas.rotate(loadAngle, (float) (width - height * 1.0 / 2), (float) (height * 1.0 / 2));
              canvas.drawCircle(width - height + 25, getCircleY(width - height + 25), 5, textPaint);
            canvas.drawCircle(width - height + 40, getCircleY(width - height + 40), 7, textPaint);
            canvas.drawCircle(width - height + 60, getCircleY(width - height + 60), 9, textPaint);
            canvas.drawCircle(width - height + 90, getCircleY(width - height + 90), 11, textPaint);
            canvas.restore();
        }
        //中間的進度文字
        Paint.FontMetrics fontMetrics = textPaint.getFontMetrics();
        float allHeight = fontMetrics.descent - fontMetrics.ascent;
        canvas.drawText(progress + "%", (float) (width * 1.0 / 2), (float) (height * 1.0 / 2 - allHeight / 2 - fontMetrics.ascent), textPaint);
    }
}

爲了便於咱們分析每個狀態,咱們就看下每一個狀態下的繪製動做吧:

if (status == Status.Normal || status == Status.Start) {
    float start = (float) (width * 1.0 / 2 - currentLength * 1.0 / 2);
    canvas.drawRoundRect(start, 0, (float) (width * 1.0 / 2 + currentLength * 1.0 / 2), height, 90, 90, bgPaint);
    Paint.FontMetrics fontMetrics = textPaint.getFontMetrics();
    float allHeight = fontMetrics.descent - fontMetrics.ascent;
    if (status == Status.Normal) {
        canvas.drawText("下載", (float) (width * 1.0 / 2), (float) (height * 1.0 / 2 - allHeight / 2 - fontMetrics.ascent), textPaint);
    }
}

你們看到變量currentLength了沒,其實這裏就是去改變背景的right座標,正好上面動畫裏面也是從width減少的一個值,那麼此時的動畫你們腦海裏能想象得出來了吧:
start效果圖.gif

Start狀態結束都就是進入到Pre狀態了:
上面collectAnimator動畫結束後啓動的動畫是:angleAnimator了,
咱們再去看看該動畫都作了些啥:

angleAnimator = ValueAnimator.ofFloat(0, 1);
angleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
        angle += 10;
        invalidate();
    }
});

改變的仍是全局的變量angle,再來看看該變量在onDraw方法裏面都作了些啥吧:

else if (status == Status.Pre) {
    canvas.drawCircle((float) (width * 1.0 / 2), (float) (height * 1.0 / 2), (float) (height * 1.0 / 2), bgPaint);
    canvas.save();
    canvas.rotate(angle, (float) (width * 1.0 / 2), (float) (height * 1.0 / 2));
    canvas.drawCircle((float) (width * 1.0 / 2), (float) (height * 1.0 / 2), 25, textPaint);
    canvas.drawCircle((float) (width * 1.0 / 2), (float) (height * 1.0 / 2) - 24, 15, textPaint);
    canvas.drawCircle((float) (width * 1.0 / 2 - 22), (float) ((height * 1.0 / 2) + 18 * 0.866), 15, textPaint);
    canvas.drawCircle((float) (width * 1.0 / 2 + 22), (float) ((height * 1.0 / 2) + 18 * 0.866), 15, textPaint);
    canvas.restore();
}

畫了幾個圓,而後經過上面的angle變量來旋轉canvas,並且幾個圓的圓心都與view的中心點有關,所以你們從示例圖中應該看出來了:
pre效果圖.gif

pre狀態結束後,就是Expand狀態了,你們能夠看pre狀態下動畫結束的代碼:

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

    }

    @Override
    public void onAnimationEnd(Animator animation) {
        status = Status.Expand;
        angleAnimator.cancel();
        startAnimation(tranlateAnimation);
    }

    @Override
    public void onAnimationCancel(Animator animation) {

    }

    @Override
    public void onAnimationRepeat(Animator animation) {

    }
});

能夠看出下一個動畫tranlateAnimation了,仍是同樣定位到該動畫的代碼吧,看看都作了些啥:

tranlateAnimation = new Animation() {
    @Override
    protected void applyTransformation(float interpolatedTime, Transformation t) {
        currentLength = (int) (height + (width - height) * interpolatedTime);
        translateX = (float) ((width * 1.0 / 2 - height * 1.0 / 2) * interpolatedTime);
        invalidate();
    }
};

能夠看出此時改變的全局變量有兩個:currentLengthtranslateX,想必你們知道currentLength是什麼做用了吧,下面就來看看onDraw吧:

else if (status == Status.Expand) {
    float start = (float) (width * 1.0 / 2 - currentLength * 1.0 / 2);
    canvas.drawRoundRect(start, 0, (float) (width * 1.0 / 2 + currentLength * 1.0 / 2), height, 90, 90, bgPaint);

    canvas.save();
    canvas.translate(translateX, 0);
    canvas.drawCircle((float) (width * 1.0 / 2), (float) (height * 1.0 / 2), 25, textPaint);
    canvas.drawCircle((float) (width * 1.0 / 2), (float) (height * 1.0 / 2) - 24, 15, textPaint);
    canvas.drawCircle((float) (width * 1.0 / 2 - 22), (float) ((height * 1.0 / 2) + 18 * 0.866), 15, textPaint);
    canvas.drawCircle((float) (width * 1.0 / 2 + 22), (float) ((height * 1.0 / 2) + 18 * 0.866), 15, textPaint);
    canvas.restore();
}

一個是改變背景的right座標,再個就是canvas.translate幾個中心點的圓了:
expand效果圖.gif

expand狀態結束後就是正式進入到下載狀態了,這裏的枚舉我定義是Load,
看下expand結束的動畫代碼吧:

tranlateAnimation.setAnimationListener(new Animation.AnimationListener() {
    @Override
    public void onAnimationStart(Animation animation) {
    }

    @Override
    public void onAnimationEnd(Animation animation) {
        clearAnimation();
        status = Status.Load;
        clearAnimation();
        loadRotateAnimation.start();
        movePointAnimation.start();
    }

    @Override
    public void onAnimationRepeat(Animation animation) {

    }
});

你們能夠看到該處有兩個動畫的啓動了(loadRotateAnimation.start()movePointAnimation.start()),說明此處有兩個動畫在同時執行罷了,先來看loadRotateAnimation動畫裏面都作了些啥吧:

loadRotateAnimation = ValueAnimator.ofFloat(0, 1);
loadRotateAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
        loadAngle += rightLoadingSpeed;
        if (loadAngle > 360) {
            loadAngle = loadAngle - 360;
        }
        invalidate();
    }
});
loadRotateAnimation.setDuration(Integer.MAX_VALUE);

仍是一個角度改變的動畫啊,那就看看loadAngle是改變誰的動畫吧,仍是照常咱們進入到onDraw方法吧:

if (progress != 100) {
    bgPaint.setColor(bgColor);
    canvas.drawCircle((float) (width - height * 1.0 / 2), (float) (height * 1.0 / 2), (float) (height * 1.0 / 2), bgPaint);
    canvas.save();
    canvas.rotate(loadAngle, (float) (width - height * 1.0 / 2), (float) (height * 1.0 / 2));
    canvas.drawCircle(width - height + 25, getCircleY(width - height + 25), 5, textPaint);
    canvas.drawCircle(width - height + 40, getCircleY(width - height + 40), 7, textPaint);
    canvas.drawCircle(width - height + 60, getCircleY(width - height + 60), 9, textPaint);
    canvas.drawCircle(width - height + 90, getCircleY(width - height + 90), 11, textPaint);
    canvas.restore();
}

仍是一個圓的旋轉啊,其實這幾個點是有規律去繪製的,他們幾個圓心應該是內圓的弧度上的,而且半徑是依次增大的。這裏調了getCircleY()方法,該方法就是算圓弧上幾個點的y座標。

/**
 * 根據x座標算出圓的y座標
 *
 * @param cx:點的圓心x座標
 * @return
 */
private float getCircleY(float cx) {
    float cy = (float) (height * 1.0 / 2 - Math.sqrt((height * 1.0 / 2 - dp2px(7)) * (height * 1.0 / 2 - dp2px(7)) - ((width - height * 1.0 / 2) - cx) * ((width - height * 1.0 / 2) - cx)));
    return cy;
}

這裏看似方法很複雜,其實就是初中定義圓的方程式:(x-cx)^2+(y-cy)^2=r^2

下面再來看看movePointAnimation動畫都作了些啥吧:

fourMovePoint[0] = new MovePoint(dp2px(4), (float) ((width - height / 2) * 0.88), 0);
fourMovePoint[1] = new MovePoint(dp2px(3), (float) ((width - height / 2) * 0.85), 0);
fourMovePoint[2] = new MovePoint(dp2px(2), (float) ((width - height / 2) * 0.80), 0);
fourMovePoint[3] = new MovePoint(dp2px(5), (float) ((width - height / 2) * 0.75), 0);

movePointAnimation = ValueAnimator.ofFloat(0, 1);
movePointAnimation.setRepeatCount(ValueAnimator.INFINITE);
movePointAnimation.setInterpolator(new LinearInterpolator());
movePointAnimation.setDuration(leftLoadingSpeed);
movePointAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
        float value = animation.getAnimatedFraction();
        fourMovePoint[0].moveX = fourMovePoint[0].startX - fourMovePoint[0].startX * value;
        if (fourMovePoint[0].moveX <= height / 2) {
            fourMovePoint[0].isDraw = false;
        }
        fourMovePoint[1].moveX = fourMovePoint[1].startX - fourMovePoint[0].startX * value;
        if (fourMovePoint[1].moveX <= height / 2) {
            fourMovePoint[1].isDraw = false;
        }
        fourMovePoint[2].moveX = fourMovePoint[2].startX - fourMovePoint[0].startX * value;
        if (fourMovePoint[2].moveX <= height / 2) {
            fourMovePoint[2].isDraw = false;
        }
        fourMovePoint[3].moveX = fourMovePoint[3].startX - fourMovePoint[0].startX * value;
        if (fourMovePoint[3].moveX <= height / 2) {
            fourMovePoint[3].isDraw = false;
        }
        fourMovePoint[0].moveY = drawMovePoints(fourMovePoint[0].moveX);
        fourMovePoint[1].moveY = drawMovePoints(fourMovePoint[1].moveX);
        fourMovePoint[2].moveY = drawMovePoints(fourMovePoint[2].moveX);
        fourMovePoint[3].moveY = drawMovePoints(fourMovePoint[3].moveX);
        Log.d("TAG", "fourMovePoint[0].moveX:" + fourMovePoint[0].moveX + ",fourMovePoint[0].moveY:" + fourMovePoint[0].moveY);
    }
});

movePointAnimation.addListener(new Animator.AnimatorListener() {
    @Override
    public void onAnimationStart(Animator animation) {
        fourMovePoint[3].isDraw = true;
        fourMovePoint[2].isDraw = true;
        fourMovePoint[1].isDraw = true;
        fourMovePoint[0].isDraw = true;
    }

    @Override
    public void onAnimationEnd(Animator animation) {

    }

    @Override
    public void onAnimationCancel(Animator animation) {

    }

    @Override
    public void onAnimationRepeat(Animator animation) {
        fourMovePoint[3].isDraw = true;
        fourMovePoint[2].isDraw = true;
        fourMovePoint[1].isDraw = true;
        fourMovePoint[0].isDraw = true;
    }
});

這裏首先定義了四個MovePoint,分別定義了他們的半徑,圓心,而後在該動畫裏面不斷地改變四個point的圓心,其實這裏核心就是如何求出四個點運行的軌跡了,把軌跡弄出來一切就都呈現出來了,能夠看看該動畫的onAnimationUpdate方法裏面調用的drawMovePoints方法:

/**
 * 這裏是在load狀況下獲取幾個點運動的軌跡數學函數
 *
 * @param moveX
 * @return
 */
private float drawMovePoints(float moveX) {
    float moveY = (float) (height / 2 + (height / 2 - fourMovePoint[3].radius) * Math.sin(4 * Math.PI * moveX / (width - height) + height / 2));
    return moveY;
}

這裏就是一個數學裏面常常用的正弦函數了,求出週期、x軸上的偏移量、y軸上的便宜量、頂點,還有一個注意點,該處求頂點的時候,須要減去這幾個圓中的最大半徑,以前我就是沒注意到這點,最後出來的軌跡就是一個圓會跑到view的外面了。效果圖以下:
load效果圖.gif

最後一個狀態就是Complete了,也就是當前的進度到了100,可見代碼:

/**
     * 進度改變的方法
     *
     * @param progress(當前進度)
     */
public void setProgress(int progress) {
    if (status != Status.Load) {
        throw new RuntimeException("your status is not loading");
    }

    if (this.progress == progress) {
        return;
    }
    this.progress = progress;
    if (onProgressUpdateListener != null) {
        onProgressUpdateListener.onChange(this.progress);
    }
    invalidate();
    if (progress == 100) {
        status = Status.Complete;
        this.stop = false;
        clearAnimation();
        loadRotateAnimation.cancel();
        movePointAnimation.cancel();
    }
}

這裏要作的就是改變狀態,中止一切動畫了,到此代碼的講解就到這裏了,趕快start起來吧。

屬性也沒怎麼整理,就抽取出了一些比較經常使用的幾個了:

屏幕快照 2017-04-01 14.21.37.png

代碼使用:

/**
 * 進度改變的方法
 * @param progress
 */
public void setProgress(int progress) {
    if (status != Status.Load) {
        throw new RuntimeException("your status is not loading");
    }

    if (this.progress == progress) {
        return;
    }
    this.progress = progress;
    if (onProgressUpdateListener != null) {
        onProgressUpdateListener.onChange(this.progress);
    }
    invalidate();
    if (progress == 100) {
        status = Status.Complete;
        this.stop = false;
        clearAnimation();
        loadRotateAnimation.cancel();
        movePointAnimation.cancel();
    }
}

/**
 * 暫停或繼續的方法
 *
 * @param stop(true:表示暫停,false:繼續)
 */
public void setStop(boolean stop) {
    if (this.stop == stop) {
        return;
    }
    this.stop = stop;
    if (stop) {
        loadRotateAnimation.cancel();
        movePointAnimation.cancel();
    } else {
        loadRotateAnimation.start();
        movePointAnimation.start();
    }
}

/**
 *設置狀態的方法
 * @param status(Down360Loading.Status.Normal:直接取消的操做)
 */
public void setStatus(Status status) {
    if (this.status == status) {
        return;
    }
    this.status = status;
    if (this.status == Status.Normal) {
        progress = 0;
        this.stop = false;
        clearAnimation();
        loadRotateAnimation.cancel();
        movePointAnimation.cancel();
    }
    invalidate();
}

3、項目文件目錄截圖:

項目結構

仿360手機助手下載按鈕

代碼地址以下:
http://www.demodashi.com/demo/12833.html

注:本文著做權歸做者,由demo大師代發,拒絕轉載,轉載須要做者受權

相關文章
相關標籤/搜索