Android自定义View条形带文字、条形圆端、圆形带文字、仿IOS进度条

代码一年前就已经撸好了,现在才想起来总结。。。先放张效果图吧。


概述

可以看到上面实现了四种样式的进度条,带进度文字的长条形进度条、带进度文字的圆形进度条、仿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次思路、代码才实现成这个样子。整个过程不仅学到了知识也开阔了自己的思路,可能以后再遇到类似的问题时会更好更快的想出解决办法。最后,希望本次例子能对大家做出一些帮助吧。





猜你喜欢

转载自blog.csdn.net/ever69/article/details/78375870