1. 程式人生 > >Android波紋進度條 輕鬆地讓它浪起來

Android波紋進度條 輕鬆地讓它浪起來

一、概述

最近專案來個需求,波紋進度條。想起來之前看到的一些實現,也想了一下原理啥的,就自己寫個吧。不過為了適配以後更多各種不規則的波紋進度條,因此需要能適配各種不同png圖片的波紋進度條。

1. 效果圖

no picture say a j8!

效果圖

2. 原理分析

波紋進度條,不外乎一張背景bitmap,一張進度波紋bitmap。之後則不停的向一個方向迴圈移動波紋即可。如下圖(手畫,輕噴):

原理圖

當然最關鍵的問題是如何把多餘的波紋給隱藏起來,這裡就要用到Android繪圖裡的點陣圖運算了。PorterDuffXfermode給我們提供了一種實現複雜的點陣圖運算的支援。其包含16中運算模式,如圖(這個圖網上到處都是,我是從APIDemo中截來的):

Xfermode

大概說一下,一般先畫的是DST,設定Xfermode之後畫的則是Src,我們會先繪製波紋,再繪製圖片。這裡我們可以看到,要實現Dst不需要的部分隱藏,而Src不會隱藏,則使用DstATop即可。

二、實現

自定義View實現步驟一般來說都很固定,先measure再draw即可。在這裡我大概寫一下這個波紋進度條的實現步驟:

  • measure,確定尺寸以及背景圖片
  • 計算波紋相關屬性
  • 畫水波紋
  • 設定Xfermode
  • 畫背景圖篇
  • 畫提示文字

1. onMeasure與計算

