写过自定义ViewGroup的都知道,当自定义一个类继承ViewGroup之后,必须要重写的一个方法就是onLayout。
那么onLayout有什么样的作用呢?为什么自定义ViewGroup就需要重写该方法,自定义View则不需要重写该方法?
疑问出在ViewGroup的onLayout里,那我们就从这里入手,逐一去分析各中原由。
首先,进入到ViewGruop代码我们看到onLayout()是一个抽象方法,所以在子类实现中我们必须要重写该方法,并且是重写于父类的方法,
所以该方法的具体调用应该是在父类中。
@Override
protected abstract void onLayout(boolean changed,
int l, int t, int r, int b);
在ViewGroup中还有一个方法layout,是被final修饰的,说明该方法不能被子类重写。
public final void layout(int l, int t, int r, int b) {
if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
if (mTransition != null) {
mTransition.layoutChange(this);
}
super.layout(l, t, r, b);
} else {
// record the fact that we noop'd it; request layout when transition finishes
mLayoutCalledWhileSuppressed = true;
}
}
这里主要关注它实际上只是调用了父类的layout方法,为了让代码看起来更加清晰,这里只贴出View的layout关键的代码。
public void layout(int l, int t, int r, int b) {
......
/*setFrame实际上就是根据四个坐标值设置了当前View本身的layout,所以View的layout已经自己设置好了自己的位置,
*ViewGroup实际继承自View,通过调用父类的layout也完成了自身layout的设置。
*/
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
//接着我们看到,在layout内,在设置完自身的layout之后调用了onLayout,而onLayout就是在我们自定义ViewGroup时需要去实现的方法
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
}
......
}
其实,onLayout就是ViewGroup用来设置内部子View的位置的方法,在onLayout方法内,需要依次遍历childView,并调用childView的layout(l, t, r, b)方法来为每个子View设置具体的布局位置,参数为左上右下四个坐标值。那么这四个坐标值如何设置呢?此处四个坐标位置理论上是可以随便设置的,但是为了达到对应的布局效果,通常是根据measure测量之后得到的View的尺寸来计算相应的坐标值。这里我通过模拟LinearLayout的横向布局和纵向布局的onLayout方法来看下具体怎么去写onLayout。
(补充一点:onMeasure的作用:测量View的尺寸大小,为layout(View的布局位置)提供参考(这里用参考来描述是因为layout(l,t,r,b)
的几个坐标值是可以随便设置的,只是随便设置可能布局比较乱或者可能会出现重叠等等,所以一般还是根据布局需求,参考view的尺寸进行设置))
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mOrientation == LinearLayout.VERTICAL) {
layoutVertical(l, t, r ,b);//纵向LinearLayout
} else {
layoutHorizontal(l, t, r ,b);//横向LinearLayout
}
}
private void layoutVertical(int l, int t, int r, int b) {
int top = 0;
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
int width = childView.getMeasuredWidth();
int height = childView.getMeasuredHeight();
//纵向位置均为从0到height,横向需要不断累加width(上一个子view的左下点为当前子view的左上顶点)
childView.layout(0, top, width, top + height);
top += height;
}
}
private void layoutHorizontal(int l, int t, int r, int b) {
int left = 0;
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View childView = getChildAt(i);
int width = childView.getMeasuredWidth();
int height = childView.getMeasuredHeight();
//横向位置均为从0到width,纵向需要不断累加height(上一个子View的右上顶点为当前View的左上顶点)
childView.layout(left, 0, left + width, height);
left += width;
}
}
在onLayout方法中,设置layout的四个坐标值(左上右下)时,为了达到需要的布局排列效果,通常是根据子View的尺寸来计算四个坐标值。
在上面的例子中,可以看到,通过getMeasuredWidth()和getMeasuredHeight()来分别获取到了childView的寬高。在开发过程中,我们可能还会看到这样的两个方法getWidth()和getHeight(),这也是获得View寬高的方法,那么二者有什么区别吗?上面onLayout可以使用getWidth()和getHeight()来代替getMeasuredWidth()和getMeasuredHeight()吗?
答案是不行!你可以试试看。
下面我们就来看看getMeasuredWidth()、getMeasuredHeight()与getWidth()、getHeight()存在什么样的差别。
二话不说上源码,相信一看代码你就立马明白了。
public final int getMeasuredWidth() {
return mMeasuredWidth & MEASURED_SIZE_MASK;
}
public final int getMeasuredHeight() {
return mMeasuredHeight & MEASURED_SIZE_MASK;
}
这里看过View的测量onMeasure方法原理的一看就能看明白此处是如何获取的寬高,这里简单说一下mMeasuredWidth、mMeasuredHeight两个值便是onMeasure方法设置最终的View的尺寸大小的方法setMeasuredDimension(int measuredWidth, int measuredHeight)传入的两个值。详细的onMeasure的原理可以移步自定义View之onMeasure原理解析
下面再看下getWidth()和getHeight()
public final int getWidth() {
return mRight - mLeft;
}
public final int getHeight() {
return mBottom - mTop;
}
这一看left、right、top、bottom就能立马想到这就是layout方法传入的四个坐标值,不信你可以看看,它在setFrame(int left, int top, int right, int bottom)
里赋的值。而setFrame()不就是之前说过的设置自身布局位置的方法么。