自定义ViewGroup
本文是读了《 Android 群英传》第三章--Android体控件架构与自定义空间详解--之后的读书笔记,感谢作者,在此特别推荐此书。
之前说过了自定义View,而对于自定义ViewGroup来说,主要是定义和管理该ViewGroup里的子View。所以自定义ViewGroup里往往需要重写onMearsure()方法来完成对各子View的测量,重写onLayout()方法来完成对各子View的位置布局,重写onTouchEvent()来完成响应事件。至于为啥没有说onDraw()方法呢,其实在draw阶段,ViewGroup都是通过drawchild方法调用子View的draw方法来绘制各子View,而作为各子View的容器,一般情况下不会再做额外的修饰。所以往往在绘制阶段里,只需要调用ViewGroup的默认实现方法就可以了。
以一个简单的ViewGroup为例子吧,我打算做一个含有三个控件,并且三个控件斜着排在我们自定义ViewGroup中。首先依然是新建一个类继承ViewGroup,写好构造函数,并给它添加上三个子View。
public MyViewGroup(Context context) {
super(context);
mContext = context ;
init() ;
}
public MyViewGroup(Context context , AttributeSet attrs){
super(context,attrs) ;
mContext = context ;
init() ;
}
//为MyViewGroup添加三个子View
private void init(){
//调用ViewGroup父类addView()方法添加子View
//child 对象一 : Button
Button btn= new Button(mContext) ;
btn.setText("I am Button") ;
this.addView(btn) ;
//child 对象二 : ImageView
ImageView img = new ImageView(mContext) ;
img.setBackgroundResource(R.mipmap.ic_launcher) ;
this.addView(img) ;
//child 对象三 : TextView
TextView txt = new TextView(mContext) ;
txt.setText("Only Text") ;
this.addView(txt) ;
}
然后是onMearsure()方法,遍历每个子View进行测量,获取每个子View的实际宽,高,然后计算出我们自定义ViewGroup需要的宽和高。
@Override
//对每个子View进行measure():设置每子View的大小,即实际宽和高
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
int childCount = getChildCount() ;
Log.i(TAG, "**** the size of this ViewGroup is ----> " + childCount+" ****") ;
Log.i(TAG, "**** onMeasure start *****") ;
//获取该ViewGroup的实际长和宽 涉及到MeasureSpec类的使用
int specSize_Width = MeasureSpec.getSize(widthMeasureSpec) ;
int specSize_Height = MeasureSpec.getSize(heightMeasureSpec) ;
Log.i(TAG, "**** specSize_Width=== " + specSize_Width+ " * specSize_Height===" + specSize_Height+" ****") ;
int new_Width=0;
int new_Height=0;
for(int i=0 ;i<childCount ; i++){
View child = getChildAt(i) ; //获得每个对象的引用
//或者可以调用ViewGroup父类方法measureChild()或者measureChildWithMargins()方法
this.measureChild(child, widthMeasureSpec, heightMeasureSpec) ;
new_Width+=child.getMeasuredWidth()+10;
new_Height+=child.getMeasuredHeight()+10;
}
Log.i(TAG, "**** new_Width=== " + new_Width+ " * new_Height===" + new_Height+" ****") ;
//设置本ViewGroup的宽高
setMeasuredDimension(new_Width , new_Height) ;
}
接下来就是布局每个子View了,设置好每个子View的起始位置以及长宽。
@Override
//对每个子View视图进行布局
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// TODO Auto-generated method stub
int childCount = getChildCount() ;
int startLeft = 0 ;//设置每个子View的起始横坐标
int startTop = 0 ; //设置每个子View的起始纵坐标;
Log.i(TAG, "**** onLayout start ****") ;
for(int i=0 ;i<childCount ; i++){
View child = getChildAt(i) ; //获得每个对象的引用
child.layout(startLeft, startTop, startLeft+child.getMeasuredWidth(), startTop+child.getMeasuredHeight()) ;
startLeft =startLeft+child.getMeasuredWidth() + 10; //校准startLeft值,View之间的横向间距设为10px ;
startTop =startTop+child.getMeasuredHeight() + 10; //校准startTop值,View之间的高度间距设为10px ;
Log.i(TAG, "**** onLayout startLeft===" +startLeft+" ****") ;
Log.i(TAG, "**** onLayout startTop===" +startTop+" ****") ;
}
}
最后我们还是写上绘制方法,仅仅来观察下整个流程。
protected boolean drawChild(Canvas canvas , View child, long drawingTime){
Log.i(TAG, "**** drawChild start ****") ;
return super.drawChild(canvas, child, drawingTime) ;
}
我们看下运行结果:
看看我们的运行日志:
书中举的例子包含了点击事件,其实就是重写onTouchEvent()方法,监听触摸点击写出响应事件,这里我也写了个简单例子,代码如下所示:
@Override
public boolean onTouchEvent(MotionEvent event){
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
child.layout((int)event.getX()-child.getMeasuredWidth()/2,(int)event.getY()-child.getMeasuredHeight()/2,
(int)event.getX()+child.getMeasuredWidth()/2,(int)event.getY()+child.getMeasuredHeight()/2);
break;
case MotionEvent.ACTION_MOVE:
child.layout((int)event.getX()-child.getMeasuredWidth()/2,(int)event.getY()-child.getMeasuredHeight()/2,
(int)event.getX()+child.getMeasuredWidth()/2,(int)event.getY()+child.getMeasuredHeight()/2);
break;
}
postInvalidate();
return true;
}
效果就是我点击以及滑动屏幕时候,child这个控件会跟着我手指头移动,在这就不贴图了。