Android自定義進度條之BeautyProgressBar

代碼一年前就已經擼好了,現在纔想起來總結。。。先放張效果圖吧。

附上代碼github地址:https://github.com/kjt666/BeautyProgressBar,支持依賴引入項目。

概述

可以看到上面實現了四種樣式的進度條,帶進度文字的長條形進度條、帶進度文字的圓形進度條、仿IOS商店的圓形進度條、兩端爲圓形的長條進度條。今天主要講的是最後一種,因爲當初實現它的時候廢了不少腦筋,也是知識涵蓋最多的一種。可能有些點寫的不全面(畢竟一年前寫的代碼,註釋也不是很多),希望大家多多包涵哈。

1、主要方法概覽

(1)、構造方法

進行一些自定義屬性的獲取和初始值定義。

(2)、private int measureHeight(int heightMeasureSpec)

測量控件高度。

(3)、 private Bitmap getSrcPic(float progress)

生成源圖像,因爲使用次數較多,所以將一些資源抽了出來,避免頻繁gc操作,耗費內存。

(4)、private Bitmap getDstPic()

獲取目標圖層圖像,初始一次就行了。

(5)、protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec)

測量設置控件大小並在測量完成後創建目標圖像

(6)、 protected synchronized void onDraw(Canvas canvas)

繪畫操作,利用生成的源圖像和目標圖像實現我們要的效果

2、自定義屬性值

(1)、progressbar_height

進度條的高度,注意,不是控件的高度,是控件裏進度條的高度(不過好像沒什麼用額,我也忘了當初爲啥要定義一個這個值)

(2)、reach_color

進度條完成進度的顏色

(3)、unreach_color

進度條未完成進度的顏色(就是進度條默認的底色)

3、源碼分析

上面方法提到的源圖像和目標圖像是這此代碼編寫的重點,自定義控件的效果主要是通過Paint類來實現的,對於Paint基礎不太熟悉的人可以去找一下資料。那麼對於這個Paint類,我用到了它的setXfermode方法和setStrokeCap方法。

構造函數

	TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.MyHoriztalProgressBar2);
        mProgressbarHeight = (int) ta.getDimension(R.styleable.MyHoriztalProgressBar2_progressbar_height, PROGRESSBAR_DEFAULT_HEIGHT);
        mReachColor = ta.getColor(R.styleable.MyHoriztalProgressBar2_reach_color, PROGRESSBAR_DEFAULT_REACH_COLOR);
        mUnReachColor = ta.getColor(R.styleable.MyHoriztalProgressBar2_unreach_color, PROGRESSBAR_DEFAULT_UNREACH_COLOR);
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setStrokeWidth(mProgressbarHeight);//設置進度條的高度
        mPaint.setStrokeCap(Paint.Cap.ROUND);//設置線冒樣式

構造函數很簡單,就是一些自定義屬性值的獲取和設置默認值。至於那個線冒,我引用一張別人的圖,大家一看就能懂。

怎麼樣,是不是一目瞭然,進度條兩端的圓形效果就是這樣實現的,其實最開始的時候我還不知道這個線冒,我是通過畫兩個半圓加一個長條組合起來,想想當初真是麻煩,費了不少功夫,所以要想搞事還是要有足夠知識的啊!

接下來再看看測量控件高度的代碼

measureHeight()

private int measureHeight(int heightMeasureSpec) {
        int height = MeasureSpec.getSize(heightMeasureSpec);
        int mode = MeasureSpec.getMode(heightMeasureSpec);
        int result = 0;
        if (mode == MeasureSpec.EXACTLY) {
            result = height;
        } else if (mode == MeasureSpec.AT_MOST) {
            result = getPaddingTop() + getPaddingBottom() + mProgressbarHeight * 2;
        }
        return result;
    }

也沒什麼難度,就是根據控件高度的 設置屬性計算出控件的 高度。

onMeasure()

protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthVal = MeasureSpec.getSize(widthMeasureSpec);
        int heightVal = measureHeight(heightMeasureSpec);

        setMeasuredDimension(widthVal, heightVal);
        mRealWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
        mDstBmp = getDstPic();//完成測量後再創建目標圖片
        mSrcBmp = Bitmap.createBitmap(getMeasuredWidth(), mProgressbarHeight, Bitmap.Config.ARGB_8888);//初始化源圖片大小
    }

 

mRealWidth,什麼叫真實寬度呢?因爲控件可能需要設置一些padding值,所以需要測量出的寬度減去控件兩端的padding值,這纔是我們需要畫出的進度條寬度。
mDstBmp,目標圖像。可能有人就有疑問了“之前就看你寫源圖像、目標圖像,這倒是是啥玩意啊?爲什麼要用到它們呢?”,你們可以先這麼理解,比如,我這有兩塊布,疊在一起,上面的布叫源圖像,下面的布叫目標圖像。這兩個圖像就是供Paint的setXfermode使用的,這個方法的目的就是設置這兩塊布的混合模式Xfermode詳解

