android:自定義絢麗的加載進(jìn)度動(dòng)畫

轉(zhuǎn)載請(qǐng)注明出處

1、前言

在我們的項(xiàng)目中涮俄,很多場(chǎng)景需要使用加載進(jìn)度動(dòng)畫蛉拙,如網(wǎng)絡(luò)請(qǐng)求,數(shù)據(jù)加載等彻亲。

現(xiàn)在市面大多數(shù)app都有擁有自己獨(dú)特風(fēng)格的加載動(dòng)畫孕锄,而不是谷歌為我們提供的菊花圈。一個(gè)絢麗美觀的加載動(dòng)畫可以消除用戶的等待焦慮苞尝。本文主要介紹利用自定義view打造一個(gè)絢麗的加載動(dòng)畫畸肆。

先看效果圖:


2、動(dòng)畫分析

從效果圖中宙址,我們可以把整個(gè)加載動(dòng)畫拆分成以下4個(gè)功能點(diǎn):

  1. 畫指定數(shù)目的環(huán)繞圓環(huán)
  2. 圓環(huán)旋轉(zhuǎn)動(dòng)畫
  3. 旋轉(zhuǎn)過(guò)程圓環(huán)聚攏
  4. 旋轉(zhuǎn)過(guò)程圓環(huán)收縮

3轴脐、代碼實(shí)現(xiàn)

3.1 自定義屬性

我們需要自定義倆個(gè)屬性:圓點(diǎn)個(gè)數(shù)dot_count、圓點(diǎn)顏色dot_color
代碼如下:

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <declare-styleable name="ProgressView">
        <attr name="dot_color" format="color"/>
        <attr name="dot_count" format="integer"/>
    </declare-styleable>

</resources>

3.2 獲取布局文件中設(shè)置好的自定義屬性

我們需要在java代碼中獲取在xml布局文件中設(shè)置的自定義屬性:

    public ProgressView(Context context, AttributeSet attrs, int defStyleAttr)
    {
        super(context, attrs, defStyleAttr);

        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ProgressView);
        mDotColor = ta.getColor(R.styleable.ProgressView_dot_color, mDotColor);
        mDotCount = ta.getInt(R.styleable.ProgressView_dot_count, mDotCount);
        ta.recycle();

    }

我們的布局屬性全部?jī)?chǔ)存在構(gòu)造器的attrs中抡砂,通過(guò)context.obtainStyledAttributes(attrs, R.styleable.ProgressView)方法即可獲取到設(shè)置的自定義屬性大咱,記得獲取完成后調(diào)用recycle()回收資源.

3、 初始化

在我們的自定義view ProgressView的構(gòu)造器中進(jìn)行初始化工作注益。

       mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setStyle(Style.FILL_AND_STROKE);
        mPaint.setColor(mDotColor);

        // 屏幕適配碴巾,轉(zhuǎn)化圓環(huán)半徑,小點(diǎn)半徑
        mRingRadius = DensityUtils.dp2px(getContext(), mRingRadius);
        mDotRadius = DensityUtils.dp2px(getContext(), mDotRadius);
        mOriginalDotRadius = mDotRadius;

初始化畫筆丑搔,設(shè)置顏色厦瓢,抗鋸齒,通過(guò)setStyle(Style.FILL_AND_STROKE)設(shè)置畫筆實(shí)心啤月。

設(shè)置小圓離中心的距離mRingRadius煮仇,小圓半徑mDotRadius,因?yàn)閯?dòng)畫工程中小圓半徑會(huì)變化顽冶,所以用一個(gè)變量mOriginalDotRadius來(lái)保存小圓半徑的初始值(用來(lái)計(jì)算變化中的小圓半徑)欺抗。

其中DensityUtils.dp2px()方法是根據(jù)屏幕像素密度將像dp值轉(zhuǎn)化為像素。
具體代碼:

public static int dp2px(Context context, float dp)
    {
        return (int) TypedValue.applyDimension(
                        TypedValue.COMPLEX_UNIT_DIP, 
                        dp, 
                        context.getResources().getDisplayMetrics());
    }

初始化動(dòng)畫

    private void initAnimatior()
    {
        mAnimator = ValueAnimator.ofInt(0, 359);
        mAnimator.setDuration(4000);
        mAnimator.setRepeatCount(-1);
        mAnimator.setRepeatMode(ValueAnimator.INFINITE);
        mAnimator.setInterpolator(new LinearInterpolator());
    }

