Android中的Veiw从内存中到呈现在UI界面上需要依次经历三个阶段:量算 -> 布局 -> 绘图,关于View的量算、布局、绘图的总体机制可参见博文 《 Android中View的布局及绘图机制》。量算是布局的基础,如果想了解量算的细节,可参见博文《源码解析Android中View的measure量算过程》。本文将从源码角度解析View的布局layout过程,本文会详细介绍View布局过程中的关键方法,并对源码加上了注释以进行说明。
对View进行布局的目的是计算出View的尺寸以及在其父控件中的位置,具体来说就是计算出View的四条边界分别到其父控件左边界、上边界的距离,即计算View的left、top、right、bottom的值。
layout
layout()方法是View布局的入口,其源码如下所示:
public void layout(int l, int t, int r, int b) { //成员变量mPrivateFlags3中的一些比特位存储着和layout相关的信息 if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) { //如果在mPrivateFlags3的低位字节的第4位(从最右向左数第4位)的值为1, //那么就表示在layout布局前需要先对View进行量算, //这种情况下就会执行View的onMeasure方法对View进行量算 onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec); //量算完成后就会将mPrivateFlags3低位字节的第4位重置为0, //移除掉标签PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT; } int oldL = mLeft; int oldT = mTop; int oldB = mBottom; int oldR = mRight; //如果isLayoutModeOptical()返回true,那么就会执行setOpticalFrame()方法, //否则会执行setFrame()方法。并且setOpticalFrame()内部会调用setFrame(), //所以无论如何都会执行setFrame()方法。 //setFrame()方法会将View新的left、top、right、bottom存储到View的成员变量中 //并且返回一个boolean值,如果返回true表示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的布局发生了变化,或者mPrivateFlags有需要LAYOUT的标签PFLAG_LAYOUT_REQUIRED, //那么就会执行以下代码 //首先会触发onLayout方法的执行,View中默认的onLayout方法是个空方法 //不过继承自ViewGroup的类都需要实现onLayout方法,从而在onLayout方法中依次循环子View, //并调用子View的layout方法 onLayout(changed, l, t, r, b); //在执行完onLayout方法之后,从mPrivateFlags中移除标签PFLAG_LAYOUT_REQUIRED mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED; //我们可以通过View的addOnLayoutChangeListener(View.OnLayoutChangeListener listener)方法 //向View中添加多个Layout发生变化的事件监听器 //这些事件监听器都存储在mListenerInfo.mOnLayoutChangeListeners这个ArrayList中 ListenerInfo li = mListenerInfo; if (li != null && li.mOnLayoutChangeListeners != null) { //首先对mOnLayoutChangeListeners中的事件监听器进行拷贝 ArrayList<OnLayoutChangeListener> listenersCopy = (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone(); int numListeners = listenersCopy.size(); for (int i = 0; i < numListeners; ++i) { //遍历注册的事件监听器,依次调用其onLayoutChange方法,这样Layout事件监听器就得到了响应 listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB); } } } //从mPrivateFlags中移除强制Layout的标签PFLAG_FORCE_LAYOUT mPrivateFlags &= ~PFLAG_FORCE_LAYOUT; //向mPrivateFlags3中加入Layout完成的标签PFLAG3_IS_LAID_OUT mPrivateFlags3 |= PFLAG3_IS_LAID_OUT; }
在layout()方法内部刚开始执行的时候,首先会根据mPrivateFlags3变量是否具有标志位PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT判断是否需要执行View的onMeasure()方法。如果具有标志位PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT,则执行onMeasure()方法,从而对View进行量算,量算的结果会保存到View的成员变量中。量算完成后就会将mPrivateFlags3低位字节的第4位重置为0,移除掉标签PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT。
如果isLayoutModeOptical()返回true,那么就会执行setOpticalFrame()方法,否则会执行setFrame()方法。并且setOpticalFrame()内部会调用setFrame(),所以无论如何都会执行setFrame()方法。setFrame()方法会将View新的left、top、right、bottom存储到View的成员变量中,并且返回一个boolean值,如果返回true表示View的位置或尺寸发生了变化,否则表示未发生变化。后面会对setFrame()方法详细介绍。
如果View的布局发生了变化,或者mPrivateFlags有需要LAYOUT的标签PFLAG_LAYOUT_REQUIRED,就会触发onLayout方法的执行,View中默认的onLayout方法是个空方法。不过继承自ViewGroup的类都需要实现onLayout方法,从而在onLayout方法中依次循环子View,并调用子View的layout方法。在执行完onLayout方法之后,从mPrivateFlags中移除标签PFLAG_LAYOUT_REQUIRED。然后会遍历注册的Layout Change事件监听器,依次调用其onLayoutChange方法,这样Layout事件监听器就得到了响应。
最后,从mPrivateFlags中移除强制Layout的标签PFLAG_FORCE_LAYOUT,向mPrivateFlags3中加入Layout完成的标签PFLAG3_IS_LAID_OUT。
setFrame
setFrame()方法是具体用来完成给View分配尺寸以及位置工作的,在layout()方法中会调用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 + ")"); } if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) { //将新旧left、right、top、bottom进行对比,只要不完全相对就说明View的布局发生了变化, //则将changed变量设置为true changed = true; //先保存一下mPrivateFlags中的PFLAG_DRAWN标签信息 int drawn = mPrivateFlags & PFLAG_DRAWN; //分别计算View的新旧尺寸 int oldWidth = mRight - mLeft; int oldHeight = mBottom - mTop; int newWidth = right - left; int newHeight = bottom - top; //比较View的新旧尺寸是否相同,如果尺寸发生了变化,那么sizeChanged的值为true boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight); // Invalidate our old position invalidate(sizeChanged); //将新的left、top、right、bottom存储到View的成员变量中 mLeft = left; mTop = top; mRight = right; mBottom = bottom; //mRenderNode.setLeftTopRightBottom()方法会调用RenderNode中原生方法的nSetLeftTopRightBottom()方法, //该方法会根据left、top、right、bottom更新用于渲染的显示列表 mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom); //向mPrivateFlags中增加标签PFLAG_HAS_BOUNDS,表示当前View具有了明确的边界范围 mPrivateFlags |= PFLAG_HAS_BOUNDS; if (sizeChanged) { //如果View的尺寸和之前相比发生了变化,那么就执行sizeChange()方法, //该方法中又会调用onSizeChanged()方法,并将View的新旧尺寸传递进去 sizeChange(newWidth, newHeight, oldWidth, oldHeight); } if ((mViewFlags & VISIBILITY_MASK) == VISIBLE || mGhostView != null) { //有可能在调用setFrame方法之前,invalidate方法就被调用了, //这会导致mPrivateFlags移除了PFLAG_DRAWN标签。 //如果当前View处于可见状态就将mPrivateFlags强制添加PFLAG_DRAWN状态位, //这样会确保下面的invalidate()方法会执行到其父控件级别。 mPrivateFlags |= PFLAG_DRAWN; invalidate(sizeChanged); //invalidateParentCaches()方法会移除其父控件的PFLAG_INVALIDATED标签, //这样其父控件就会重建用于渲染的显示列表 invalidateParentCaches(); } // 重新恢复mPrivateFlags中原有的PFLAG_DRAWN标签信息 mPrivateFlags |= drawn; mBackgroundSizeChanged = true; if (mForegroundInfo != null) { mForegroundInfo.mBoundsChanged = true; } notifySubtreeAccessibilityStateChangedIfNeeded(); } return changed; }
在该方法中,会将新旧left、right、top、bottom进行对比,只要不完全相同就说明View的布局发生了变化,则将changed变量设置为true。然后比较View的新旧尺寸是否相同,如果尺寸发生了变化,并将其保存到变量sizeChanged中。如果尺寸发生了变化,那么sizeChanged的值为true。
然后将新的left、top、right、bottom存储到View的成员变量中保存下来。并执行mRenderNode.setLeftTopRightBottom()方法会,其会调用RenderNode中原生方法的nSetLeftTopRightBottom()方法,该方法会根据left、top、right、bottom更新用于渲染的显示列表。
如果View的尺寸和之前相比发生了变化,那么就执行sizeChange()方法,该方法中又会调用onSizeChanged()方法,并将View的新旧尺寸传递进去。
如果View处于可见状态,那么会调用invalidate和invalidateParentCaches方法。invalidateParentCaches()方法会移除其父控件的PFLAG_INVALIDATED标签,这样其父控件就会重建用于渲染的显示列表。
sizeChange
sizeChange方法会在View的尺寸发生变化时调用,在setFrame()方法中就可能会调用sizeChange()方法。当然,在View的setLeft()、setTop()、setRight()、setBottom()等其他改变View尺寸的方法中也会调用sizeChange()方法,其源码如下所示:
private void sizeChange(int newWidth, int newHeight, int oldWidth, int oldHeight) { //将View的新旧尺寸传递给onSizeChanged()方法 onSizeChanged(newWidth, newHeight, oldWidth, oldHeight); if (mOverlay != null) { mOverlay.getOverlayView().setRight(newWidth); mOverlay.getOverlayView().setBottom(newHeight); } rebuildOutline(); }
在该方法中其主要将View的新旧尺寸传递给onSizeChanged()方法使其执行。
onSizeChanged
onSizeChanged()方法是个空方法,代码如下所示:
protected void onSizeChanged(int w, int h, int oldw, int oldh) { }
该方法会在View的尺寸发生变化时,通过sizeChange()方法的执行而被调用。当View第一次加入到View树中时,该方法也会被调用,只不过传入的旧尺寸oldWidth和oldHeight都是0。
总结
layout方法总的调用过程主线如下所示:
layout() -> onMeasure() -> setFrame() -> sizeChange() -> onSizeChanged() -> onLayout() ->遍历执行OnLayoutChangeListener.onLayoutChange()
希望本文对大家理解View的layout布局过程有所帮助!
相关阅读:
《我的Android博文整理汇总》
《 Android中View的布局及绘图机制》
《源码解析Android中View的measure量算过程》