我们在上一篇文章《Android中的自定义View(一)》中介绍过自定义View的几种方式,并通过示例一演示了“直接继承View”的自定义View的使用情况。今天接着来介绍自定义View里稍为复杂的“直接继承ViewGroup”的使用情况。一般地,直接继承ViewGroup的自定义View需要我们自己去处理ViewGroup的measure和layout这两个过程,并同时处理子元素的measure和layout过程,而且还要考虑padding和子元素的margin对其造成的影响。
其实可以简单地理解成,measure过程就是在重写onMeasure中实现,此方法就是为了能让我们自定义的ViewGroup的wrap_content属性可生效,并循环子元素个数,分别通过measureChild方法将measure要处理的事情传递到子元素。而layout过程就是重写onLayout中实现,原理类似地循环和通过childView.layout方法确定子元素出现的位置。更多measure和layout的知识,详细可以看回之前的文章《View的工作原理(二)之 View的工作流程》 。
那么下面我们就通过流式布局示例来一步步地分析和理解直接继承ViewGroup的自定义View的过程。首次,我们先来认识一下什么样的布局叫流式布局。请看下图,子元素在父元素内部像排队一样,从左到右像排队一般跟在后排,若单行位置不够就直接换行到下一行左边,然后同样道理接着从左到右一直排下去。
public class StreamLayout extends ViewGroup {
publicStreamLayout(Context context) {
super(context);
}
publicStreamLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
publicStreamLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protectedvoid onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protectedvoid onLayout(boolean changed, int left, int top, int right, int bottom) {
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// A、当前ViewGroup的宽和高的MeasureSpec值
int widthSpaceSize = MeasureSpec.getSize(widthMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSpaceSize = MeasureSpec.getSize(heightMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
// B、当前ViewGroup的Padding值
int paddingLeftSize = getPaddingLeft();
int paddingRightSize = getPaddingRight();
int paddingTopSize = getPaddingTop();
int paddingBottomSize = getPaddingBottom();
// C、用于记录单行的动态宽和高
int singleLineWidth = 0;
int singleLineHeight = 0;
// D、根据内容决定的动态宽和高
int wrapContentWidth = 0;
int wrapContentHeight = 0;
for(int i = 0; i < getChildCount(); i ++) {
View childView = getChildAt(i);
// E、子View是隐藏的,跳过
if (childView.getVisibility() == View.GONE) {
continue;
}
// F、子View的measure
measureChild(childView, widthMeasureSpec, heightMeasureSpec);
// G、获得子View + 外边框空出的宽和高
MarginLayoutParams childMarginLayoutParams = (MarginLayoutParams)childView.getLayoutParams();
int childWidth = childMarginLayoutParams.leftMargin +childView.getMeasuredWidth() + childMarginLayoutParams.rightMargin;
int childHeight = childMarginLayoutParams.topMargin +childView.getMeasuredHeight() + childMarginLayoutParams.bottomMargin;
// H、换行情况
if (singleLineWidth + childWidth > widthSpaceSize - paddingLeftSize -paddingRightSize) {
// 更新最终想要结果wrapContentWidth 和 wrapContentHeight 的值,高要累加;宽取最宽
wrapContentWidth = Math.max(wrapContentWidth,singleLineWidth);
wrapContentHeight +=singleLineHeight;
// 重置singleLineWidth 和 singleLineHeight
singleLineWidth = 0;
singleLineHeight = 0;
}
// I、更新本行的宽和高
singleLineWidth += childWidth;
singleLineHeight = Math.max(singleLineHeight, childHeight);
// J、最后一个,跟换行情况一样处理
if (i == getChildCount() -1) {
wrapContentWidth =Math.max(wrapContentWidth, singleLineWidth);
wrapContentHeight +=singleLineHeight;
}
}
// K、最终结果加上padding值
wrapContentWidth += (paddingLeftSize + paddingRightSize);
wrapContentHeight += (paddingTopSize + paddingBottomSize);
// 最宽和最高不能大于可容纳的宽和高
if (wrapContentWidth > widthSpaceSize) {
wrapContentWidth = widthSpaceSize;
}
if (wrapContentHeight > heightSpaceSize) {
wrapContentHeight = heightSpaceSize;
}
// L、设置当前ViewGroup的wrap_Content的值
if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode ==MeasureSpec.AT_MOST) {
setMeasuredDimension(wrapContentWidth, wrapContentHeight);
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(wrapContentWidth, heightSpaceSize);
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpaceSize, wrapContentHeight);
}
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
1、A处是onMeasure方法接收宽和高的MeasureSpec值,我们在《View的工作原理(一)之View的三大过程和 认识MeasureSpec》 中介绍也过MeasureSpec高2位代表SpecMode,低30位代表SpecSize,此两个值就是用于测量出View的宽和高。在这先获取width和height它们的spaceSize和spaceMode,然后在L处判断spaceMode若果是非AT_MOST,则传递spaceSize值,否则传递我们通过计算得出的wrapContent值来使wrap_content属性生效。我们在上篇文章中示例一也是这样的做法。
2、我们自定义的View还要考虑padding和子元素的margin对整体子元素排队造成的影响,所以B处先获得当前ViewGroup的Padding值。
3、C处用于记录单行的动态宽和高,而D处理而是我们最终想要计算的动态宽和高的结果。
4、接着是循环子无素个数,E处是针对隐藏的子元素不作任何处理。
5、F处就是上面说的处理子元素的measure,它的参数是子元素对象和宽高的MeasureSpec值。
6、G处是获得子元素加上它Margin的值。
7、在遍历子元素过程中,获得了子元素和它的Margin值后就开始将其值记录,这就是I处做的事情。其中,宽是递增,而高是拿当行最高。
8、当遇到余下的位置放不下当前子元素时,便要处理换行情况,H处进行了余下位置的判断。其中会将自定义View自身的paddingLeft 和 paddingRight 两个值进行了考虑。此时就是要更新最终结果的两个变量wrapContentWidth和wrapContentHeight,宽要拿历史最宽,因为自定义View最终的宽度是以最宽的那行来决定,而高此时就需要递增。
9、J处要处理最后一个子元素,此处逻辑跟换行情况一样就可以。因为最后的子元素无论处于什么位置都要换行结束当前的行的宽和高。
10、K处就是将通过循环计算出来的结果加下自定义View自身的padding值,便就是最终要传递的wrap_content的值。
11、请注意,上面代码除了重写了onMeasure方法外,还重写了generateLayoutParams方法,此方法的作用是构造MarginLayoutParams对象,因为只有MarginLayoutParams对象才可以获得子元素所Margin的值。如若少了这步,便会在G处地方发生异常。
@Override
protected void onLayout(boolean changed, intleft, int top, int right, int bottom) {
// 当前ViewGroup的Padding值
int paddingLeftSize = getPaddingLeft();
int paddingRightSize = getPaddingRight();
int paddingTopSize = getPaddingTop();
// 开始的坐标
int startLeft = 0;
int startTop = 0;
// 用于记录单行的高
int singleLineHeight = 0;
for(int i = 0; i < getChildCount(); i ++) {
final View childView = getChildAt(i);
if (childView.getVisibility() == View.GONE) {
continue;
}
// 获得子View + 外边框空出的宽和高
MarginLayoutParams childMarginLayoutParams = (MarginLayoutParams)childView.getLayoutParams();
int childWidth = childMarginLayoutParams.leftMargin +childView.getMeasuredWidth() + childMarginLayoutParams.rightMargin;
int childHeight = childMarginLayoutParams.topMargin +childView.getMeasuredHeight() + childMarginLayoutParams.bottomMargin;
// 换行情况
if (startLeft + childWidth > getWidth() - paddingLeftSize -paddingRightSize) {
startLeft = 0;
startTop += singleLineHeight;
singleLineHeight = 0;
}
// 子View的layout
int childLeft = paddingLeftSize + startLeft +childMarginLayoutParams.leftMargin;
int chileTop = paddingTopSize + startTop +childMarginLayoutParams.topMargin;
int chileRight = childLeft + childView.getMeasuredWidth();
int chileBottom = chileTop + childView.getMeasuredHeight();
childView.layout(childLeft, chileTop, chileRight, chileBottom);
// 更新开始左坐标
startLeft += childWidth;
// 更新行高
singleLineHeight = Math.max(singleLineHeight, childHeight);
}
}
onLayout实现的思想跟onMeasure一样,它需要循环对每个子元素进行位置的确定,而每个子元素的位置就是通过它前一个子元素位置来决定排在后边或换行到下一行的第一位,这里就不作过多的解释了,大家可以直接看代码。
完整代码点击此处下载