我們用ValueAnimator來(lái)動(dòng)態(tài)計(jì)算當(dāng)前旋轉(zhuǎn)角度mCurrentAngle强重,變化值從0到359更換竖共,其他設(shè)置都很簡(jiǎn)單,比如設(shè)置動(dòng)畫時(shí)間跌穗,重復(fù)次數(shù)-1(表示無(wú)限循環(huán))偷霉,注意這行代碼mAnimator.setInterpolator(new LinearInterpolator()),設(shè)置動(dòng)畫差值器為線性勻速倘要,這個(gè)值改變后會(huì)改變動(dòng)畫效果圾亏。

   mAnimator.addUpdateListener(new AnimatorUpdateListener()
        {
            @Override
            public void mAnimator(ValueAnimator animation)
            {
                mCurrentAngle = (int) animation.getAnimatedValue();

                invalidate();
            }
        });

給我們的mAnimator設(shè)置監(jiān)聽(tīng)十拣,在mAnimator()方法中將當(dāng)前計(jì)算出來(lái)的值賦值給mCurrentAngle,再調(diào)用invalidate()重繪頁(yè)面志鹃,此時(shí)view會(huì)執(zhí)行ondraw方法夭问,我們這個(gè)動(dòng)畫的原理就是,動(dòng)態(tài)更改mCurrentAngle的值曹铃,不斷重繪缰趋,稍后講解怎么根據(jù)mCurrentAngle繪圖。

4陕见、 重新調(diào)整小球到中心點(diǎn)得距離

一個(gè)好的自定義view必須提供完美的兼容性秘血,有時(shí)候,我們可能在布局文件了設(shè)置了view的大小评甜,如果view的長(zhǎng)寬小于我們代碼設(shè)值得小圓點(diǎn)離中心點(diǎn)得距離mRingRadius的倆倍灰粮,小球?qū)?huì)繪制在視圖之外,導(dǎo)致看不到忍坷。所以我們?cè)?code>onLayout方法中調(diào)整mRingRadius粘舟,這里計(jì)算寬高需要扣除內(nèi)邊距。

因?yàn)樵趘iew的繪制流程onLayout()中佩研,可以獲取到view的實(shí)際寬高蓖乘,所以我們把調(diào)整代碼放在這里,以下是具體代碼:

   @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom)
    {
        super.onLayout(changed, left, top, right, bottom);

        // 重設(shè)圓環(huán)半徑韧骗,防止超出視圖大小
        int effectiveWidth = getWidth() - getPaddingLeft() - getPaddingRight();
        int effectiveHeight = getHeight() - getPaddingBottom() - getPaddingTop();
        int maxRadius = Math.min(effectiveWidth / 2, effectiveHeight / 2) - mDotRadius;

        mRingRadius = mRingRadius > maxRadius ? maxRadius : mRingRadius;
        mOriginalRingRadius = mRingRadius;
    }

5嘉抒、繪制小球

5.1 小球坐標(biāo)分析

假設(shè)坐標(biāo)軸y軸向上方向?yàn)?度,小球的當(dāng)前角度angle可以計(jì)算出小球所在的坐標(biāo)袍暴,計(jì)算方法如下圖:

這里寫圖片描述

在我們這些侍,圓點(diǎn)坐標(biāo)是view的中心點(diǎn),即寬高的一半政模。

    private void drawDot(Canvas canvas, double angle)
    {
        //根據(jù)當(dāng)前角度獲取x岗宣、y坐標(biāo)點(diǎn)
        float x = (float) (getWidth() / 2 + mRingRadius * Math.sin(angle));
        float y = (float) (getHeight() / 2 - mRingRadius * Math.cos(angle));
        
        //繪制圓
        canvas.drawCircle(x, y, mDotRadius, mPaint);
    }

5.2 小球縮小聚攏實(shí)現(xiàn)

將小球到中心的距離mRingRadius縮小就達(dá)到了聚攏的效果,同理縮小小球半徑mDotRadius就可以改變小球大小淋样。我們根據(jù)當(dāng)前旋轉(zhuǎn)角度mCurrentAngle進(jìn)行變化耗式。

為了方便計(jì)算,我們封裝一個(gè)估值器方法:

    private Integer evaluate(float fraction, Integer startValue, Integer endValue)
    {
        int startInt = startValue;
        return (int) (startInt + fraction * (endValue - startInt));
    }

這個(gè)方法在安卓動(dòng)畫計(jì)算中很常用趁猴,實(shí)現(xiàn)還是很簡(jiǎn)單的刊咳,傳入三個(gè)參數(shù),含義如下:

  • fraction:估值器的值儡司,大小從0-1變化娱挨,控制我們最終值變化的變量
  • startValue:起始值,當(dāng)fraction為0時(shí)計(jì)算得出的值
  • endValue:最終值捕犬,當(dāng)fraction為1時(shí)計(jì)算得出的值

下面講解如何通過(guò)該方法mCurrentAngle計(jì)算mRingRadius

