【安卓自定义控件系列】自绘控件打造界面超炫功能超强的圆形进度条


在前面我们讲过了安卓自定义控件三种方式中的组合控件,现在我们来讲解一下通过自绘的方式来实现自定义控件,本博客将以自定义圆形进度条为例向大家讲解自定义控件的知识,首先来看一下效果图吧,这个是本人自定义圆形进度条demo工程的运行截图:



首先说一下自己这个自定义圆形进度条要达到的目标:

1能够支持设置进度条各种属性,如圆环的大小,颜色,进度条的大小,颜色,进度条的颜色支持设置三种颜色来达到渐变色的效果。

2圆形进度条的内部支持设置三层文本,即上层的标题,如上图的“您的等级超越全国”,中间层的进度值,如上图的“700”,下层的附带内容,如上图的“万的用户”

3支持设置三层文本的大小与颜色,如上图标题与底部文本为黑色,中间文本为红色

4支持进度条从任意位置开始显示,为何要支持该功能,是因为在不同的场合,进度条开始显示的位置一般是不同的,如在某些手机助手类下载App的应用中显示下载进度的时候都是从圆环的顶部开始,以顺时针为方向逐渐递增显示,本例的第三个小圆环即是模仿的该场合,但是因为截的动态图上传出错,只能上传几张图片,所以看的不是很清楚,而在某些计步器类的app中进度的绘制一般是从左下角开始显示,然后以顺时针为方向达到对称的位置,本例的最后一个大圆环即是模仿的该场合。

5支持设置部分圆弧,而不是整个圆,如本例的最后一个大圆环的进度条显示效果,因为在某些场合是不需要绘制整个圆的,如在模拟汽车速度表盘的场合。

6具备极强的自适应能力,即wrap_content参数要能够比较完美的适应用户输入的文本的长度。


在做这个自定义控件的工程时也遇到了一坑,不过都一一解决了,其中最难的就是最后一条,要求具备极强的自适应能力,即当我们在xml文件中指定该自定义控件的宽度与高度为wrap_content时如何完美的适应用户输入的文本,这个是关键,这涉及到安卓中控件的绘制过程的知识和一些绘图API的使用,主要是paint与draw这两个类的API,而本人完美解决wrap_content涉及到了paint类中某些不常用的API,因此说这也是难以解决的一个原因,你必须对paint类的API非常熟悉,即使是相对而言很少使用的。

因为本人已打算将该组件开源,上传到我的github上,大家可以到我的github上去fork我的代码,因此本博客不是对整个自定义圆形进度条做讲解,因为这没啥难度,另外网上很多这些方面的博客,本博客重点讲解如何解决极强的自适应能力,因此下面重点说一下如何解决wrap_content.


在贴出自己解决方案的代码之前,先给大家普及一下关于安卓中控件的绘制过程,因为你明白了这个过程才知道如何去解决wrap_content.

我们知道安卓中一个控件要显示在界面上要经过三个过程,即测量,布局与绘制,对应onMeasure,onLayout,onDraw这三个函数,很显然我们要解决的是测量过程,即当在xml中设置wrap_content的时候如何较准确的测量出我们自定义控件的大小,在安卓中一个控件的大小用MeasureSpec这个类来表示所,测量过程实际上可以说是得到该控件的MeasureSpec的过程。下面是一些关于MeasureSpec类的常识:

- MeasureSpec是通过将一个int(32)的数组成而成的,本质是一个int数,MeasureSpec由两部分组成:SepcMode 和 SpecSize 。其中SpecMode为MeasueSpec的高2位,SpecSize为MeasureSpec的低30位。SpecMode有3类:

 - UNSPECIFIED:父容器不对View有任何限制,要多大有多大,这种情况一般用于系统内容。在我们使用过程中,一般不考虑这种模式。
 - EXACTLY:父容器已经检测到了View所需要的精确大小,这个时候View的最终大小就是SpecSize所指定的大小。它对应的LayotParams中的match_parent 和具体的数值。
 - AT_MOST: 父容器指定了一个可用大小SpecSize,View的大小不能大于这个值,它对应于LayoutParams中的wrap_content。

一个子控件的MeasureSpec与父容器的MeasureSpec和自身的LayoutParams相关,由二者共同决定。一个子控件的onMeasure方法一般由其父容器所调用,代码如下:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);
        switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }

从上面的代码可以看到AT_MOST和EXACTLY两种模式都会被设置成specSize。我们也知道AT_MOST对应于wrap_content,而EXACTLY对应于match_parent和具体数值情况。也就说默认情况下wrap_content和match_parent是具有相同的效果的,这也是为何说在我们通过继承自View类自绘的方式自定义控件的时候为何需要自己支持wrap_content的原因。

知道了原因我们就知道要解决的话则应该重写自定义控件的onMeasure函数,在该函数中为自定义控件设置一个默认的宽与高即可,但是为了做到极强的自适应,这个宽与高必须非常恰当,那么怎样才算恰当呢?这个就和自定义控件的功能相关,如本人自定义控件中要求支持三层文本显示,那么很显然所谓的恰当就是刚好能够容纳这三行文本中的最大长度的文本,因此此时解决思路就转换为了如何求自绘文本的长度,这个涉及到了Paint类中的一个API getTextBounds,通过该API就可以知道文本的长度,从而较好的支持wrap_content,下面是解决wrap_content的完整代码,注释很详细,大家应该可以看懂。

	 //必须重写该方法,否则在xml文件中定义warp_content与match_parent效果相同
	@Override
	 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

		 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
		 int desiredWidth ;
		 int desiredHeight ;
		 
     //设置了文本,且属性为wrap_content,那么以输入的文本的长度为宽度
		 if (isSetToptitle) {
		 Rect rect = new Rect();  
		 titlePaint.getTextBounds(topTitle, 0, topTitle.length(), rect); 
//		 desiredWidth =(int) (rect.width()+4*progressWidth);
//		 desiredHeight = (int) (rect.width()+4*progressWidth);
		 desiredWidth =(int) (1.5*rect.width());
		 desiredHeight = (int) (1.5*rect.width());
		 }
		 else//没设置文本那么不可能设置wrap_content属性,事实上这些设置无效
		 {
			 desiredWidth =500;
			 desiredHeight = 500;
		 }
		    
		    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
		    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
		    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
		    int heightSize = MeasureSpec.getSize(heightMeasureSpec);

		    int width;
		    int height;

		    //Measure Width
		    if (widthMode == MeasureSpec.EXACTLY) {
		        //Must be this size
		        width = widthSize;
		    } else if (widthMode == MeasureSpec.AT_MOST) {
		        //Can't be bigger than...
		        width = Math.min(desiredWidth, widthSize);
		    } else {
		        //Be whatever you want
		        width = desiredWidth;
		    }

		    //Measure Height
		    if (heightMode == MeasureSpec.EXACTLY) {
		        //Must be this size
		        height = heightSize;
		    } else if (heightMode == MeasureSpec.AT_MOST) {
		        //Can't be bigger than...
		        height = Math.min(desiredHeight, heightSize);
		    } else {
		        //Be whatever you want
		        height = desiredHeight;
		    }

		    //MUST CALL THIS
		    setMeasuredDimension(width, height);
		    
		    center = getWidth()/2; //该方法必须在onDraw或者onMeasure中调用,否则不起作用d
	        
			//圆环的半径 ,此处必须是progressWidth与circleWidth中较大的一个
		    //radius = (int) (center - progressWidth/2); 
			if(progressWidth>circleWidth)
			  radius=(int)(center-progressWidth/2);
			else
			  {radius=(int)(center-circleWidth/2);}
	        
	       
	        sweepGradient = new SweepGradient(0, 0, colors, null);
	        hideRect=new RectF(center - radius, center - radius, center  
	                + radius, center + radius); 

		}

至于其它属性因为很简单,无非就是在atrs文件中定义属性,在构造函数中通过context.obtainStyledAttributes得到一个TypedArray,然后通过TypedArray获取属性,重写onDraw函数,在该函数中处理这些获取到的属性而已,不涉及到很多原理上的思考,另外网上关于这方面的资料也很多,所以没贴出代码,大家如果感兴趣可以follow我的github账号,本人将上传该项目工程到我的github上,这个应该是目前github上开源的相关组件中功能最强大的自定义圆形进度条了,欢迎大家follow,star与fork。


我的github:https://github.com/HuTianQi

如果大家觉得不错记得小手一抖点个赞哦,欢迎大家关注我的博客账号,将会不定期为大家分享技术干货,福利多多哦!



posted on 2016-07-28 14:33  海南一哥  阅读(200)  评论(0编辑  收藏  举报

导航