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方法,可以不重写;
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自定义控件大致过程理解分析,具体细节不太明白,需待研究。