我們需要一個(gè)fraction變量來(lái)控制mRingRadius的最終值跷坝,前面說(shuō)了酵镜,變量是當(dāng)前旋轉(zhuǎn)的角度mCurrentAngle。那么如何一個(gè)0-360的mCurrentAngle將轉(zhuǎn)為為一個(gè)0-1的值呢柴钻?
倆行代碼搞定:

        float fraction = 1.0f * mCurrentAngle / 180 - 1;
        fraction = Math.abs(fraction);

這樣淮韭,當(dāng)mCurrentAngle從0到180度變化時(shí),fraction從1到0贴届,隨著mCurrentAngle從180變化到360時(shí)fraction繼續(xù)從1變化到0缸濒,如此循環(huán)往復(fù)。得到了我們估值器的估值變量fraction粱腻。
我們mRingRadius的變化是從原始值減小到一半,mDotRadius的變化是從原始值減小到4/5斩跌。
我們打印下mRingRadius變化情況绍些。

這里寫圖片描述

6、完整代碼

public class ProgressView extends View
{
    private int mDotCount = 5; // 圓點(diǎn)個(gè)數(shù)
    private int mDotColor = 0xFFFF9966;// 圓點(diǎn)顏色

    private Paint mPaint;

    private int mRingRadius = 50;// 圓環(huán)半徑耀鸦,單位dp
    private int mOriginalRingRadius;// 保存的原始圓環(huán)半徑柬批,單位dp
    private int mDotRadius = 7; // 小點(diǎn)半徑,單位dp
    private int mOriginalDotRadius; // 保存的原始小點(diǎn)半徑袖订,單位dp

    private int mCurrentAngle = 0; // 當(dāng)前旋轉(zhuǎn)的角度

    private ValueAnimator mAnimator;// 旋轉(zhuǎn)動(dòng)畫

    public ProgressView(Context context, AttributeSet attrs, int defStyleAttr)
    {
        super(context, attrs, defStyleAttr);

        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ProgressView);
        mDotColor = ta.getColor(R.styleable.ProgressView_dot_color, mDotColor);
        mDotCount = ta.getInt(R.styleable.ProgressView_dot_count, mDotCount);
        ta.recycle();

        init();
    }

    public ProgressView(Context context, AttributeSet attrs)
    {
        this(context, attrs, 0);
    }

    public ProgressView(Context context)
    {
        this(context, null);
    }

    private void init()
    {
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setStyle(Style.FILL_AND_STROKE);
        mPaint.setColor(mDotColor);

        // 屏幕適配氮帐,轉(zhuǎn)化圓環(huán)半徑,小點(diǎn)半徑
        mRingRadius = DensityUtils.dp2px(getContext(), mRingRadius);
        mDotRadius = DensityUtils.dp2px(getContext(), mDotRadius);
        mOriginalDotRadius = mDotRadius;

        initAnimatior();
    }

    private void initAnimatior()
    {
        mAnimator = ValueAnimator.ofInt(0, 359);
        mAnimator.setDuration(4000);
        mAnimator.setRepeatCount(-1);
        mAnimator.setRepeatMode(ValueAnimator.INFINITE);
        mAnimator.setInterpolator(new LinearInterpolator());

        mAnimator.addUpdateListener(new AnimatorUpdateListener()
        {
            @Override
            public void onAnimationUpdate(ValueAnimator animation)
            {
                mCurrentAngle = (int) animation.getAnimatedValue();

                invalidate();
            }
        });
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom)
    {
        super.onLayout(changed, left, top, right, bottom);

        // 重設(shè)圓環(huán)半徑洛姑,防止超出視圖大小
        int effectiveWidth = getWidth() - getPaddingLeft() - getPaddingRight();
        int effectiveHeight = getHeight() - getPaddingBottom() - getPaddingTop();
        int maxRadius = Math.min(effectiveWidth / 2, effectiveHeight / 2) - mDotRadius;

        mRingRadius = mRingRadius > maxRadius ? maxRadius : mRingRadius;
        mOriginalRingRadius = mRingRadius;
    }

    @Override
    protected void onDraw(Canvas canvas)
    {
        // 根據(jù)小球總數(shù)平均分配整個(gè)圓上沐,得到每個(gè)小球的間隔角度
        double cellAngle = 360 / mDotCount;

        for (int i = 0; i < mDotCount; i++)
        {
            double ange = i * cellAngle + mCurrentAngle;

            // 根據(jù)當(dāng)前角度計(jì)算小球到圓心的距離
            calculateRadiusFromProgress();

            // 根據(jù)角度繪制單個(gè)小球
            drawDot(canvas, ange * 2 * Math.PI / 360);
        }
    }

    /**
     * 根據(jù)當(dāng)前旋轉(zhuǎn)角度計(jì)算mRingRadius、mDotRadius的值
     * mCurrentAngle:   0 - 180 - 360
     * mRingRadius:     最小 - 最大 - 最小
     * @author 漆可
     * @date 2016-6-17 下午3:04:35
     */
    private void calculateRadiusFromProgress()
    {
        float fraction = 1.0f * mCurrentAngle / 180 - 1;
        fraction = Math.abs(fraction);

        mRingRadius = evaluate(fraction, mOriginalRingRadius, mOriginalRingRadius * 2 / 4);
        mDotRadius = evaluate(fraction, mOriginalDotRadius, mOriginalDotRadius * 4 / 5);
    }

    // fraction:當(dāng)前的估值器計(jì)算值,startValue:起始值,endValue:終點(diǎn)值
    private Integer evaluate(float fraction, Integer startValue, Integer endValue)
    {
        return (int) (startValue + fraction * (endValue - startValue));
    }

    @Override
    protected void onAttachedToWindow()
    {
        super.onAttachedToWindow();
        startAnimation();
    }

    private void drawDot(Canvas canvas, double angle)
    {
        // 根據(jù)當(dāng)前角度獲取x楞艾、y坐標(biāo)點(diǎn)
        float x = (float) (getWidth() / 2 + mRingRadius * Math.sin(angle));
        float y = (float) (getHeight() / 2 - mRingRadius * Math.cos(angle));

        // 繪制圓
        canvas.drawCircle(x, y, mDotRadius, mPaint);
    }

    public void startAnimation()
    {
        mAnimator.start();
    }

    public void stopAnimation()
    {
        mAnimator.end();
    }

    //銷毀頁(yè)面時(shí)停止動(dòng)畫
    @Override
    protected void onDetachedFromWindow()
    {
        super.onDetachedFromWindow();
        stopAnimation();
    }
}

