Android波纹进度条 轻松地让它浪起来

来源:互联网 发布:影楼制作软件apk 编辑:程序博客网 时间:2024/05/23 00:09

一、概述

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

@Overrideprotected 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上了,同时还要绘制上文本。

@Overrideprotected 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

原创粉丝点击