一、概述
最近项目来个需求,波纹进度条。想起来之前看到的一些实现,也想了一下原理啥的,就自己写个吧。不过为了适配以后更多各种不规则的波纹进度条,因此需要能适配各种不同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也很简单。
项目地址:github