最后参咙,奉上demo下載地址:http://download.csdn.net/detail/q649381130/9552643

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市硫眯,隨后出現(xiàn)的幾起案子蕴侧,更是在濱河造成了極大的恐慌,老刑警劉巖两入,帶你破解...
    沈念sama閱讀 217,185評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件净宵,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡裹纳,警方通過(guò)查閱死者的電腦和手機(jī)择葡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)剃氧,“玉大人刁岸,你說(shuō)我怎么就攤上這事∷遥” “怎么了虹曙?”我有些...
    開(kāi)封第一講書人閱讀 163,524評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵迫横,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我酝碳,道長(zhǎng)矾踱,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 58,339評(píng)論 1 293
  • 正文 為了忘掉前任疏哗,我火速辦了婚禮呛讲,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘返奉。我一直安慰自己贝搁,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,387評(píng)論 6 391
  • 文/花漫 我一把揭開(kāi)白布芽偏。 她就那樣靜靜地躺著雷逆,像睡著了一般。 火紅的嫁衣襯著肌膚如雪污尉。 梳的紋絲不亂的頭發(fā)上膀哲,一...
    開(kāi)封第一講書人閱讀 51,287評(píng)論 1 301
  • 那天,我揣著相機(jī)與錄音被碗,去河邊找鬼某宪。 笑死,一個(gè)胖子當(dāng)著我的面吹牛锐朴,可吹牛的內(nèi)容都是我干的兴喂。 我是一名探鬼主播,決...
    沈念sama閱讀 40,130評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼焚志,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼瞻想!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起娩嚼,我...
    開(kāi)封第一講書人閱讀 38,985評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤蘑险,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后岳悟,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體佃迄,經(jīng)...
    沈念sama閱讀 45,420評(píng)論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,617評(píng)論 3 334
  • 正文 我和宋清朗相戀三年贵少,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了呵俏。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,779評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡滔灶,死狀恐怖普碎,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情录平,我是刑警寧澤麻车,帶...
    沈念sama閱讀 35,477評(píng)論 5 345
  • 正文 年R本政府宣布缀皱,位于F島的核電站,受9級(jí)特大地震影響动猬,放射性物質(zhì)發(fā)生泄漏啤斗。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,088評(píng)論 3 328
  • 文/蒙蒙 一赁咙、第九天 我趴在偏房一處隱蔽的房頂上張望钮莲。 院中可真熱鬧,春花似錦彼水、人聲如沸崔拥。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,716評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)链瓦。三九已至,卻和暖如春叛赚,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背稽揭。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 32,857評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工俺附, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人溪掀。 一個(gè)月前我還...
    沈念sama閱讀 47,876評(píng)論 2 370
  • 正文 我出身青樓事镣,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親揪胃。 傳聞我的和親對(duì)象是個(gè)殘疾皇子璃哟,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,700評(píng)論 2 354

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