下面通过复写一个ViewGroup来增加自己对View的工作流程的理解。理解view的测量、布局和绘制流程。
先上代码吧,然后一行一行了来分析。
public class HorizontalScrollViewEx extends ViewGroup {
...
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int measuredWidth = 0;
int measuredHeight = 0;
final int childCount = getChildCount();
measureChildren(widthMeasureSpec, heightMeasureSpec);
int widthSpaceSize = MeasureSpec.getSize(widthMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSpaceSize = MeasureSpec.getSize(heightMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
if (childCount == 0) {
setMeasuredDimension(0, 0);
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
final View childView = getChildAt(0);
measuredHeight = childView.getMeasuredHeight();
setMeasuredDimension(widthSpaceSize, childView.getMeasuredHeight());
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
final View childView = getChildAt(0);
measuredWidth = childView.getMeasuredWidth() * childCount;
setMeasuredDimension(measuredWidth, heightSpaceSize);
} else {
final View childView = getChildAt(0);
measuredWidth = childView.getMeasuredWidth() * childCount;
measuredHeight = childView.getMeasuredHeight();
setMeasuredDimension(measuredWidth, measuredHeight);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childLeft = 0;
final int childCount = getChildCount();
mChildrenSize = childCount;
for (int i = 0; i < childCount; i++) {
final View childView = getChildAt(i);
if (childView.getVisibility() != View.GONE) {
final int childWidth = childView.getMeasuredWidth();
mChildWidth = childWidth;
childView.layout(childLeft, 0, childLeft + childWidth,
childView.getMeasuredHeight());
childLeft += childWidth;
}
}
}
...
}
这是一个水平滑动容器,里面存放上下滑动的容器,从而页面可以左右翻页,上下滑动查看更多元素。
一、测量 onMeasure()
1.1 为什么要复写onMeasure()函数
首先来看onMeasure()测量函数,它用来测量viewgroup及其内部子view的大小。我们知道如果直接继承View,必须复写onMeasure()方法。我们这里继承的是View的子类ViewGroup,查看源码发现ViewGroup并没有复写View的onMeasure(),所以直接继承ViewGroup的子类也必须继承 onMeasure()方法,并在该函数内部对layoutPrams==Wrap_content这种情况实现自定义处理。如果不处理, 则即便子view的layoutPrams==Wrap_content,那么子view实际的大小是父容器的剩余大小。
下面上View的源码来解释上面。当子View的layoutPrams==Wrap_content时,其specMode=AT_MOST,getDefault()中返回的是specSize, 这个值是父容器剩余的空间大小。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
1.2 自定义onMeasure()内部逻辑
这部分是测量viewgroup自己及其内部所有子view的大小。
1、因为我们只需要修改View的onMeasure()中AT_MOST的逻辑,其他的不需要改变。所以
首先执行super.onMeasure(widthMeasureSpec, heightMeasureSpec);保持原有的逻辑;
2、接下来针对AT_MOST的情况特殊处理。
2.1 、获取viewgroup内部子view数量;---viewgroup才需要
2.2、测量所有内部子view大小;---viewgroup才需要
2.3、获取viewgroup宽和高的specMode和specSize;
2.4、对viewgroup的specMode=AT_MOST特殊处理
2.4.1、childCount==0,设置viewgroup大小为setMeasuredDimension(0, 0);
2.4.2、widthSpecMode == MeasureSpec.AT_MOST
HorizontalScrollViewEx假定所有子view的宽高一致。所以获取下标0的子view,得到它的
宽度,”子view数量*子view宽度“就是viewgroup的总宽度。调用setMeasuredDimension(measuredWidth, heightSpaceSize);设置viewgroup的尺寸。
2.4.3、heightSpecMode == MeasureSpec.AT_MOST 逻辑同上;
2.4.4、其他情况。
二、布局 onLayout()
其内部逻辑为:
1、获取viewgroup内部所有子view数量;
2、for循环遍历所有子view,拿到每个子view对象进行布局处理。如果子view可见,获取子view的宽度值,调用
childView.layout(childLeft, 0, childLeft + childWidth,
childView.getMeasuredHeight());
根据给出的左上和右下两个点的位置来布局子view。上面的点的x,y方向的大小是以viewgroup左上角为坐标系原点求得的。