从本篇开始,我们来讲述自定义ViewGroup的相关知识。本系列还是来自 李赞红 老师的《Android自定义组件开发详解》,书看完了,如果仅仅是看完了,没有任何输出的话,那我们对书中知识的吸收只怕是少得可怜。只有从输入–》输出–》应用 这样形成一个闭环,我们才能说自己理解这个这个知识点,也才能将这个知识点列入自己的知识框架中。
废话不多说了,我们现在就进入自定义ViewGroup的学习吧!
一般情况下,自定义View,更关注的是组件的外观和功能。比如我们前面自定义view系列中的圆形头像、验证码等等;而自定义ViewGroup,关注的则是容器内的组件怎么排列和摆放,比如LinearLayout的组件只能水平或者垂直排列,帧布局FrameLayout中的组件可以重叠。。。
ViewGroup作为容器类的父类,有它自己鲜明的特征,我们必须先要了解下。
在ViewGroup中定义了一个View[]类型的数组 mChildren,该数组保存了容器中所有的子组件,负责维护组件的add、remove、组件顺序等功能。
相关API如下:
- public int getChildCount()
- public View getChildAt(int index)
- public void addView(View child, int index, LayoutParams params)
- public void addView(View child, int index)
- public void addView(View child)
- public void removeViewAt(int index)
- public void removeView(View view)
- protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec)
ViewGroup运行的基本流程大致为:
1, 测量容器尺寸
重写onMeasure()方法测量容器大小,和自定义View的区别是,在测量容器大小之前,必须先调用measureChildren()方法测量所有子组件的大小。
// measureChildren ==》measureChild
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);//*******
}
}
}
// measureChild 中的关键方法getChildMeasureSpec
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
根据父布局的模式和大小, 辅助子布局的模式和大小,一起来确认子布局的模式和大小(resultSize和resultMode)
2,确定每个子组件的位置(自定义ViewGroup的难点部分)
重写onLayout()方法确定每个子组件的位置,在onLayout中,调用View的layout()确定自组件的位置。
3,绘制容器
重写onDraw()方法,其实ViewGroup类并没有重写onDraw(),除非特殊要求,比如Linearlayout重写了该方法用于绘制水平或垂直分割条。
参照上面的步骤,我们给出一个简单的自定义viewGroup,叫做SizeViewGroup。
注意:SizeViewGroup只是为了说明自定义VierGroup的思路,其本身不具有实际意义。
我们假定这个SizeViewGroup是一个带红色边框的,且是圆角的矩行,其有一个TextView类型的子view。
public class SizeViewGroup extends ViewGroup {
public SizeViewGroup(Context context) {
this(context,null,0);
}
public SizeViewGroup(Context context, AttributeSet attrs) {
this(context,attrs,0);
}
public SizeViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs,defStyleAttr);
//new 子view
TextView textView = new TextView(context);
//设置子view的大小
ViewGroup.LayoutParams layoutParams = new LayoutParams(200,200);
textView.setText("Android");
textView.setBackgroundColor(Color.YELLOW);
//将子view添加进来
addView(textView,layoutParams);
this.setBackgroundColor(Color.alpha(255));
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
//定位子view
View textView = getChildAt(0);
textView.layout(50,50,textView.getMeasuredWidth() + 50, textView.getMeasuredHeight() + 50);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//测量子View
this.measureChildren(widthMeasureSpec, heightMeasureSpec);
//设置SizeViewGroup自身大小(即使在layout.xml文件中指定了layout_width/layout_height,也没有效果了。)
setMeasuredDimension(500,500);
}
@Override
protected void onDraw(Canvas canvas) {
//绘制SizeViewGroup自身的边框效果
RectF rect = new RectF(0,0,getMeasuredWidth(),getMeasuredHeight());
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(2);
paint.setColor(Color.RED);
Path path = new Path();
path.addRoundRect(rect,20,20,Path.Direction.CCW);
canvas.drawPath(path,paint);
super.onDraw(canvas);
}
}
运行后SizeVieGroup的效果如下:
我们在xml中是这样定义SizeViewGroup的
<com.example.cyy.customerview.view.SizeViewGroup
android:layout_width="10dp"
android:layout_height="10dp"/>
看,即使我们给出的具体宽高值,但是由于在java的onMeasure()方法中,直接设置setMeasuredDimension(500,500);xml中的值就会被忽略。因此,我们也得知,真正决定控件大小的不是xml中设置的值,而是measure方法。
DONE
此系列后续持续更新。