自定义View

2015-10-10 chenhui 自定义控件

我们可以通过组合控件和扩展原有控件两种方式来实现自定义控件,本文只介绍扩展原有控件的方法。


扩展原有控件,要说起来其实非常简单,就是定义一个继承自需拓展的控件的类,然后在需要使用这个控件的 XML 文件内写他的类全名或者直接 new 他即可,当然这两种使用方法有各自的构造函数:对于前者,构造函数除第一个 context 参数外,必须要有第二个 AttributeSet 类型的 attr 参数,他封装了 xml 标签内的属性;对于后者,可以只需要一个 context 参数,也可以再多加一些参数,这都要根据情况而定。


那么这个类我们该怎么定义?(我们假设他继承 View 类)实际上既然他继承了 View 类,那实际上这个类的定义方法就应该是重写 View 类的几个方法,那么我们又要重写哪些方法呢?对于一个 View 而言,他其实就是一个显示在屏幕内的二维图形,那么他就有三个必要的参数:1、大小;2、屏幕中的位置;3、显示内容。


既然如此,我们就可以清楚需要重写哪些方法了。

1、算出 View 的大小(宽高),重写 onMeasure(int,int) ,那他该怎么写呢?有一个 setMeasuredDimension(width,height) 方法,直接调用他就是设置了。一般来说,自制 view 会有一个背景图片,算出那张图片的高宽填入即可。如果我们不重写这个函数,那么当使用者定义这个控件时如果设置了宽高为 "wrap_content",他就会充满整个屏幕。

2、算出 VIew 的位置,重写 onLayout(),不过这个函数不重写也是可以的。

3、得到 View 的内容,重写 onDraw(Canvas),他是必须重写的,那么他该怎么写呢?其实很简单,在  onMeasure 和 onLayout 都被调用后,绘图的位置和大小都已经确定好了,那么系统会基于位置和大小创建一个画板,就是 Canvas 参数,我们只要把要显示的数据绘制在这个画板上即可。


下面实现一个最简单的自定义控件:一个点击后就会换图片的控件。


public class Test extends View implements OnClickListener{
	
	private Bitmap imgs[] = new Bitmap[2];
	int i = 0;
	
	public Test(Context context){
		super(context);
	}
	
	public Test(Context context,AttributeSet attrs) {
		
		super(context, attrs);
		
		imgs[0] = 
			BitmapFactory.decodeResource(
					context.getResources(),R.drawable.ee_1);
		imgs[1] = 
			BitmapFactory.decodeResource(
					context.getResources(),R.drawable.ee_2);
		setOnClickListener(this);
	}

	/*
	 * 测量view的大小
	 * */
	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		// TODO Auto-generated method stub
		
		setMeasuredDimension(imgs[0].getWidth(),imgs[0].getHeight());
	}
	
	/*
	 * 绘制view
	 * */
	@Override
	protected void onDraw(Canvas canvas) {
		// TODO Auto-generated method stub
		
		canvas.drawBitmap(imgs[i], 0, 0, new Paint());
	}

	@Override
	public void onClick(View arg0) {
		// TODO Auto-generated method stub
		
		i = i == 1 ? 0 : 1;
		
		/* 他会导致控件重绘 */
		invalidate();
	}
}


XML:


<com.example.freehui.Test
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" >
</com.example.freehui.Test>


onDraw() 不必多说,关键是 onMeasure() 这个方法需要了解。


onMeasure() 是用来测量 View 的高宽的,如果我们不实现他,那么当这个 View 的高宽设为 wrap_content 或 full_parent 时,他都会充满父容器;如果这个 View 的高宽为精确的值,那么他就会是一个精确的大小。


在上面那个例子中,由于 View 只是一张图片,所以是直接把图片的高宽填了进入,但如果这不是一张图片呢?就像 wrap_content,如果我们需要他自适应呢?这就需要利用到 onMeasure() 的那两个参数了。


onMeasure() 有两个参数:widthMeasureSpec 和 heightMeasureSpec,这两个参数里分别包含两个信息,即父容器建议的高宽和 XML 里的高宽设置(如 wrap_content)。怎么利用他们?下面给个例子。


void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    
	/* 得到处理后的高度和宽度 */
	int width = MeasureDimension(100,widthMeasureSpec);
	int height = MeasureDimension(100,heightMeasureSpec);
	
	/* 生效 */
    setMeasuredDimension(width, height);                
}

/*
 * 主要处理
 */
int MeasureDimension(int defaultSize,int measureSpec){
	
	//得到建议值和模式
	int size = MeasureSpec.getSize(measureSpec);
	int mode = MeasureSpec.getMode(measureSpec);
	
	if (mode == MeasureSpec.EXACTLY) 
		//已指定了精确值或者充满父容器,那么直接使用建议值
		return size;  
		
	if (mode == MeasureSpec.AT_MOST)
		//情况1. 设置的是 wrap_content 
		//情况2. 设置的是 full_parent 但父控件是精确值,那么不能超过父控件的大小
		return Math.min(defaultSize,size);//不能超过父控件给定的大小
		
	if (mode == MeasureSpec.UNSPECIFIED)
		//这个表示由控件自己设置大小,那么返回默认大小
		return defualtSize;
		 
	return defualtSize;
}


我们可以调用 MeasureSpec 对象的 getMode() 方法来得到模式值,模式值共有以下三种:


  • EXACTLY,精确值模式。当高宽为一个确定值或者 match_parent(fill_parent) 时生效。
  • AT_MOST,最大值模式,当高宽为 wrap_content 时,控件大小会跟随子控件的大小而变化,此时其大小不得超过其父控件允许的最大尺寸。
  • UNSPECIFIED,无模式,如果是这个值,表示大小随便指定。



上面我们实现了一个继承自 View 的自定义控件,但有时候我们可能会继承比如 TextView 之类的控件。这个时候,我们就需要做出额外的处理,但大体上是差不多的,主要差别在于 onDraw() 时,除了绘制自身的内容外,还要调用 super.onDraw() 来绘制 TextView 的内容,至于自身和父类的内容谁先绘制,则要按自己的需求了。额外要注意的是,无论是谁先绘制,绘制完后都要调用 Canvas.translate(x,y) 来移动绘制点,否则两层内容会重叠。



发表评论:

Copyright ©2015-2016 freehui All rights reserved