android中自定义View及ViewGroup 学习心得

本文详细解析了如何在Android中自定义控件与视图,包括继承、构造方法、重写关键方法(如onMeasure、onLayout、onDraw)以及自定义属性的实现。以CustomView和CustomLayout为例,展示了自定义控件的基本步骤和关键方法的实现逻辑。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

android SDK中自带了很多控件,一般情况下可以满足各类开发需求。但有时候可能需要自定义View以更好的适应开发需求。

系统自带的所有控件都是直接或间接的继承class View的,所以自定义一个View也要继承class View,如果你想要其他某控件的功能或是想修改某功能,可以继承其他控件,这就是间接继承class View。

自定义View

自定义view一般步骤是:

1、继承class View 或 View的子类;

2、实现3个构造方法;

3、重写onMeasure方法,可以不重写;

4、重写layout方法,可以不重写;

5、重写onDraw方法,一般必须重写;


例如现需自定义view叫CustomView ,代码如下:

<span style="font-size:18px;">public class CustomView extends View {

	public CustomView(Context context) {
		super(context);
		System.out.println("--------CustomView 1 ------------");
	}

	public CustomView(Context context, AttributeSet attrs) {
		this(context, attrs,0);</span><span style="font-size:14px;">//有默认<span style="background-color: rgb(240, 240, 240);">defStyleAttr可以在此时传给第三个构造方法,没有就传0</span></span><span style="font-size:18px;">
		System.out.println("--------CustomView 2 ------------");

	}

	public CustomView(Context context, AttributeSet attrs, int defStyleAttr) {
		super(context, attrs, defStyleAttr);
		System.out.println("--------CustomView 3 ------------");

	}
</span><span style="font-size:18px; white-space: pre;">	</span><span style="font-size:18px;">……
</span><span style="font-size:18px; white-space: pre;">	</span><span style="font-size:18px;">……
}</span>
继承class View并要给出3个公有构造方法,分别带1,2,3个参数,参数类型,顺序必须跟父类一样。第一个构造方法是在代码里直接new该控件需要用到;第二个构造方法是在布局xxx.xml文件里申明该控件,在渲染布局时用到,如果你有默认defStyleAttr可以在此时传给第三个构造方法,没有就传入0;第三个构造方法一般被第二个方法里调用,查看父类View就是这么做的

重写onMeasure方法,该方法是用来精确测量该控件自身大小,测试代码如下:

<span style="font-size:18px; white-space: pre;">	</span><span style="font-size:18px;">@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		int wmode = MeasureSpec.getMode(widthMeasureSpec);
		int hmode = MeasureSpec.getMode(heightMeasureSpec);
		int wsize = MeasureSpec.getSize(widthMeasureSpec);
		int hsize = MeasureSpec.getSize(heightMeasureSpec);
 
		if(wmode==MeasureSpec.EXACTLY){</span><span style="font-size:14px;">//已经是确定的值</span><span style="font-size:18px;">
			mWidth = wsize;
		}else{ 
			//wmode==MeasureSpec.AT_MOST | MeasureSpec.UNSPECIFIED </span><span style="font-size:14px;">//需要计算测量</span><span style="font-size:18px;">
			if(mWidth<=0) 
				mWidth = wsize; 
			else 
				mWidth = Math.min(mWidth, wsize);
		}
		
		if(hmode==MeasureSpec.EXACTLY){
			mHeight = hsize;
		}else{
			//wmode==MeasureSpec.AT_MOST | MeasureSpec.UNSPECIFIED
			if(mHeight<=0) 
				mHeight=hsize;
			else
				mHeight = Math.min(mHeight,hsize);
		}
		
		setMeasuredDimension(mWidth,mHeight);//</span><span style="font-size:14px;">存储</span><span style="font-size:18px;">
		//super.onMeasure(widthMeasureSpec, heightMeasureSpec);
	}</span>
该方法有两个int参数:widthMeasureSpec,heightMeasureSpec都是父容器控件传入的,并要求该子控件,在精确计算自己的大小时必须遵循这两个值的规则。参数里面包含了mode和size两种值,需通过mode才能确定size究竟代表何值,可以通过MeasureSpec.getMode(w)获得mode,MeasureSpec.getSize(w)获取size;当measure完后必需调用setMeasuredDimension(w,h)存储,以备后续步骤可以拿到该值。