@Override
protected void onMeasure(int widthMeasureSpec, int
heightMeasureSpec) { int measuredWidth = measureWidth(widthMeasureSpec); int measuredHeight = measureHeight(heightMeasureSpec); setMeasuredDimension(measuredWidth, measuredHeight); if (null == mTmpBackground) { mIsAutoBack = true; int min = Math.min(measuredWidth, measuredHeight); mStrokeWidth = DEFAULT_STROKE_RADIO * min; float
spaceWidth = DEFAULT_SPACE_RADIO * min; // 預設背景時,線和波紋圖片間距 mWidth = (int) (min - (mStrokeWidth + spaceWidth) * 2); mHeight = (int) (min - (mStrokeWidth + spaceWidth) * 2); mBackground = autoCreateBitmap(mWidth / 2); } else { mIsAutoBack = false; mBackground = getBitmapFromDrawable(mTmpBackground); if (mBackground != null && !mBackground.isRecycled()) { mWidth = mBackground.getWidth(); mHeight = mBackground.getHeight(); } } mWaveCount = calWaveCount(mWidth, mWaveWidth); } /** * 測量view高度,如果是wrap_content,則預設是200 */ private int measureHeight(int heightMeasureSpec) { int height = 0; int mode = MeasureSpec.getMode(heightMeasureSpec); int size = MeasureSpec.getSize(heightMeasureSpec); if (mode == MeasureSpec.EXACTLY) { height = size; } else if (mode == MeasureSpec.AT_MOST) { if (null != mTmpBackground) { height = mTmpBackground.getMinimumHeight(); } else { height = 400; } } return height; } /** * 測量view寬度,如果是wrap_content,則預設是200 */ private int measureWidth(int widthMeasureSpec) { int width = 0; int mode = MeasureSpec.getMode(widthMeasureSpec); int size = MeasureSpec.getSize(widthMeasureSpec); if (mode == MeasureSpec.EXACTLY) { width = size; } else if (mode == MeasureSpec.AT_MOST) { if (null != mTmpBackground) { width = mTmpBackground.getMinimumWidth(); } else { width = 400; } } return width; } /** * 建立預設是圓形的背景 * * @param radius 半徑 * @return 背景圖 */ private Bitmap autoCreateBitmap(int radius) { Bitmap bitmap = Bitmap.createBitmap(2 * radius, 2 * radius, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); Paint p = new Paint(Paint.ANTI_ALIAS_FLAG); p.setColor(mWaveBackgroundColor); p.setStyle(Paint.Style.FILL); canvas.drawCircle(radius, radius, radius, p); return bitmap; } /** * 從drawable中獲取bitmap */ private Bitmap getBitmapFromDrawable(Drawable drawable) { if (null == drawable) { return null; } if (drawable instanceof BitmapDrawable) { return ((BitmapDrawable) drawable).getBitmap(); } try { Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); drawable.draw(canvas); return bitmap; } catch (OutOfMemoryError e) { return null; } } /** * 計算波紋數目 * * @param width 波紋圖寬度 * @param waveWidth 每條波紋的寬度 * @return 波紋數目 */ private int calWaveCount(int width, float waveWidth) { int count; if (width % waveWidth == 0) { count = (int) (width / waveWidth + 1); } else { count = (int) (width / waveWidth + 2); } return count; }

測量

測量這裡,我們先測量整個控制元件的尺寸,寫法也很固定,就是根據給的×××MeasureSpec獲得模式與尺寸(比如widthMeasureSpec,其中高2位封裝了其模式,後面的則是其尺寸),如果是使用EXACTLY指定了尺寸,則為指定尺寸,否則如果有背景則使用背景尺寸,否則指定一個固定值。

確定背景圖

然後,再根據是否有背景來決定使用的是背景還是自己繪製的一個圓。這裡mTmpBackground就是背景圖片。在初始化時候已經把背景圖片獲取到,並且重置背景為透明的,這樣就防止了重複背景的出現(而且背景會變形,醜逼)。如果沒有背景,就使用autoCreateBitmap(radius)方法繪製一個圓形,這個是我專案裡的一個樣式,所以,我就把它作為預設的效果了,就是效果圖中第一個那樣的。如果有背景圖,就把背景Drawable通過getBitmapFromDrawable(drawable)方法轉換為Bitmap即可。

計算波紋屬性

在最後呢,就是計算波紋的數量了。我根據波紋寬度與背景圖片寬度來計算波紋的個數,這裡要強調一下,實際的波紋數量一定要比背景圖片能容納的波紋數量多一個,否則在移動波紋時,會很僵硬。因此,上面會根據是否能整除寬度而指定不同的數量,如果正好能顯示整數個,則再加1,否則,要算上不能整除的1個再加1。

2. 繪製波紋與背景圖

程式碼:

/**
 * 繪製重疊的bitmap,注意:沒有背景則預設是圓形的背景,有則是背景
 *
 * @param width  背景高
 * @param height 背景寬
 * @return 帶波紋的圖
 */
private Bitmap createWaveBitmap(int width, int height) {
    Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
    Canvas canvas = new Canvas(bitmap);

    // 計算波浪位置
    int mCurY = (int) (height * (mMaxProgress - mProgress) / mMaxProgress);

    // 畫path
    mPath.reset();
    mPath.moveTo(-mDistance, mCurY);
    for (int i = 0; i < mWaveCount; i++) {
        mPath.quadTo(i * mWaveWidth + mHalfWaveWidth - mDistance, mCurY - mWaveHeight,
            i * mWaveWidth + mHalfWaveWidth * 2 - mDistance, mCurY);    // 起
        mPath.quadTo(i * mWaveWidth + mHalfWaveWidth * 3 - mDistance, mCurY + mWaveHeight,
            i * mWaveWidth + mHalfWaveWidth * 4 - mDistance, mCurY);    // 伏
    }
    mPath.lineTo(width, height);
    mPath.lineTo(0, height);
    mPath.close();
    canvas.drawPath(mPath, mWavePaint);

    mDistance += mSpeed;
    mDistance %= mWaveWidth;

    mWavePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_ATOP));
    canvas.drawBitmap(mBackground, 0, 0, mWavePaint);
    return bitmap;
}

波紋

這塊程式碼首先建立繪製波紋的canvas,之後計算波紋此時按進度百分比的起始位置y值,之後使用Path類來完成波紋的繪製。繪製完成偏移量會增加。

注意,這裡使用到二階貝塞爾曲線來繪製波紋,正如上面的for迴圈來繪製曲線,由於一個波紋寬度是一個起伏的寬度,是兩個曲線(起、伏),所以要繪製兩次,而上面的mHalfWaveWidth變數其實是1/4的波紋寬。如果大家不理解貝塞爾曲線,可以去搜一下。

