1.创建自定义属性,在res/values目录下创建attrs.xml文件,声明自定义控件的属性
2.创建自定义View类,继承于View类,重写View的三个构造方法
3.通过TypeArray获得各个自定义属性,并将Paint设置为这些属性的内容
4.重写onMeasure方法,设置好视图在界面上所显示的大小
5.重写onDraw方法,通过paint和canvas渲染出自定义控件
<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="myTextView"> <attr name="mytextContent" format="string" type="string"></attr> <attr name="mytextColor" format="color" type="color"></attr> <attr name="mytextSize" format="dimension" type="dimension"></attr> </declare-styleable> </resources>
声明自定义属性有两个作用,一方面可以让我们在布局文件中直接使用这些属性,另一方面,在xml中声明属性之后会在R类中生成对应的资源ID,方便到时候TypeArray的使用(关于TypeArray见下文)
每个attr标签表示声明一个属性,name是属性的名字,format是属性的格式,比如string表示这个属性必须为字符串,color表示这个属性必须为颜色类型,dimension表示这个属性必须为像素即dp、sp之类的
public class MyTextView extends View implements View.OnClickListener{ private String mytextContent; private int mytextColor; private int mytextSize; //画笔,用于绘制图形时使用 private Paint paint; //矩形对象,用于计算文字位置时使用 private Rect rect; public MyTextView(Context context) { // TODO Auto-generated constructor stub this(context,null); } public MyTextView(Context context, AttributeSet attrs) { // TODO Auto-generated constructor stub this(context,attrs,0); } public MyTextView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); // TODO Auto-generated constructor stub } }
可以看到三个构造方法的区别在于参数
public MyTextView(Context context) 【通过传入上下文对象来创建view】
public MyTextView(Context context, AttributeSet attrs) 【AttributeSet类型是用来获得我们声明的各个属性,当我们在xml布局文件中声明自定义View时,就会调用此构造方法】
public MyTextView(Context context, AttributeSet attrs, int defStyleAttr) 【多了一个defStyleAttr属性,当我们需要为view设定style时才会用到】
另外,我们对前两个构造方法都调用了this(),学过java的都知道,this表示调用本类的构造方法,在第一个构造方法中,我们调用了 this(context,null);其实是调用了第二个构造方法,在第二个构造方法中调用了this(context,attrs,0);其实是调用了第三个构造方法,所以这样写的好处是无论我们使用哪个构造方法,最终都会进到第三个构造方法,所以接下来我们要做的便是在第三个构造方法中获得我们的属性。
public MyTextView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); // TODO Auto-generated constructor stub TypedArray array = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.myTextView, defStyleAttr, 0); //获得属性,并设置默认值 mytextContent = array.getString(R.styleable.myTextView_mytextContent); mytextColor = array.getColor(R.styleable.myTextView_mytextColor,Color.WHITE); mytextSize = array.getDimensionPixelSize(R.styleable.myTextView_mytextSize, 30); array.recycle(); paint = new Paint(); //将画笔的文字大小设置为我们定义的大小 paint.setTextSize(mytextSize); rect = new Rect(); /** * 此方法可以获得文字所在的矩形区域,并赋给rect * 参数1:传入文字的内容 * 参数2:传入文字起始的长度,一般为0 * 参数3:传入文字结束的长度,一般为text.length * 参数4:传入一个Rect矩形对象 */ paint.getTextBounds(mytextContent, 0, mytextContent.length(), rect); }
刚才在上文中已说过,此构造方法的AttributeSet参数可以得到我们在attr中声明的属性,那为什么此处还要通过TypeArray来获得呢?TypeArray有什么用?其实如果我们是直接通过AttributeSet获得属性的话,还需要解析才能获得我们需要的各个属性的值以及格式,而TypeArray帮我们把这些操作都封装好了,所以通过TypeArray可以很方便的获取到我们的属性。
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // TODO Auto-generated method stub super.onMeasure(widthMeasureSpec, heightMeasureSpec); //setMeasuredDimension(widthMeasureSpec, heightMeasureSpec); }
对这个方法的使用见下文
@Override protected void onDraw(Canvas canvas) { // TODO Auto-generated method stub paint.setColor(Color.BLACK); //canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), paint); /** * 参数1:圆心的横坐标 * 参数2:圆心的纵坐标 * 参数3:圆的半径 * 参数4:用来绘制的画笔 */ canvas.drawCircle(getMeasuredWidth()/2, getMeasuredHeight()/2, getMeasuredWidth()/2, paint); paint.setColor(Color.RED); canvas.drawCircle(getMeasuredWidth()/2, getMeasuredHeight()/2, getMeasuredWidth()/3, paint); paint.setColor(mytextColor); canvas.drawText(mytextContent, getWidth() / 2 - rect.width() / 2, getHeight() / 2 + rect.height() / 2, paint); }
我们在这里通过canvas调用了两次drawCircle,即绘制了两个圆形,注意到第二个圆形的半径为getMeasuredWidth()/3,比第一个圆的半径小了,而圆心又与第一个圆一致,所以待会儿绘制出来的效果就是一个小圆叠在一个大圆前面,在每次绘制圆之前都调用了paint设置颜色,是为了两个圆形的颜色不一样。
最后再次设置paint颜色为mytextColor,即我们的文字内容的颜色,然后通过canvas.drawText进行文字的绘制,这里之所以getWidth() / 2 - rect.width() / 2是因为将View的宽度的一半减去文字内容的宽度的一半,得到的就是文字内容的左上角的横坐标(可以自行画图理解),纵坐标也是同理。
至此,我们完成了基本的定义,接下来就是在布局文件中使用它了:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" xmlns:mytext="http://schemas.android.com/apk/res/com.example.myview" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="${relativePackage}.${activityClass}" > <com.example.view.MyTextView android:layout_width="200dp" android:layout_height="200dp" mytext:mytextContent="1" mytext:mytextColor="#FFF" mytext:mytextSize="20sp" /> </RelativeLayout>
xmlns:mytext="http://schemas.android.com/apk/res/com.example.myview"是命名空间的声明,等下我们声明自定义属性的时候需要用到,这里的路径是http://scheam.....res/+项目包名
运行
运行之后,发现在屏幕成功出现了一个圆形背景中间带有文字内容,这里我们设置的宽和高都是200dp,如果将layout_width和layout_height都设置为wrap_content,会发现它并不像我们平时那样会自动有一个默认大小,而是像fill_parent一样的填充屏幕,很明显这不是我们愿意看到的,这个时候就需要用到onMeasure方法了:
//wrap_content时默认的宽度 private final int DEFAULT_WIDTH = 200; //wrap_content时默认的高度 private final int DEFAULT_HEIGHT = 200;
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // TODO Auto-generated method stub //super.onMeasure(widthMeasureSpec, heightMeasureSpec); int width; //最终的宽度 int height; //最终的高度 int wspecMode = MeasureSpec.getMode(widthMeasureSpec); int wspecSize = MeasureSpec.getSize(widthMeasureSpec); int hspecMode = MeasureSpec.getMode(heightMeasureSpec); int hspecSize = MeasureSpec.getSize(heightMeasureSpec); if(wspecMode==MeasureSpec.EXACTLY){ width = wspecSize; } else{ width = DEFAULT_WIDTH; } if(hspecMode==MeasureSpec.EXACTLY){ height = hspecSize; } else{ height = DEFAULT_HEIGHT; } setMeasuredDimension(width, height); }
其中,有两个关键的方法,getMode和getSize,getSize是获得用户所设定的view的宽高,getMode是获得这个属性的模式,Mode有三种:
MeasureSpec.EXACTLY【设置了明确的值或者是MATCH_PARENT】
MeasureSpec.AT_MOST【表示子布局限制在一个最大值内,一般为WARP_CONTENT】
MeasureSpec.UNSPECIFIED【表示子布局想要多大就多大,很少使用】
对模式进行判断,如果模式是EXACTLY,说明用户在布局文件中对宽高设定了具体数值或者match_parent,如果是具体数值,那么就将这个具体数值作为最终宽高,如果是match_parent,就将其填充父布局。如果是AT_MOST,说明用户在布局文件中设定为了wrap_content,那么就应该将我们的默认宽度或者高度作为最终的宽高
通过以上测量,就能够在设定为wrap_content时依然能够限定在一定的大小中。
运行并点击,就会发现文字内容每点击一次就+1
希望本文能够帮助你解决自定义View的困惑,关于ViewGroup和重写系统控件的见解会在以后的博文进行整理