查看API,mode有分三种情况,如果mode为MeasureSpec.EXACTLY,说明父容器控件已经可以确定该子控件的大小了,一般当该子控件的属性宽高是一个确定的数值(layout_width=“100dp”)或是fill_parent/match_parent(layout_width="match_parent")时,当然也可以此时修改大小,但一般不做修改;如果mode为MeasureSpec.AT_MOST,说明父容器控件最大可以提供多少空间给该子控件,具体多少还需子控件自己计算测量,一般当该子控件的属性宽高是wrap_content(layout_width="wrap_content")时,此时需要计算,比如text文本超出,换行等等;如果mode为MeasureSpec.UNSPECIFIED,未指明,具体还需子控件自己计算测量,这种情况一般很少见。最后必须setMeasuredDimension(w,h)存储,调用超类的onMeasure可以注释掉。


重写layout方法,该法主要确定该控件的显示(摆放)位置,测试代码如下:

<span style="font-size:18px; white-space: pre;">	</span><span style="font-size:18px;">@Override
	public void layout(int l, int t, int r, int b) {
		ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) getLayoutParams(); //</span><span style="font-size:14px;">外边距margin</span><span style="font-size:18px;">
		mLeft = getPaddingLeft();</span><span style="font-size:14px;">//内边距padding</span><span style="font-size:18px;">
		mTop = getPaddingTop();
		mHeight = b - t - mlp.topMargin-mlp.bottomMargin;
		mWidth = r - l - mlp.leftMargin-mlp.rightMargin;
		super.layout(l, t, r, b);
		postInvalidate();
	}</span>
该方法的四个int参数l,t,r,b是父容器控件传入的。android中所有可见的控件都是用矩形来表示其大小的。四个参数分别代表该子控件在父容器中的位置及大小,可以看做两个坐标点,(l,t)左上角点,(r,b)右下角点,是相对父容器的,控件大小位置便可显而易见。当然在该方法中可以修改自身位置及大小,此时控件还没有绘制(onDraw)。


重写onDraw方法,该方法用来绘制东西的,把控件外观呈现到屏幕上,测试代码如下:

	@Override
	protected void onDraw(Canvas canvas) {
		int R = mWidth / 2;
		int x = mWidth / 2 ;
		int y = mWidth / 2 ;
		canvas.drawCircle(x,y,R, mPaint);
		super.onDraw(canvas);
	}
除非你不想该控件被看见,非则需要重写该方法,在当中绘制一些内容,以便输出到屏幕上。

到此一个简单的自定义控件已经写好了,此控件没有事件,没有输入等,只是在屏幕上绘制一个圆。

当然自定义控件,一般还需要自定义属性,可以在res/values/attrs.xml文件中定义属性:

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <declare-styleable name="test_view_style">
        <attr name="view_color" format="color" />
        <attr name="view_pwid" format="integer" />
    </declare-styleable>
    
</resources>
自定义属性必须放在attrs.xml文件中,标签<declare-styleable >中全是属性了,然后在布局文件中引用,引用的name是<attr />标签的,布局文件最外层控件中还必须配置自定义属性位置 xmlns:cust="http://schemas.android.com/apk/res/com.test.viewgroup" ,cust是前缀可以随便写, apk/res/后面就是包名,然后在控件上就可以使用自定义属性了,前缀+name ,如下:

<com.test.viewgroup.CustomView
        android:layout_width="80dp"
        android:layout_height="80dp"
        android:layout_marginRight="5dp"
        android:layout_marginLeft="5dp"
        android:layout_marginTop="10dp"
        android:layout_marginBottom="10dp"
        android:paddingTop="10dp"
        cust:view_color="#f00"
        />
最后在控件构造方法中需要获取自定义属性值,在传入的参数attrs中已经包含了自定义属性值,获取即可,测试代码如下:

<span style="white-space:pre">	</span>if(attrs==null) return;
	TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.test_view_style);
	for(int i=0;i<ta.getIndexCount();i++){
		int attr = ta.getIndex(i);
		if(attr==R.styleable.test_view_style_view_color){
			mPaint.setColor(ta.getColor(attr, 0));
		}else if(attr==R.styleable.test_view_style_view_pwid){
			mPaint.setStrokeWidth(ta.getInteger(attr, 1));
		}
	}
	//回收对象
	ta.recycle();
代码中R.styleable.test_view_style是attrs.xml文件中<declare-styleable >的name,而属性的Index对应的是R.styleable.test_view_style_view_color,是两个name连起来的。最后记得回收资源,父类View中也是这么做的。

最基本的自定义控件大致就这样,真要写个能用的还真的不简单,可以查看class View代码有20000多行


自定义ViewGroup

android中自带的容器控件很多,常用的布局控件(FrameLayout,RelativeLayout,LinearLayout,TableLayout,AbsoluteLayout)就是最典型的容器控件了,android中容器控件都必须直接或间接继承ViewGroup,从继承关系图可以看出,ViewGroup是抽象类,父类也是View,所有View有的东西它自然也有,只是增加或修改某功能方法等。自定义容器控件也需继承ViewGroup或者继承其他容器控件。

自定义ViewGroup的一般步骤跟自定义View大致一样的:

1、继承class ViewGroup 或 ViewGroup的子类;

2、实现3个构造方法;

3、重写onMeasure方法,一般必须的;

4、重写onLayout方法,抽象方法,必须的

5、重写onDraw方法,可以不重写;


现以自定义CustomLayout控件为例,代码如下:

public class CustomLayout extends ViewGroup {

	Context context ;
	public CustomLayout(Context context) {
		this(context,null);
		this.context = context;
	}
	public CustomLayout(Context context, AttributeSet attrs) {
		this(context, attrs,0);
		this.context = context;
	}
	public CustomLayout(Context context, AttributeSet attrs, int defStyleAttr) {
		super(context, attrs, defStyleAttr);
		this.context = context;
	}
	……
	……
}
以上是继承ViewGroup,接着是三个公有构造方法,这和自定义View是一样的。


重写onMeasure方法,这跟自定义View是一样的,测试代码如下:

<span style="font-size:18px;">	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		int mode = MeasureSpec.getMode(widthMeasureSpec);
		int wsize = MeasureSpec.getSize(widthMeasureSpec);
		int hsize = MeasureSpec.getSize(heightMeasureSpec);

		</span><span style="font-size:14px;">//测量所有子控件大小</span><span style="font-size:18px;">
		measureChildren(widthMeasureSpec,heightMeasureSpec);
		
		</span><span style="font-size:14px;">//没有判断直接用了</span><span style="font-size:18px;">
		</span><span style="font-size:14px;">//…………</span><span style="font-size:18px;">
		</span><span style="font-size:14px;">//存储自身的大小</span><span style="font-size:18px;">
		setMeasuredDimension(wsize,hsize);
 
		//super.onMeasure(widthMeasureSpec, heightMeasureSpec);
	}</span>
这个方法是View类的方法,它跟自定义View一样的,唯一的不同就是不光测量自身容器大小,还需测量容器内所有子控件的大小。当调用measureChildren(int,int)方法,触发所有子控件的onMeasure方法得以执行。


重写onLayout方法,它是抽象方法,所有继承ViewGroup的子类都必须重写该方法。View类的layout没有前面on。类似于布局控件LinearLayout、RelativeLayout……它们各自有不同的摆放方式,最主要还是onLayout方法实现上不一样。这里的子控件都是上面自定义控件CustomView。测试代码如下:

<span style="font-size:18px; white-space: pre;">	</span><span style="font-size:18px;">@Override
	protected void onLayout(boolean changed, int l, int t, int r, int b) {
		</span><span style="font-size:14px;">//这里实现类似于流式布局从上往下,从左至右排列子控件
		   //获取测量后自身的大小</span><span style="font-size:18px;">
		int selfWidth = getMeasuredWidth();
		int selfHeight = getMeasuredHeight();
		</span><span style="font-size:14px;">//当前父容器中的光标位置</span><span style="font-size:18px;">
		int x = l;
		int y = t;
		
		</span><span style="font-size:14px;">//遍历所有子控件</span><span style="font-size:18px;">
		for(int i=0;i<getChildCount();i++){
			View child = getChildAt(i);
			if(child.getVisibility()==GONE) return;
			</span><span style="font-size:14px;">//获取测量后子控件的大小</span><span style="font-size:18px;">
			int childWidth = child.getMeasuredWidth();
			int childHeight = child.getMeasuredHeight();
			
			</span><span style="font-size:14px;">//获取子控件外边距 </span><span style="font-size:18px;">
			ViewGroup.MarginLayoutParams mlp =  (MarginLayoutParams) child.getLayoutParams();
			int childMarginTop = mlp.topMargin;
			int childMarginLeft = mlp.leftMargin;
			int childMarginRight = mlp.rightMargin;
			int childMarginBottom = mlp.bottomMargin;
			
			childWidth +=childMarginLeft+childMarginRight;
			childHeight +=childMarginTop+childMarginBottom;
			if(x+childWidth <=selfWidth){
				//
			}else{
				y+=childHeight;
				x=l;
			}
			</span><span style="font-size:14px;">//触发子控件的layout方法</span><span style="font-size:18px;">
			child.layout(x, y, childWidth+x, childHeight+y);
			x+=childWidth;
			postInvalidate();
 		}
	}</span>
这里onLayout方法实现类似于流式布局,从上往下,从左至右,依次排列子控件。当然为了简单起见,这里只是实现一些基本功能而已。只有控件测量过后并调用了setMeasuredDimension方法,才能使用getMeasuredWidth()方法获取到测试后的值,否则调用该方法返回0,获取子控件的大小也是一样的,因为也测量过了。这里获取子控件的外边距后,需计算到控件的大小里面,在子控件真正绘制时又需减去这部分大小。

在获取子控件的外边距是需要类型转换 ViewGroup.MarginLayoutParams mlp =  (MarginLayoutParams) child.getLayoutParams(); 所以容器控件必须重写如下方法:

<span style="font-size:18px;">	@Override
	public LayoutParams generateLayoutParams(AttributeSet attrs) {
		</span><span style="font-size:14px;">//返回MarginLayoutParams对象</span><span style="font-size:18px;">
		return new MarginLayoutParams(context,attrs);
	}</span>


因为只有MarginLayoutParams类中可以获取到外边距,它是ViewGroup的内部类。默认情况下view.getLayoutParams()返回的是ViewGroup.LayoutParams类型的对象,查看源码可以看到MarginLayoutParams继承于ViewGroup.LayoutParams,但是在java语言中父类向子类转换是不被允许的, 这里为什么可以转换?假若在Activity中new CustomView() 并添加到CustomLayout中需调用AddView(),查看源代码发现在ViewGroup.addViewInner()方法中,有如下代码:

<span style="white-space:pre">	</span>if (!checkLayoutParams(params)) {
       <span style="white-space:pre">	</span>params = generateLayoutParams(params);
   <span style="white-space:pre">	</span>}

     if (preventRequestLayout) {
         child.mLayoutParams = params;
     } else {
         child.setLayoutParams(params);
     }
若params为null就调用generateLayoutParams(params);只有在Activity中AddView()时传入了params,否则一般情况下此时params都为null;因为重写了该方法,子类有的调子类的,没有则调父类的。后又赋值给子控件child.setLayoutParams(params)。在代码中若要动态向容器控件添加子控件,若传入params,需要new MarginLayoutParams()其子类对象,否则报类型转换异常。如下:

<span style="white-space:pre">	</span>CustomView v = new CustomView(this);
	v.setLayoutParams(new ViewGroup.MarginLayoutParams(90,90));
<span style="white-space:pre">	</span>custlayout.addView(v);
所以child.getLayoutParams()返回的就是MarginLayoutParams或其子类对象,当然可以类型转换了;

最后就是onDraw方法了,这个跟自定义View一样的,向屏幕呈现图画。

也可以自定义属性,跟View是一样的。运行结果一张图片:


以上都是android自定义控件大致过程理解分析,具体细节不太明白,需待研究。






评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值