Android波紋進度條 輕鬆地讓它浪起來
一、概述
最近專案來個需求,波紋進度條。想起來之前看到的一些實現,也想了一下原理啥的,就自己寫個吧。不過為了適配以後更多各種不規則的波紋進度條,因此需要能適配各種不同png圖片的波紋進度條。
1. 效果圖
no picture say a j8!
2. 原理分析
波紋進度條,不外乎一張背景bitmap,一張進度波紋bitmap。之後則不停的向一個方向迴圈移動波紋即可。如下圖(手畫,輕噴):
當然最關鍵的問題是如何把多餘的波紋給隱藏起來,這裡就要用到Android繪圖裡的點陣圖運算了。PorterDuffXfermode給我們提供了一種實現複雜的點陣圖運算的支援。其包含16中運算模式,如圖(這個圖網上到處都是,我是從APIDemo中截來的):
大概說一下,一般先畫的是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也很簡單。