創建目標圖像的方法

private Bitmap getDstPic() {
        //創建畫布,不用mRealWidth的原因是加入給控件設定10dp寬度,在設定10dp的pading,那麼這個mReaalWidth則爲負數,創建bitmap會報錯,同理,高度我們也是限定了的
        Bitmap bitmap = Bitmap.createBitmap(getMeasuredWidth(), mProgressbarHeight, Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);
        mPaint.setColor(mUnReachColor);
        //因爲線帽是線段兩頭的樣式,是多出來的一部分,如果不設置線帽從0開始就可以,線帽長度一般爲線的高度。
        canvas.drawLine(mProgressbarHeight, mProgressbarHeight / 2, mRealWidth - mProgressbarHeight, mProgressbarHeight / 2, mPaint);
        return bitmap;
    }

用Canvas畫線和畫圖片,它們的起始位置是不一樣額,畫圖是從圖片的左上角開始畫,可以理解爲從它的(0,0)座標開始。畫線是從線高度的一半開始畫,也就是(0,線高度/2)開始,如果我不設置從mProgressbarHeight / 2處開始畫,那麼這條線在豎直方向上只能顯示一半,這一點需要注意一下。

創建源圖像的方法

 private Bitmap getSrcPic(float progress) {
        mSrcCanvas = new Canvas(mSrcBmp);
        mPaint.setColor(mReachColor);
        if (getProgress() <= 50) {
            mSrcCanvas.drawLine(0, mProgressbarHeight / 2, progress, mProgressbarHeight / 2, mPaint);
        } else {
            mSrcCanvas.drawLine(0, mProgressbarHeight / 2, progress - mProgressbarHeight / 2, mProgressbarHeight / 2, mPaint);
        }
        return mSrcBmp;
    }

爲什麼要判斷進度值是否大小於50呢,我能說我也記不清了嗎。。。

依稀記得是因爲進度條顯示精度的問題,好像當進度在10以內(還是5)顯示的完成長度和當進度在90(還是95)剩餘的長度不一樣長,所以才做了這麼一個判斷進行一些補差。

到這裏,重要的代碼基本擼的差不多了,剩下的就是在onDraw方法中把它們組合起來了。

onDraw()方法

@Override
    protected synchronized void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        progressX = mRealWidth * getProgress() / 100;
        mSrcBmp = getSrcPic(progressX);
        int layer = canvas.saveLayer(0, 0, getMeasuredWidth(), getMeasuredHeight(), mPaint, Canvas.ALL_SAVE_FLAG);
        canvas.translate(getPaddingLeft(), (getMeasuredHeight() - mProgressbarHeight) / 2); //畫圖和普通的畫線不一樣。畫圖是從圖片的(0,0)點開始畫的,畫線是從(0,線高度的一半)開始畫的。
        canvas.drawBitmap(mDstBmp, 0, 0, mPaint);
        mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP));
        canvas.drawBitmap(mSrcBmp, 0, 0, mPaint);
        mPaint.setXfermode(null);
        canvas.restoreToCount(layer);
    }

canvas.saveLayer(saveLayer可以爲canvas創建一個新的透明圖層,在新的圖層上繪製,並不會直接繪製到屏幕上,而會在restore之後,繪製到上一個圖層或者屏幕上(如果沒有上一個圖層)。爲什麼會需要一個新的圖層,例如在處理xfermode的時候,原canvas上的圖(包括背景)會影響src和dst的合成,這個時候,使用一個新的透明圖層是一個很好的選擇。又例如需要當前繪製的圖形都帶有一定的透明度,那麼創建一個帶有透明度的圖層,也是一個方便的選擇。)

ps

可能會有人問不使用Paint的setXfermode,直接畫兩張圖片,疊在一起不就行了嗎,對於本次這個圓角進度條來說,要考慮圖片的邊角顯示問題,自己寫個Demo看一下就懂了

總結

以上就是本次自定義View的所有內容了。自定義View並不是一個簡單的代碼組合過程,從一個想法到成品 的實現,中途可能經過很多次思路、方法、代碼的改版,不僅僅是對知識的考驗,更像是一場頭腦風暴,你要發揮大腦所有能量思考怎樣去完成一個高質量、高效率、低成本的事情。像這次例子我改了至少3次思路、代碼才實現成這個樣子。整個過程不僅學到了知識也開闊了自己的思路,可能以後再遇到類似的問題時會更好更快的想出解決辦法。最後,希望本次例子能對大家做出一些幫助吧。

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章