背景圖

背景圖則很簡單了,在測量時我們已經確定了背景圖,只需要繪製出來即可。但在這之前一定要設定好xfermode。

3. 繪製文字與其他

文字

圖片建立完,就要繪製到View上了,同時還要繪製上文字。

@Override
protected void onDraw(Canvas canvas) {
    Bitmap bitmap = createWaveBitmap(mWidth, mHeight);
    if (mIsAutoBack) {  // 如果沒有背景,就畫預設背景
        if (null == mStrokePaint) {
            mStrokePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
            mStrokePaint.setColor(mStrokeColor);
            mStrokePaint.setStrokeWidth(mStrokeWidth);
            mStrokePaint.setStyle(Paint.Style.STROKE);
        }
        // 預設背景下先畫個邊框
        float radius = Math.min(getMeasuredWidth() / 2, getMeasuredHeight() / 2);
        canvas.drawCircle(getMeasuredWidth() / 2, getMeasuredHeight() / 2, radius - mStrokeWidth / 2, mStrokePaint);
        float left = getMeasuredWidth() / 2 - mWidth / 2;
        float top = getMeasuredHeight() / 2 - mHeight / 2;
        canvas.drawBitmap(bitmap, left, top, null);
    } else {
        canvas.drawBitmap(bitmap, 0, 0, null);
    }
    // 畫文字
    if (!TextUtils.isEmpty(mText)) {
        mTextPaint.setColor(mTextColor);
        mTextPaint.setTextSize(mTextSize);
        mTextPaint.getTextBounds(mText, 0, mText.length() - 1, mTextRect);
        float textLength = mTextPaint.measureText(mText);
        Paint.FontMetrics metrics = mTextPaint.getFontMetrics();
        float baseLine = mTextRect.height() / 2 + (metrics.descent - metrics.ascent) / 2 - metrics.descent;
        canvas.drawText(mText, getMeasuredWidth() / 2 - textLength / 2,
            getMeasuredHeight() / 2 + baseLine, mTextPaint);
    }
    postInvalidateDelayed(10);
}

在這裡前面的mIsAutoBack判斷是我們的開發需求(就是效果圖中第一個圓形的進度),這個只是在圓外面畫了個圈。也挺好看的,我就沒有刪掉。之後就是繪製文字,這裡文書處理要計算其寬高,就不細說了。之後呼叫postInvalidateDelayed(10)方法進行重繪,形成動畫效果。

要注意這裡計算文字繪製基線baseline的方法。

4. 補充

上述只是實現的各個步驟,還有自定義屬性、初始化和公共方法沒有寫出來。放在後面的程式碼下載裡。
自定義屬性有:

<!-- 波紋進度條 -->
<declare-styleable name="WaveProgressView">
    <attr name="progress_max" format="integer" />
    <attr name="progress" format="integer" />
    <attr name="speed" format="float" />
    <attr name="wave_width" format="float" />
    <attr name="wave_height" format="float" />
    <attr name="wave_color" format="color" />
    <attr name="wave_bg_color" format="color" />
    <attr name="stroke_color" format="color" />
    <attr name="main_text" format="string" />
    <attr name="main_text_color" format="color" />
    <attr name="main_text_size" format="dimension" />
    <attr name="hint_text" format="string" />
    <attr name="hint_color" format="color" />
    <attr name="hint_size" format="dimension" />
    <attr name="text_space" format="dimension" />
</declare-styleable>

具體我也不細說了,看名稱應該就知道啥意思了。
公共方法則有setMax(max)設定最大進度、setProgress(progress)設定進度、setWaveColor(color)設定波紋顏色等,不一一列舉了,大家到程式碼裡去看吧。

三、總結

這樣一個波紋進度條,可以方便的幫大家實現以後各種不規則波紋進度條的需求,只需要換換圖片以及波紋顏色即可。

上面帶著大家瞭解該波紋進度條的實現步驟,從中我們不難發現,其實就是一個自定義View的實現順序,只要你瞭解了需求,熟悉相關的實現原理以及api,自定義View也很簡單。