当初刚入门Android时用的都是原生的控件,刚开始觉得原生的控件其实也可以满足当时的一些学校的小项目开发,也就没怎么深入自定义view。但参加工作后,发现有时美工给的设计图某些功能实现起来还是挺刁钻的,于是便开始了自定义view的学习。或许很多人都觉得自定义view是个很难的东西,其实当你真正用心去弄了几个自定义view之后就会发现其实也并没有那么难。由于个人工作效率还是蛮快的,项目之余闲蛋疼的很,常常自己看到那些好玩的东西就用自定义view画下来。
自定义view的基本步骤无非也就那么几步:
1. values文件夹下创建attrs.xml文件,在attrs里添加你想给自己view添加的属性。例:
attrs.xml
<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="MyTextView"> <attr name="text" format="string"/> <attr name="textSize" format="dimension"/> </declare-styleable> </resources>
declare-styleable是你自定义view的一套新定义的属性,下面包含了你要定义的各种属性attr
format是指定attr属性的单位,其中包括:
(1) reference: 引用某一资源,如:src="@drawable/sourcename";
(2)color:颜色,如color="#ff0000";
(3)boolean:布尔值,true或false;
(4)dimension:尺寸值,如sp,dp,px;
(5)float:浮点型,也就是小数,如0.5, 1.8;
(6)integer:整形, 如 1, 100;
(7)string:字符串
(8)fraction:百分数, 如100%
(9)enum:枚举,如 orientation="vertical"
(10)flag:位或运算,如gravity="centerHorizontal | right"
2. 创建类文件,添加构造体,获取属性并初始化变量。例:
public class MyTextView extends View { private String text; private int textSize; private Paint paint; public MyTextView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); // TODO Auto-generated constructor stub //顾名思义,获取风格和属性,得到一个包含各种属性的数组array,包括你自定义的attr属性 //R.styleable.MyTextView就是一个指向你刚在attrs.xml中自定义的属性数组的id TypedArray array=context.obtainStyledAttributes(attrs, R.styleable.MyTextView); //获取文本内容 text=array.getString(R.styleable.MyTextView_text); //获取文本字体大小,第二个参数是默认值,就是没有使用你定义属性时的提供值, sp2px()是sp转px函数。 textSize=array.getDimensionPixelSize(R.styleable.MyTextView_textSize, sp2px(18)); //这玩意初始化完成后务必回收 array.recycle(); //画笔初始化,用于后面的绘图; paint=new Paint(); //至此,完成变量的初始化 } public MyTextView(Context context, AttributeSet attrs) { this(context, attrs, 0); // TODO Auto-generated constructor stub //若使用xml加载view,必须要重写上面或这个构造体 } public MyTextView(Context context) { this(context, null); // TODO Auto-generated constructor stub //统一到第一个构造体进行初始化 } }
3. 重写onMeasure(),,测量view,确定view的尺寸。(这步并不是自定义view的必要步骤,但重写后可以适应wrap_content这参数等)
这一步因为不是必须,可以跳过,但当你在设置layout_width和layout_height的时候只能设match_parent或指定值,不然设置wrap_content会很别扭。
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // TODO Auto-generated method stub //widthMeasureSpec参数可以被MeasureSpec类的静态方法解析出宽度计算的模式和值 //模式有AT_MOST, EXACTLY, UNSPECIFIED int width=measureViewWidth(widthMeasureSpec); //计算高度,和宽度处理差不多 int height=measureViewHeight(heightMeasureSpec); setMeasuredDimension(width, height); } //处理view的宽度 private int measureViewWidth(int widthSpec){ int result=0; int mode=MeasureSpec.getMode(widthSpec); int width=MeasureSpec.getSize(widthSpec); //对应wrap_content, viewgroup只提供一个最大值,子view尺寸不能超过这个值 //这种情况下,可以根据内容大小设置view的大小,如令view的width=text的宽度 if(mode==MeasureSpec.AT_MOST){ int textWidth=measureTextWidth(); result=Math.min(textWidth, width); } //对应match_parent或指定的值,viewgourp提供的值为parent的宽度或指定的宽度 if(mode==MeasureSpec.EXACTLY){ result=width; } return result; } //处理view的高度 private int measureViewHeight(int heightSpec){ int result=0; int mode=MeasureSpec.getMode(heightSpec); int height=MeasureSpec.getSize(heightSpec); if(mode==MeasureSpec.AT_MOST){ int textHeight=measureTextHeight(); result=Math.min(textHeight, height); } if(mode==MeasureSpec.EXACTLY){ result=height; } return result; } //测量text的宽度 private int measureTextWidth(){ int textWidth=(int) paint.measureText(text); return textWidth; } //测量text的高度 private int measureTextHeight(){ FontMetrics fm=paint.getFontMetrics(); int textHeight=(int) (fm.bottom-fm.top); return textHeight; }
widthMeasureSpec和heightMeasureSpec两个值是viewgroup传给子view的,通过MeasureSpec的解析后再根据模式来计算最后的值,若是wrap_content则计算view内容的尺寸再计算view的尺寸,若是match_parent或指定值,则直接使用viewgroup传过来的值,经过处理后,最后还要调用setMeasuredDimension来确定view的最终尺寸。
view的尺寸设置为wrap_content情况下,左边没有重写onMeasure,viewgroup会传一个父组件可分配给子view的最大尺寸,所以子view的尺寸便和父容器一样大了;而右边的重写了onMeasure之后,因为经过处理,使子view尺寸等于文本内容大小,所以尺寸只有文本大小。
4.重写onDraw(), 在一块空白的View上绘制你想要的东西,这一步是最重要的。如:
@Override protected void onDraw(Canvas canvas) { // TODO Auto-generated method stub //把view的背景绘成黄色 canvas.drawColor(Color.YELLOW); //测量绘制字体的高度 FontMetrics fm=paint.getFontMetrics(); int textHeight=(int) (fm.bottom-fm.top); //参数1.要绘制的文本, 2.文本左边位于view的x坐标, 3.文本baseline位于view的y坐标, 4.画笔 //因为baseline到文本底部的距离无法获取,只能取文本高度的3/10 canvas.drawText(text, 0, textHeight-textHeight*0.3f, paint); }
canvas类封装了一大堆绘图工具,所以画图并不是很难的事,不过若要实现比较复杂的图,那就需要懂得一些几何计算知识了,此处只是把文本简单地画上去而已。
还有那个文本的高度处理不懂的可以百度搜索android baseline或android测量字体高度。
5. 在布局文件里使用,记得添加属性使用的空间,也就是最顶部xmlns=xxxxxx,那一串东西。
activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:custom="http://schemas.android.com/apk/res/com.example.test" android:id="@+id/layout" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <com.example.test.MyTextView android:layout_width="wrap_content" android:layout_height="wrap_content" custom:text="aaaaaaaaaaagggggggggggggggg" custom:textSize="18sp"/> </LinearLayout>
下面的custom属性命名空间必须加上xmlns:custom="http://schemas.android.com/apk/res/com.example.test"才能用
格式: xmlns:定义的空间名称="http://schemas.android.com/apk/res/在AndroidManifest中的包名。
至此,一个简单的自定义view就实现了,看起来代码挺多的,但真正去把它写完后,就感觉其实自定义view也就这样而已。当然,简单的view只要实现以上几个步骤,基本就可以满足需要了,如果要实现华丽的效果,仅仅是上面几个步骤不够的, 还要重新onTouchEvent等函数,使view能处理触摸事件从而达到交互效果。
下面贴出完整代码:
MyTextView.java
public class MyTextView extends View { private String text; private int textSize; private Paint paint; public MyTextView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); // TODO Auto-generated constructor stub //顾名思义,获取风格和属性,得到一个包含各种属性的数组array,包括你自定义的attr属性 //R.styleable.MyTextView就是一个指向你刚在attrs.xml中自定义的属性数组的id TypedArray array=context.obtainStyledAttributes(attrs, R.styleable.MyTextView); //获取文本内容 text=array.getString(R.styleable.MyTextView_text); //获取文本字体大小,第二个参数是默认值,就是没有使用你定义属性时的提供值, sp2px()是sp转px函数。 textSize=array.getDimensionPixelSize(R.styleable.MyTextView_textSize, sp2px(18)); //这玩意初始化完成后务必回收 array.recycle(); //画笔初始化,用于后面的绘图; paint=new Paint(); //至此,完成变量的初始化 paint.setTextSize(textSize); } public MyTextView(Context context, AttributeSet attrs) { this(context, attrs, 0); // TODO Auto-generated constructor stub //若使用xml加载view,必须要重写上面或这个构造体 } public MyTextView(Context context) { this(context, null); // TODO Auto-generated constructor stub //统一到第一个构造体进行初始化 } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // TODO Auto-generated method stub //widthMeasureSpec参数可以被MeasureSpec类的静态方法解析出宽度计算的模式和值 //模式有AT_MOST, EXACTLY, UNSPECIFIED int width=measureViewWidth(widthMeasureSpec); //计算高度,和宽度处理差不多 int height=measureViewHeight(heightMeasureSpec); setMeasuredDimension(width, height); } //处理view的宽度 private int measureViewWidth(int widthSpec){ int result=0; int mode=MeasureSpec.getMode(widthSpec); int width=MeasureSpec.getSize(widthSpec); //对应wrap_content, viewgroup只提供一个最大值,子view尺寸不能超过这个值 //这种情况下,可以根据内容大小设置view的大小,如令view的width=text的宽度 if(mode==MeasureSpec.AT_MOST){ int textWidth=measureTextWidth(); result=Math.min(textWidth, width); } //对应match_parent或指定的值,viewgourp提供的值为parent的宽度或指定的宽度 if(mode==MeasureSpec.EXACTLY){ result=width; } return result; } //处理view的高度 private int measureViewHeight(int heightSpec){ int result=0; int mode=MeasureSpec.getMode(heightSpec); int height=MeasureSpec.getSize(heightSpec); if(mode==MeasureSpec.AT_MOST){ int textHeight=measureTextHeight(); result=Math.min(textHeight, height); } if(mode==MeasureSpec.EXACTLY){ result=height; } return result; } //测量text的宽度 private int measureTextWidth(){ int textWidth=(int) paint.measureText(text); return textWidth; } //测量text的高度 private int measureTextHeight(){ FontMetrics fm=paint.getFontMetrics(); int textHeight=(int) (fm.bottom-fm.top); return textHeight; } @Override protected void onDraw(Canvas canvas) { // TODO Auto-generated method stub //把view的背景绘成黄色 canvas.drawColor(Color.YELLOW); //测量绘制字体的高度 FontMetrics fm=paint.getFontMetrics(); int textHeight=(int) (fm.bottom-fm.top); //参数1.要绘制的文本, 2.文本左边位于view的x坐标, 3.文本baseline位于view的y坐标, 4.画笔 //因为baseline到文本底部的距离无法获取,只能取文本高度的3/10 canvas.drawText(text, 0, textHeight-textHeight*0.3f, paint); } //sp转px单位 private int sp2px(int sp){ return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, getResources().getDisplayMetrics()); } }
布局文件很简单就不贴了, attrs文件也很简单,在上面了。