View的layout源码分析
我们在View的measure一文中学习了View的measure,这篇文章我们将来学习View的layout,就是View的摆放。我们先来看View,在看ViewGroup的。
单一View的layout
我们直接来看源码:
public void layout(int l, int t, int r, int b) {
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
/**
*调用setOpticalFrame或setFrame方法来确定当前View自身的位置,并返回View自身是否变化。
*/
boolean changed = isLayoutModeOptical(mParent) ?setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
/**
*/确定当前View的所有子View的位置。
*/
onLayout(changed, l, t, r, b);
if (shouldDrawRoundScrollbar()) {
if(mRoundScrollbarRenderer == null) {
mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
}
} else {
mRoundScrollbarRenderer = null;
}
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLayoutChangeListeners != null) {
ArrayList<OnLayoutChangeListener> listenersCopy =
(ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
int numListeners = listenersCopy.size();
for (int i = 0; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
}
}
}
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) {
mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
notifyEnterOrExitForAutoFillIfNeeded(true);
}
}
方法的四个参数 l ,t ,r ,b分别是当前View从左,上,右,下相对于其父容器的距离。isLayoutModeOptical(mParent)返回出true就调用setOpticalFrame方法,false就调用setFrame方法。我们来看setOpticalFrame的源码:
private boolean setOpticalFrame(int left, int top, int right, int bottom) {
Insets parentInsets = mParent instanceof View ?
((View) mParent).getOpticalInsets() : Insets.NONE;
Insets childInsets = getOpticalInsets();
return setFrame(
left + parentInsets.left - childInsets.left,
top + parentInsets.top - childInsets.top,
right + parentInsets.left + childInsets.right,
bottom + parentInsets.top + childInsets.bottom);
}
我们发现,setOpticalFrame方法里面其实调用的是setFrame方法,我们再看这个方法源码:
protected boolean setFrame(int left, int top, int right, int bottom) {
boolean changed = false;
if (DBG) {
Log.d("View", this + " View.setFrame(" + left + "," + top + ","
+ right + "," + bottom + ")");
}
/**
*判断当前VIew的,位置是否发现变化
*/
if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
changed = true;// 当前View的位置发生变化的标志。
int drawn = mPrivateFlags & PFLAG_DRAWN;
int oldWidth = mRight - mLeft;// 原来的宽
int oldHeight = mBottom - mTop;// 原来的高
int newWidth = right - left; // 现在的宽
int newHeight = bottom - top; // 现在的高
// 大小是否变化
boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
invalidate(sizeChanged);
mLeft = left;// 把当前的自己的位置跟新保存下来
mTop = top;
mRight = right;
mBottom = bottom;
mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
mPrivateFlags |= PFLAG_HAS_BOUNDS;
if (sizeChanged) {
sizeChange(newWidth, newHeight, oldWidth, oldHeight);
}
if ((mViewFlags & VISIBILITY_MASK) == VISIBLE || mGhostView != null) {
mPrivateFlags |= PFLAG_DRAWN;
invalidate(sizeChanged);
invalidateParentCaches();
}
mPrivateFlags |= drawn;
mBackgroundSizeChanged = true;
mDefaultFocusHighlightSizeChanged = true;
if (mForegroundInfo != null) {
mForegroundInfo.mBoundsChanged = true;
}
notifySubtreeAccessibilityStateChangedIfNeeded();
}
return changed;
}
当前View的自身位置确定后,如果当前View的位置发生了变化,就会接着调用onLayout(changed, l, t, r, b)方法,我们来看View的onLayout方法的源码:
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
方法是空的,没有方法体。为什么呢?经过上面的介绍,layout方法确定了当前VIew的自身位置,那onLayout方法是干嘛用的呢?这个方法是用来确定当前View的所有子View的位置。这样我就知道View 的onLayout方法为什么是个空方法了,因为对于单一View来说,是没有子View的,所有不需要有方法。
总结
单一View的layout我们就学习到这。我们再看ViewGroup的。
ViewGroup的layout
我们来看ViewGroup的源码:
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);// 调用父View的ayout方法。
} else {
mLayoutCalledWhileSuppressed = true;
}
}
我们看到,在ViewGroup的layout方法中,调用他父View的layout方法,而ViewGroup是继承View的,所以最终会调用到View的layout方法中。所以ViewGroup也一样,在layout方法中,确定当前ViewGroup的自身的位置。若大小或位置发生了变化,接着会调用onLayout方法。我们在看ViewGroup的onLayout方法源码:
protected abstract void onLayout(boolean changed,int l, int t, int r, int b);
是个抽象方法,即我们在自定义ViewGroup类型的view时,我们必须要重写这个onLayout方法。为什么这样设计呢?我们知道ViewGroup一般都是有子View,且不同的ViewGroup中的子View的摆放位置的方式不一样,比如LinearLayout,RelativeLayout,FrameLayout他们子View的摆放规则都不一样。所以我们在自定义ViewGroup类型的必须要重写这个onLayout方法,根据自己的要求来摆放子View的位置,把它定义为抽象的最好不过。
总结
自定义ViewGroup步骤:
1.计算自身ViewGroup的位置:layout()。
2.确定所有子View在当前ViewGroup的位置。onLayout()。根据自己的需要来计算出每个子View的位置,即子View在当前ViewGroup的位置(left,top,right,botton),然后调用子View的layout,完成子View的摆放。
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
// 参数说明
// changed 当前View的大小和位置改变了
// left 左部位置
// top 顶部位置
// right 右部位置
// bottom 底部位置
// 1. 遍历子View:循环所有子View
for (int i=0; i<getChildCount(); i++) {
View child = getChildAt(i);
// 2. 计算当前子View的四个位置值
// 2.1 位置的计算逻辑
...// 需自己实现,也是自定义View的关键
// 2.2 对计算后的位置值进行赋值
int mLeft = Left
int mTop = Top
int mRight = Right
int mBottom = Bottom
// 3. 根据上述4个位置的计算值,设置子View的4个顶点:调用子view的layout() & 传递计算过的参数
// 即确定了子View在父容器的位置
child.layout(mLeft, mTop, mRight, mBottom);
// 该过程类似于单一View的layout过程中的layout()和onLayout(),此处不作过多描述
}
}
}
此处需注意:ViewGroup 和 View 同样拥有layout()和onLayout(),但二者不同的:
- 一开始计算ViewGroup位置时,调用的是ViewGroup的layout()和onLayout();
- 当开始遍历子View & 计算子View位置时,调用的是子View的layout()和onLayout()
ViewGroup的layout我们就学到这里。
View的测量宽/高和最终的宽/高有什么区别?
View的宽/高测量值的获取getMeasuredWidth()和getMeasuredHeight()
直接看源码:
/**
*获取测量的宽
*MEASURED_SIZE_MASK = 0x00FFFFFF;为什么要用这个值参与 与运算呢?
*0x00FFFFFF是16进制,32位。我们在介绍MeasureSpec时,高1和2位用来表示模式,而低30位是值,所以mMeasuredWidth与MEASURED_SIZE_MASK参与与运算。
*/
public final int getMeasuredWidth() {
return mMeasuredWidth & MEASURED_SIZE_MASK;
}
/**
*获取测量的高
*MEASURED_SIZE_MASK = 0x00FFFFFF
*/
public final int getMeasuredHeight() {
return mMeasuredHeight & MEASURED_SIZE_MASK;
}
mMeasuredWidth和mMeasuredHeight是在哪被赋值的呢?
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
Insets insets = getOpticalInsets();
int opticalWidth = insets.left + insets.right;
int opticalHeight = insets.top + insets.bottom;
measuredWidth += optical ? opticalWidth : -opticalWidth;
measuredHeight += optical ? opticalHeight : -opticalHeight;
}
setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;
mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}
我们自定义View的时候,在onMeasure()方法完成了自己的运算得到了测量值,在方法的最后调用setMeasuredDimensionRaw(int measuredWidth, int measuredHeight)方法来设置我们测量出来的测量值。
我们再看最终值的获取,看源码:
public final int getWidth() {
return mRight - mLeft;
}
public final int getHeight() {
return mBottom - mTop;
}
mLeft,mTop,mRight,mBottom分别是Viwe左,上,右,下相对于其父容器的距离。mRight - mLeft得大的是View的是Vew的实际宽值,mBottom - mTop得到的是View实际的高值。经过setFram方法的介绍,我们知道mLeft,mTop,mRight,mBottom是在这个setFram方法中得到赋值的。我们在自定义View的时候,在重写onLayout方法的时候,我们获取某个子View的测量宽高,然后根据要求计算出这个子View在这个ViewGroup的left,top,right,bottom,然后调用的这个子View的layout方法来完成这个子View的放置。
总结
即mMeasuredWidth,mMeasuredHeight的测量值是在onMeasure方法中测量完后调用setMeasuredDimension方法来赋值的来决定的。而实际的宽高是有layout方法是由参数的值通过运算得来的。日常开发中,mMeasuredWidth = mRight - mLeft , mMeasuredHeight = mBottom -mTop。
但确实存在测量值和实际值不相等的情况。什么时候不相等呢?人为的情况下。从上面我们知道,我们在自定义View的时候,在onLayout方法中,根据子View的测量宽高值计算出了子View的left,right,top bottom,来设置子View在ViewGroup的中位置,实际也是设置了子View的实际宽高值,于是有
mMeasuredWidth = mRight - mLeft , mMeasuredHeight = mBottom -mTop。但是如果我们在计算出了子View的left,right,top bottom后,手动改了layout方法的参数值,这时mMeasuredWidth = mRight - mLeft , mMeasuredHeight = mBottom - mTop就不成立了,这时测量值和实际宽高值就不想等了。
public void layout( int l , int t, int r , int b){
// 改变传入的顶点位置参数
super.layout(l,t,r+100,b+100);
// 如此一来,在任何情况下,getWidth() / getHeight()获得的宽/高 总比 getMeasuredWidth() / getMeasuredHeight()获取的宽/高大100px
// 即:View的最终宽/高 总比 测量宽/高 大100px
}
上面代码没有任何意义,但是证明了测量宽/高的确可以不等于最终的宽/高。另外一种情况时在某些情况下,View需要多次的measure才能确定自己的测量宽/高,在前几次测量过程中,其得出测量宽/高有可能和最终宽/高不一致,但最终来说,测量宽/高和最终得宽/高相同。
上一篇: View的Measure过程源码分析
下一篇: View的draw源码分析