组件布局流程结论
1.)layout流程始于ViewRootImpl的performLayout()方法,该方法会调用根View(DecorView)的layout()方法进行布局,因为DecorView是ViewGroup(FrameLayout),所以layout流程来到了ViewGroup(其实调用的是父类View类)的layout()方法
- )layout()方法负责组件自身的布局,会存储mLeft、mTop、mRight、mBottom 四个值来存储自身的位置,如果布局变化,还会回调onSizeChanged()方法通知我们布局发生了变化。最后layout()方法还会调用onLayout()方法进行子组件的布局。
- )View类因为没有子组件,所以View类的onLayout()方法是个空实现,ViewGroup类因为有子组件,但我们自定义的ViewGroup类都有自己的布局规则,所以ViewGroup类的onLayout()方法是个抽象方法,需要子类去实现,所以我们直接继承ViewGroup类一定需要重写onLayout()方法。
- )实现ViewGroup的onLayout()方法,我们需要循环所有的子组件,根据我们自己的布局规则,确定他们的位置,然后调用子组件的layout()方法 ,让子组件存储自身的位置,如果子组件是View,则没有后续的布局流程,如果子组件还是ViewGroup,则会重复上述步骤,直到完成所有的布局流程。
View的layout方法
这是一个被final修饰的方法,意味着无法被子类重写。但是内部事件还是调用了View中的layout方法
View中的onLayout是一个空实现的方法,通过layout方法将新的left、top、right、bottom传给onLayout。 官方注释在此视图应该时从布局调用(layout方法执行),此视图(onLayout方法执行)给每个孩子分配一个大小和位置。子类的派生类应该重写此方法并在每个子View上调用布局(layout方法)。 View的子类当然是ViewGroup了,所以ViewGroup更加像是一个View的管理器,用来实现对子View的大小和位置变化进行控制。简单来说View会通过onLayout方法进行确认View的显示位置。
ViewGroup中的onLayout方法是一个抽象方法,所以子类必须实现。并且调用View中的layout方法来确认View的位置和大小。
在layout方法中mLeft、mTop、mRight、mBottom这四个值是用来进行对View位置的摆放和大小的限制,是相对于父控件而言的。
如图白色区域是父View,黑色区域为子View。
针对getWidth()和getMeasuredWidth()进行分析 ,先看下区别
mMeasuredWidth这个值之前在 小试牛刀-onMeasure方法 中介绍过是View的实际大小宽对应的值是mMeasuredWidth,而mMeasuredWidth是通过setMeasuredDimension方法设置进来的。所以在measure方法结束后mMeasuredWidth才会有值,此时调用getMeasuredWidth可以获取对应的值。
在getWidth()中是通过mRight - mLeft的计算返回的结果。而mRight和mLeft这两个值,上面有介绍是通过layout传递过来的。
Layout(源码分析):
Layout的作用就是为整个View树计算实际的位置,而通过刚才对View树的介绍知道,想计算整个View树的位置,就需要递归的去计算每一个子视图的位置(Measure同理)。
而确定这个位置很简单,只需要mLeft,mTop,mRight,mBottom四个值(注意:这4个值是子View相对于父View的值,下面会详细介绍)。
在代码中如何设置这4个值呢? 首先,无论是系统提供的LinearLayout还是我们自定义的View视图,他都需要继承自ViewGroup类,之后必须要做的就是重写onLayout方法(因为在onLayout在ViewGroup中被定义为抽象方法)。
ViewGroup-onlayout:
@Override
protected abstract void onLayout(boolean changed, int l, int t, int r, int b); 12
onLayout被定义为抽象方法,所以在继承ViewGroup时必须要重写该方法(onMeasure不需要)。另外这个方法也被override标注,所以也是重写的方法,他重写的是其父类view中的onLayout方法。
View-onlayout:
/**
* 当这个view和其子view被分配一个大小和位置时,被layout调用。
* @param changed 当前View的大小和位置改变了
* @param left 左部位置(相对于父视图)
* @param top 顶部位置(相对于父视图)
* @param right 右部位置(相对于父视图)
* @param bottom 底部位置(相对于父视图)
*/
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {} 123456789
注解说:当这个view和其子view被分配一个大小和位置时,被layout调用。所以我们去看看layout中做了什么。(注解没有完全按照英文翻译,并且有省略)
View-layout:
/**
* 给View和其所有子View分配大小和位置
*
* 这是布局的第二个阶段(第一个阶段是测量)。在这个阶段中,每个父视图需要去调用layout去为他所有的子视图确定位置
* 派生的子类不应该重写layout方法,应该重写onLayout方法,在onlayout方法中应该去调用每一个view的layout
*/
public void layout(int l, int t, int r, int b) {
// 将当前视图的左上右下记录为old值(参数中传入的为新的l,t,r,b值)
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
// setFrame方法的作用就是将新传入的ltrb属性赋值给View,然后判断当前View大小和位置是否发生了变化并返回
boolean changed = setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
// 调用onLayout回调方法,具体实现由重写了onLayout方法的ViewGroup的子类去实现(后面详细说明)
onLayout(changed, l, t, r, b);
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
// 调用所有重写了onLayoutChange监听的方法,通知View大小和位置发生了改变
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;
} 12345678910111213141516171819202122232425262728293031323334
在这段代码中我们只要知道:如果视图的大小和位置发生变化后,会调用我们前面分析过的onLayout方法。 对于onLayout方法的最终实现全部依靠我们在自定义ViewGroup类中重写的onLayout去实现。
计算View位置:
在重写的onLayout方法中,唯一的目的就是: 对当前视图和其所有子View设置它们在父视图中具体位置(确定这个位置就依靠mLeft,mTop,mRight,mBottom这四个值) 之前介绍过,mLeft,mTop,mRight,mBottom这四个值表示的是子view相对于父view的位置。下面我贴出我画的图看一下就明白了。
如图,黄色区域是我们的父view,而中间的深色的区域就是我们的子view。 所以对于这个View来说,我列出它相对于父view的各个值是如何计算和相关函数:
mLeft,mTop,mRight,mBottom:
view.getLeft()——mLeft:子View左边界到父view左边界的距离
public final int getLeft() {
return mLeft;
} 123
view.getTop()——mTop:子View上边界到父view上边界的距离 view.getRight()——mRight:子View右边界到父view左边界的距离 view.getBottom()——mBottom:子View下边距到父View上边界的距离
视图宽高:
视图宽度 view.getWidth();子View的右边界 - 子view的左边界。
public final int getWidth() {
return mRight - mLeft;
} 123
视图高度 view.getHeight() ;子View的下边界 - 子view的上边界。
public final int getHeight() {
return mBottom - mTop;
} 123
测量宽高:
view.getMeasuredWidth();measure过程中返回的mMeasuredWidth
public final int getMeasuredWidth() {
return mMeasuredWidth & MEASURED_SIZE_MASK;
} 123
view.getMeasuredHeight();measure过程中返回的mMeasuredHeight
public final int getMeasuredHeight() {
return mMeasuredHeight & MEASURED_SIZE_MASK;
} 123
最后介绍一下getWidth/Height和getMeasuredWidth/Height的区别: getWidth,和getLeft等这些函数都是View相对于其父View的位置。而getMeasuredWidth,getMeasuredHeight是测量后该View的实际值(有点绕,下面摘录一段jafsldkfj所写的Blog中的解释). 实际上在当屏幕可以包裹内容的时候,他们的值是相等的,只有当view超出屏幕后,才能看出他们的区别: getMeasuredHeight()是实际View的大小,与屏幕无关,而getHeight的大小此时则是屏幕的大小。 当超出屏幕后,getMeasuredHeight()等于getHeight()加上屏幕之外没有显示的大小
在计算子View在父View中的位置时,主要就是应用上面这几个函数。下面就来看看如何去重写onLayout。
onLayout:
对于重写onLayout的思路和重写onMeasure相同: 如果只需要测量单个View,则单独测量它自己就行。如果需要测量的View其下还有子View,则需要测量其所有的子View。
就以上面的View为例子,他最外面是一个黄色的父View,中间一个居中的深色子View。 我的思路如下: 如果想画出一个View,就要计算它的l,t,r,b值。并传递到onlayout( l, t, r, b )中; mRight = view.getWidth + mLeft; mBottom = view.getHeight + mTop; 所以最后可以用如下形式传入:onlayout( l, t, l+width, t+height );
剩下的任务就只需要知道它的mLeft值,mTop值,加上长、宽值就行了。 长宽值很简单,使用getWidth/Height和getMeasuredWidth/Height都可以。 由于这个View需要居中显示,剩下的问题就是如何计算该View的mLeft值和mTop值。我的思路如下: r(父View的mRight) = mLeft + width + mLeft(因为左右间距一样) b(父View的mBottom) = mTop + height + mTop(因为上下间距一样)
我的代码如下:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// 循环所有子View
for (int i=0; i<getChildCount(); i++) {
View child = getChildAt(i);
// 取出当前子View长宽
int width = child.getMeasuredWidth();
int height = child.getMeasuredHeight();
// 计算当前的mLeft和mTop值(r,b为传递进来的父View的mRight和mBottom值)
int mLeft = (r - width) / 2;
int mTop = (b - height) / 2;
// 调用layout并传递计算过的参数为子view布局
child.layout(mLeft, mTop, mLeft + width, mTop + height);
}
} 123456789101112131415161718
布局文件如下:
<com.gxy.text.CostomViewGroup xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#eee999" >
<Button
android:text="ChildView"
android:layout_width="200dip"
android:layout_height="200dip"
android:background="#333444"
android:id="@+id/textView2" />
</com.gxy.text.CostomViewGroup> 123456789101112
效果图:
总结:
onMeasure和onLayout的大致总结完了,在自定义View的时候最关键的是onLayout,因为无论你如何Measure这个View的大小,最后的决定权永远在onLayout手中,onLayout会决定具体View的大小和位置。当然onMeasure也很重要,有的情况控件的宽高不确定或者需要自定义,这时候需要我们人工Measure它。而在复杂的自定义View时,很多计算也需要在onMeasure中完成,并且些值会记录下来在onLayout中重新使用。