【Android(安卓)界面效果15】Android(安卓)UI 之一步步教你自定义控件(自定义属性、合理设计onMeasure、合理设计onDraw等)
Android开发做到了一定程度,多少都会用到自定义控件,一方面是更加灵活,另一方面在大数据量的情况下自定义控件的效率比写布局文件更高。
一个相对完善的自定义控件在布局文件中和java代码中都应能灵活设置属性。另外在普通的布局中和AdapterView中都应能正确绘制,这就要求合理设计onMeasure方法,下文中会做比较详细的讲解。
本文原创,如需转载,请注明转载地址:http://blog.csdn.net/carrey1989/article/details/11757409
接下来我就一步一步来讲解如何设计和编写一个比较完善的自定义控件。
首先要来设计好我们要完成的效果,我们今天来实现下图所示的这样一个控件:
用文字来描述一下:我们要定义的控件上方会显示一张图片,我们可以设置这张图片的内容,长宽比,透明度,伸缩模式,以及图片四周的填充空间大小。图片下方会显示一行文字,作为一级标题,我们可以设置文字的内容,大小,颜色,以及文字区域四周的填充空间的大小。一级标题下方显示一行二级标题,具体设置内容和一级标题相同。
我们不妨先来直接看一下完成后的效果,这样可以更直观的了解要实现的控件的样子。
左图的样子是在常规的布局中自定义控件的样子,右图则是在大数据量的情况下自定义控件作为AdapterView的item的时候绘制出来的样子。
上面我们大体完成了初步的控件设计,下面我们开始编写代码。
第一步,我们写好自定义属性,根据我们上面所做的设计,我们的自定义属性涉及到三个方面,分别是图片相关的属性,一级标题相关的属性,二级标题相关的属性。
按照惯例,我们首先在res/values文件目录下创建一个attrs.xml文件。
然后我们在attrs.xml文件中完成我们对属性的定义,代码片段如下:
[html]view plaincopy- <?xmlversion="1.0"encoding="utf-8"?>
- <resources>
- <attrname="imageSrc"format="reference"/>
- <attrname="imageAspectRatio"format="float"/>
- <attrname="imageAlpha"format="float"/>
- <attrname="imagePaddingLeft"format="dimension"/>
- <attrname="imagePaddingTop"format="dimension"/>
- <attrname="imagePaddingRight"format="dimension"/>
- <attrname="imagePaddingBottom"format="dimension"/>
- <attrname="imageScaleType">
- <enumname="fillXY"value="0"/>
- <enumname="center"value="1"/>
- </attr>
- <attrname="titleText"format="string"/>
- <attrname="titleTextSize"format="dimension"/>
- <attrname="titleTextColor"format="color"/>
- <attrname="titlePaddingLeft"format="dimension"/>
- <attrname="titlePaddingTop"format="dimension"/>
- <attrname="titlePaddingRight"format="dimension"/>
- <attrname="titlePaddingBottom"format="dimension"/>
- <attrname="subTitleText"format="string"/>
- <attrname="subTitleTextSize"format="dimension"/>
- <attrname="subTitleTextColor"format="color"/>
- <attrname="subTitlePaddingLeft"format="dimension"/>
- <attrname="subTitlePaddingTop"format="dimension"/>
- <attrname="subTitlePaddingRight"format="dimension"/>
- <attrname="subTitlePaddingBottom"format="dimension"/>
- <declare-styleablename="CustomView">
- <attrname="imageSrc"/>
- <attrname="imageAspectRatio"/>
- <attrname="imageAlpha"/>
- <attrname="imagePaddingLeft"/>
- <attrname="imagePaddingTop"/>
- <attrname="imagePaddingRight"/>
- <attrname="imagePaddingBottom"/>
- <attrname="imageScaleType"/>
- <attrname="titleText"/>
- <attrname="titleTextSize"/>
- <attrname="titleTextColor"/>
- <attrname="titlePaddingLeft"/>
- <attrname="titlePaddingTop"/>
- <attrname="titlePaddingRight"/>
- <attrname="titlePaddingBottom"/>
- <attrname="subTitleText"/>
- <attrname="subTitleTextSize"/>
- <attrname="subTitleTextColor"/>
- <attrname="subTitlePaddingLeft"/>
- <attrname="subTitlePaddingTop"/>
- <attrname="subTitlePaddingRight"/>
- <attrname="subTitlePaddingBottom"/>
- </declare-styleable>
- </resources>
这里需要说明几点:<attr>标签的format属性值代表属性的类型,这个类型值一共有10种,分别是:reference,float,color,dimension,boolean,string,enum,integer,fraction,flag
。但是我们作为开发者常用的基本上只有reference,float,color,dimension,boolean,string,enum这7种。在attrs.xml文件中的<declare-styleable>标签的name属性的值,按照惯例我们都是写成自定义控件类的名字。一个同名的<attr>在attrs.xml中只可以定义一次。
除此之外,上面的代码都是针对前面的设计来定义了各种属性,相信各位同学都能看懂。
第二步就是编写我们自定义控件的java类了,我们首先将之前做的自定义属性在自定义控件类中做好声明:
[java]view plaincopy- /**图片Bitmap*/
- privateBitmapimageBitmap;
- /**图片的长宽比*/
- privatefloatimageAspectRatio;
- /**图片的透明度*/
- privatefloatimageAlpha;
- /**图片的左padding*/
- privateintimagePaddingLeft;
- /**图片的上padding*/
- privateintimagePaddingTop;
- /**图片的右padding*/
- privateintimagePaddingRight;
- /**图片的下padding*/
- privateintimagePaddingBottom;
- /**图片伸缩模式*/
- privateintimageScaleType;
- /**图片伸缩模式常量fillXY*/
- privatestaticfinalintSCALE_TYPE_FILLXY=0;
- /**图片伸缩模式常量center*/
- privatestaticfinalintSCALE_TYPE_CENTER=1;
- /**标题文本内容*/
- privateStringtitleText;
- /**标题文本字体大小*/
- privateinttitleTextSize;
- /**标题文本字体颜色*/
- privateinttitleTextColor;
- /**标题文本区域左padding*/
- privateinttitlePaddingLeft;
- /**标题文本区域上padding*/
- privateinttitlePaddingTop;
- /**标题文本区域右padding*/
- privateinttitlePaddingRight;
- /**标题文本区域下padding*/
- privateinttitlePaddingBottom;
- /**子标题文本内容*/
- privateStringsubTitleText;
- /**子标题文本字体大小*/
- privateintsubTitleTextSize;
- /**子标题文本字体颜色*/
- privateintsubTitleTextColor;
- /**子标题文本区域左padding*/
- privateintsubTitlePaddingLeft;
- /**子标题文本区域上padding*/
- privateintsubTitlePaddingTop;
- /**子标题文本区域右padding*/
- privateintsubTitlePaddingRight;
- /**子标题文本区域下padding*/
- privateintsubTitlePaddingBottom;
- /**控件用的paint*/
- privatePaintpaint;
- privateTextPainttextPaint;
- /**用来界定控件中不同部分的绘制区域*/
- privateRectrect;
- /**宽度和高度的最小值*/
- privatestaticfinalintMIN_SIZE=12;
- /**控件的宽度*/
- privateintmViewWidth;
- /**控件的高度*/
- privateintmViewHeight;
然后我们要在构造方法中,将从布局文件中读取的自定义属性解析出来。
[java]view plaincopy- TypedArraya=context.getTheme().obtainStyledAttributes(
- attrs,R.styleable.CustomView,defStyle,0);
- intn=a.getIndexCount();
- for(inti=0;i<n;i++){
- intattr=a.getIndex(i);
- switch(attr){
- caseR.styleable.CustomView_imageSrc:
- imageBitmap=BitmapFactory.decodeResource(
- getResources(),a.getResourceId(attr,0));
- break;
- caseR.styleable.CustomView_imageAspectRatio:
- imageAspectRatio=a.getFloat(attr,1.0f);//默认长宽相等
- break;
- caseR.styleable.CustomView_imageAlpha:
- imageAlpha=a.getFloat(attr,1.0f);//默认不透明
- if(imageAlpha>1.0f)imageAlpha=1.0f;
- if(imageAlpha<0.0f)imageAlpha=0.0f;
- break;
- caseR.styleable.CustomView_imagePaddingLeft:
- imagePaddingLeft=a.getDimensionPixelSize(attr,0);
- break;
- caseR.styleable.CustomView_imagePaddingTop:
- imagePaddingTop=a.getDimensionPixelSize(attr,0);
- break;
- caseR.styleable.CustomView_imagePaddingRight:
- imagePaddingRight=a.getDimensionPixelSize(attr,0);
- break;
- caseR.styleable.CustomView_imagePaddingBottom:
- imagePaddingBottom=a.getDimensionPixelSize(attr,0);
- break;
- caseR.styleable.CustomView_imageScaleType:
- imageScaleType=a.getInt(attr,0);
- break;
- caseR.styleable.CustomView_titleText:
- titleText=a.getString(attr);
- break;
- caseR.styleable.CustomView_titleTextSize:
- titleTextSize=a.getDimensionPixelSize(
- attr,(int)TypedValue.applyDimension(
- TypedValue.COMPLEX_UNIT_SP,25,getResources().getDisplayMetrics()));//默认标题字体大小25sp
- break;
- caseR.styleable.CustomView_titleTextColor:
- titleTextColor=a.getColor(attr,0x00000000);//默认黑色字体
- break;
- caseR.styleable.CustomView_titlePaddingLeft:
- titlePaddingLeft=a.getDimensionPixelSize(attr,0);
- break;
- caseR.styleable.CustomView_titlePaddingTop:
- titlePaddingTop=a.getDimensionPixelSize(attr,0);
- break;
- caseR.styleable.CustomView_titlePaddingRight:
- titlePaddingRight=a.getDimensionPixelSize(attr,0);
- break;
- caseR.styleable.CustomView_titlePaddingBottom:
- titlePaddingBottom=a.getDimensionPixelSize(attr,0);
- break;
- caseR.styleable.CustomView_subTitleText:
- subTitleText=a.getString(attr);
- break;
- caseR.styleable.CustomView_subTitleTextSize:
- subTitleTextSize=a.getDimensionPixelSize(attr,
- (int)TypedValue.applyDimension(
- 20,TypedValue.COMPLEX_UNIT_SP,getResources().getDisplayMetrics()));//默认子标题字体大小20sp
- break;
- caseR.styleable.CustomView_subTitleTextColor:
- subTitleTextColor=a.getColor(attr,0x00000000);
- break;
- caseR.styleable.CustomView_subTitlePaddingLeft:
- subTitlePaddingLeft=a.getDimensionPixelSize(attr,0);
- break;
- caseR.styleable.CustomView_subTitlePaddingTop:
- subTitlePaddingTop=a.getDimensionPixelSize(attr,0);
- break;
- caseR.styleable.CustomView_subTitlePaddingRight:
- subTitlePaddingRight=a.getDimensionPixelSize(attr,0);
- break;
- caseR.styleable.CustomView_subTitlePaddingBottom:
- subTitlePaddingBottom=a.getDimensionPixelSize(attr,0);
- break;
- }
- }
- a.recycle();
这里需要说明几点,TypedArray对象在使用完毕后一定要调用recycle()方法。我之前曾在一篇文章中总结过在java代码中进行px与dip(dp)、px与sp单位值的转换。实际上,android中也提供了单位转换的函数,我们也可以使用TypedValue.applyDimension(int unit, float value,DisplayMetrics metrics)方法来进行单位的互换,其中,第一个参数是你想要得到的单位,第二个参数是你想得到的单位的数值,比如:我要得到一个25sp,那么我就用TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 25,getResources().getDisplayMetrics()),返回的就是25sp对应的px数值了。
接下来我们要开始设计onMeasure方法,再设计onMeasure之前我们简单了解几个概念。
MeasureSpec的三种模式:
EXACTLY:表示我们设置了MATCH_PARENT或者一个准确的数值,含义是父布局要给子布局一个确切的大小。
AT_MOST:表示子布局将被限制在一个最大值之内,通常是子布局设置了wrap_content。
UNSPECIFIED:表示子布局想要多大就可以要多大,通常出现在AdapterView中item的heightMode中。
了解了上面几个概念,我们就可以开始设计onMeasure了,具体代码如下:
[java]view plaincopy- @Override
- protectedvoidonMeasure(intwidthMeasureSpec,intheightMeasureSpec){
- intwidthMode=MeasureSpec.getMode(widthMeasureSpec);
- intwidthSize=MeasureSpec.getSize(widthMeasureSpec);
- intheightMode=MeasureSpec.getMode(heightMeasureSpec);
- intheightSize=MeasureSpec.getSize(heightMeasureSpec);
- intwidth;
- intheight;
- if(widthMode==MeasureSpec.EXACTLY){
- width=widthSize;
- }else{
- intdesired=getPaddingLeft()+getPaddingRight()+
- imagePaddingLeft+imagePaddingRight;
- desired+=(imageBitmap!=null)?imageBitmap.getWidth():0;
- width=Math.max(MIN_SIZE,desired);
- if(widthMode==MeasureSpec.AT_MOST){
- width=Math.min(desired,widthSize);
- }
- }
- if(heightMode==MeasureSpec.EXACTLY){
- height=heightSize;
- }else{
- intrawWidth=width-getPaddingLeft()-getPaddingRight();
- intdesired=(int)(getPaddingTop()+getPaddingBottom()+imageAspectRatio*rawWidth);
- if(titleText!=null){
- paint.setTextSize(titleTextSize);
- FontMetricsfm=paint.getFontMetrics();
- inttextHeight=(int)Math.ceil(fm.descent-fm.ascent);
- desired+=(textHeight+titlePaddingTop+titlePaddingBottom);
- }
- if(subTitleText!=null){
- paint.setTextSize(subTitleTextSize);
- FontMetricsfm=paint.getFontMetrics();
- inttextHeight=(int)Math.ceil(fm.descent-fm.ascent);
- desired+=(textHeight+subTitlePaddingTop+subTitlePaddingBottom);
- }
- height=Math.max(MIN_SIZE,desired);
- if(heightMode==MeasureSpec.AT_MOST){
- height=Math.min(desired,heightSize);
- }
- }
- setMeasuredDimension(width,height);
- }
思路是这样的:我们首先判断是不是EXACTLY模式,如果是,那就可以直接设置值了,如果不是,我们先按照UNSPECIFIED模式处理,让子布局得到自己想要的最大值,然后判断是否是AT_MOST模式,来做最后的限制。
完成onMeasure过程之后,我们需要开始onDraw的设计,在onDraw中我们需要考虑各个部分设置的padding值,然后对应做出坐标的处理,整体的思路是从下向上绘制。具体的代码如下:
[java]view plaincopy- @Override
- protectedvoidonDraw(Canvascanvas){
- rect.left=getPaddingLeft();
- rect.top=getPaddingTop();
- rect.right=mViewWidth-getPaddingRight();
- rect.bottom=mViewHeight-getPaddingBottom();
- paint.setAlpha(255);
- if(subTitleText!=null){
- paint.setTextSize(subTitleTextSize);
- paint.setColor(subTitleTextColor);
- paint.setTextAlign(Paint.Align.LEFT);
- FontMetricsfm=paint.getFontMetrics();
- inttextHeight=(int)Math.ceil(fm.descent-fm.ascent);
- intleft=getPaddingLeft()+subTitlePaddingLeft;
- intright=mViewWidth-getPaddingRight()-subTitlePaddingRight;
- intbottom=mViewHeight-getPaddingBottom()-subTitlePaddingBottom;
- Stringmsg=TextUtils.ellipsize(subTitleText,textPaint,right-left,TextUtils.TruncateAt.END).toString();
- floattextWidth=paint.measureText(msg);
- floatx=textWidth<(right-left)?left+(right-left-textWidth)/2:left;
- canvas.drawText(msg,x,bottom-fm.descent,paint);
- rect.bottom-=(textHeight+subTitlePaddingTop+subTitlePaddingBottom);
- }
- if(titleText!=null){
- paint.setTextSize(titleTextSize);
- paint.setColor(titleTextColor);
- paint.setTextAlign(Paint.Align.LEFT);
- FontMetricsfm=paint.getFontMetrics();
- inttextHeight=(int)Math.ceil(fm.descent-fm.ascent);
- floatleft=getPaddingLeft()+titlePaddingLeft;
- floatright=mViewWidth-getPaddingRight()-titlePaddingRight;
- floatbottom=rect.bottom-titlePaddingBottom;
- Stringmsg=TextUtils.ellipsize(titleText,textPaint,right-left,TextUtils.TruncateAt.END).toString();
- floattextWidth=paint.measureText(msg);
- floatx=textWidth<right-left?left+(right-left-textWidth)/2:left;
- canvas.drawText(msg,x,bottom-fm.descent,paint);
- rect.bottom-=(textHeight+titlePaddingTop+titlePaddingBottom);
- }
- if(imageBitmap!=null){
- paint.setAlpha((int)(255*imageAlpha));
- rect.left+=imagePaddingLeft;
- rect.top+=imagePaddingTop;
- rect.right-=imagePaddingRight;
- rect.bottom-=imagePaddingBottom;
- if(imageScaleType==SCALE_TYPE_FILLXY){
- canvas.drawBitmap(imageBitmap,null,rect,paint);
- }elseif(imageScaleType==SCALE_TYPE_CENTER){
- intbw=imageBitmap.getWidth();
- intbh=imageBitmap.getHeight();
- if(bw<rect.right-rect.left){
- intdelta=(rect.right-rect.left-bw)/2;
- rect.left+=delta;
- rect.right-=delta;
- }
- if(bh<rect.bottom-rect.top){
- intdelta=(rect.bottom-rect.top-bh)/2;
- rect.top+=delta;
- rect.bottom-=delta;
- }
- canvas.drawBitmap(imageBitmap,null,rect,paint);
- }
- }
- }
当做完这一步的时候,我们的自定义控件已经能够在布局文件中进行使用了,但是我们还不能在AdapterView中用我们设计的布局文件,因为AdapterView中每一个item属性都是在java代码中动态设置的,因此我们就需要给我们的自定义控件开放属性设置的接口,我们这里暂时只开放了设置图片和文字内容的接口。
- publicvoidsetImageBitmap(Bitmapbitmap){
- imageBitmap=bitmap;
- requestLayout();
- invalidate();
- }
- publicvoidsetTitleText(Stringtext){
- titleText=text;
- requestLayout();
- invalidate();
- }
- publicvoidsetSubTitleText(Stringtext){
- subTitleText=text;
- requestLayout();
- invalidate();
- }
做到这一步的时候,这个自定义控件基本就算完成了,后续的工作就是一些完善和修补了。
接下来就是自定义控件的使用了,在布局文件中使用自定义控件的时候我们需要额外做一点工作,如下:
[java]view plaincopy- <RelativeLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:carrey="http://schemas.android.com/apk/res/com.carrey.customview"
- xmlns:tools="http://schemas.android.com/tools"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- tools:context=".MainActivity">
- <com.carrey.customview.customview.CustomView
- android:id="@+id/customview"
- android:layout_width="200dp"
- android:layout_height="200dp"
- android:layout_centerInParent="true"
- android:background="#FFD700"
- carrey:imageSrc="@drawable/clock"
- carrey:imageAspectRatio="1.0"
- carrey:imageAlpha="0.5"
- carrey:imagePaddingLeft="5dp"
- carrey:imagePaddingTop="5dp"
- carrey:imagePaddingRight="5dp"
- carrey:imagePaddingBottom="5dp"
- carrey:imageScaleType="center"
- carrey:titleText="这是一级标题"
- carrey:titleTextSize="30sp"
- carrey:titleTextColor="#1E90FF"
- carrey:titlePaddingLeft="4dp"
- carrey:titlePaddingTop="4dp"
- carrey:titlePaddingRight="4dp"
- carrey:titlePaddingBottom="4dp"
- carrey:subTitleText="这是二级子标题"
- carrey:subTitleTextSize="20sp"
- carrey:subTitleTextColor="#00FF7F"
- carrey:subTitlePaddingLeft="3dp"
- carrey:subTitlePaddingTop="3dp"
- carrey:subTitlePaddingRight="3dp"
- carrey:subTitlePaddingBottom="3dp"/>
- <Button
- android:id="@+id/button"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:text="nextpage"/>
- </RelativeLayout>
我们需要添加一行xmlns:carrey="http://schemas.android.com/apk/res/com.carrey.customview",其中carrey是一个前缀,你可以随意设置,com.carrey.customview是我们的应用的包名,如果拿不准的可以打开Manifest文件,在<manifest>节点中找到package属性值即可。
对于在AdapterView中的使用方法就和我们正常使用一个常用控件的方法是一样的,这里就不赘述了,如果说到了这里还有一些不明白的地方,可以下载我下面提供的源码,然后对照着博客的思路来看,或者给我留言进行交流。
源码下载
更多相关文章
- Android控件之SlidingDrawer(滑动式抽屉)详解与实例
- Android高手进阶教程(二十八)之---Android(安卓)ViewPager控件的
- Toast(吐司提示)的曾经、现在与未来
- Xamarin android 使用RecyclerView结合SwipeRefreshLayout下拉刷
- Android关于Activity知识点总结(二)任务、返回栈与启动模式
- Android(安卓)带你玩转实现游戏2048 其实2048只是个普通的控件
- Activity 启动模式和 taskAffinity 属性详解
- Android(安卓)画廊效果之ViewPager显示多个图片
- Android(安卓)UI控件之Gallery(拖动效果) --拖动式图片浏览