具体请看我写的另外一篇文章:(1)自定义View基础 - 最易懂的自定义View原理系列
计算View视图的位置。
即计算View的四个顶点位置:Left、Top、Right和Bottom
同measure过程一样,layout过程根据View的类型分为两种情况:
如果View = 单一View,则仅计算本身View的位置;如果View = VieGroup,除了计算自身View的位置外,还需要确定子View在父容器中的位置。View树的位置是由包含的每个子视图的位置所决定,所以想计算整个View树的位置,就需要递归去计算每一个子视图的位置(与measure过程同理)
接下来,我将详细分析这两种情况下的layout过程。
单一View的layout过程如下图所示:
单一View的layout过程下面我将一个个方法进行详细分析。
即设置View本身的四个顶点位置
源码分析如下:(仅贴出关键代码) public void layout(int l, int t, int r, int b) { // 当前视图的四个顶点 int oldL = mLeft; int oldT = mTop; int oldB = mBottom; int oldR = mRight; // setFrame() / setOpticalFrame():确定View的位置 // 即初始化四个顶点的值,然后判断当前View大小和位置是否发生了变化并返回 (具体请看下面源码分析) boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b); //如果视图的大小和位置发生变化,会调用onLayout() if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) { // onLayout():确定该View所有的子View在父容器的位置 // 由于单一View是没有子View的,所以onLayout()是一个空实现(后面会详细说) onLayout(changed, l, t, r, b); // 由于确定位置与具体布局有关,所以onLayout()在ViewGroup和View均没有实现。 // 在单一View中,onLayout()是一个空实现(后面会详细说) // 在ViewGroup中,onLayout()被定义为抽象方法 // 所以onLayout()需要ViewGroup的子类去重写实现(后面会详细说) ... } /* * 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(); // setOpticalFrame()实际上是调用setFrame() return setFrame( left + parentInsets.left - childInsets.left, top + parentInsets.top - childInsets.top, right + parentInsets.left + childInsets.right, bottom + parentInsets.top + childInsets.bottom); } /* * setFrame()源码分析 **/ protected boolean setFrame(int left, int top, int right, int bottom) { ... // 通过以下赋值语句记录下了视图的位置信息,即确定View的四个顶点 // 即确定了视图的位置 mLeft = left; mTop = top; mRight = right; mBottom = bottom; mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom); }下面,我们继续分析在layout()中调用的onLayout()。
对于单一View来说,由于在layout()中已经对自身View进行了位置计算,所以单一View的layout()已经完成了。
源码分析: // 当这个view和其子view被分配一个大小和位置时,被layout()调用。 即单个View的情况 // View的onLayout()为空实现 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { // 参数说明 * @param changed 当前View的大小和位置改变了 * @param left 左部位置 * @param top 顶部位置 * @param right 右部位置 * @param bottom 底部位置 }至此,单一View的layout过程已经分析完毕。
单一View的layout过程解析如下:
单一View的layout过程如:底部导航条中的条目,一般都是上图标(ImageView)、下文字(TextView),那么这两个就可以用自定义ViewGroup组合成为一个Veiw,提供两个属性分别用来设置文字和图片,使用起来会更加方便。
Paste_Image.png 原理(步骤)步骤1:ViewGroup调用layout()计算自身的位置;步骤2:ViewGroup调用onLayout()遍历子View并调用子View layout()确定自身子View的位置。步骤2类似于单一View的layout过程
Paste_Image.png这样自上而下、一层层地传递下去,直到完成整个View树的layout()过程
ViewGroup的layout过程如下图所示: ViewGroup的layout过程这里需要注意的是:ViewGroup和View同样拥有layout()和onLayout(),二者是不一样的。
一开始计算ViewGroup位置时,调用的是ViewGroup的layout()和onLayout();当开始遍历子View计算子View位置时,调用的是子View的layout()和onLayout()。类似于单一View的layout过程
下面我将一个个方法进行详细分析。
这里是ViewGroup的layout()
源码分析如下:(仅贴出关键代码) // 与单一View的layout()源码是一致的。 public void layout(int l, int t, int r, int b) { // 当前视图的四个顶点 int oldL = mLeft; int oldT = mTop; int oldB = mBottom; int oldR = mRight; // setFrame() / setOpticalFrame():确定View的位置 // 即初始化四个顶点的值,然后判断当前View大小和位置是否发生了变化并返回 (具体请看下面源码分析) boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b); //如果视图的大小和位置发生变化,会调用onLayout() if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) { // onLayout():确定该ViewGroup所有子View在父容器的位置 // 由于单一View是没有子View的,所以onLayout()是一个空实现(后面会详细说) onLayout(changed, l, t, r, b); // 由于确定位置与具体布局有关,所以onLayout()在ViewGroup没有实现。(被定义为抽象方法) // 所以onLayout()需要ViewGroup的子类去重写实现(后面会详细说) ... } /* * 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(); // setOpticalFrame()实际上是调用setFrame() return setFrame( left + parentInsets.left - childInsets.left, top + parentInsets.top - childInsets.top, right + parentInsets.left + childInsets.right, bottom + parentInsets.top + childInsets.bottom); } /* * setFrame()源码分析 **/ protected boolean setFrame(int left, int top, int right, int bottom) { ... // 通过以下赋值语句记录下了视图的位置信息,即确定View的四个顶点 // 即确定了视图的位置 mLeft = left; mTop = top; mRight = right; mBottom = bottom; mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom); }下面,我们继续分析在layout()中调用的onLayout()。
所以在自定义ViewGroup时必须重写onLayout()!!!!!
根据上面说的原理描述,在ViewGroup调用layout()计算完自身的位置后,是需要ViewGroup调用onLayout()遍历子View并调用子View layout()确定自身子View的位置。
所以,重写ViewGroup的onLayout()的本质是:遍历子View并调用子View的layout()确定子View的位置。复写的套路如下:
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { // 参数说明 * @param changed 当前View的大小和位置改变了 * @param l 即left,父View的左部位置 * @param t 即top,父View的顶部位置 * @param r 即right,父View的右部位置 * @param b 即bottom,父View的底部位置 // 循环所有子View for (int i=0; i<getChildCount(); i++) { View child = getChildAt(i); // 计算当前子View的四个位置值 // 计算的逻辑需要自己实现,也是自定义View的关键 ... // 对计算后的位置值进行赋值 int mLeft = Left int mTop = Top int mRight = Right int mBottom = Bottom // 调用子view的layout()并传递计算过的参数 // 从而计算出子View的位置 child.layout(mLeft, mTop, mRight, mBottom); } } }在复写的onLayout()会调用子View的layout()和onLayout(),这两个过程类似于单一View的layout过程中的layout()和onLayout(),这里不作过多描述
详细请看上面的单一View的layout过程
对于ViewGroup的layout过程,如下:
ViewGroup的layout过程至此,ViewGroup的layout过程已经讲解完毕。
为了让大家更好地理解ViewGroup的layout过程(特别是复写onLayout()),接下来,我将用两个实例来加深对ViewGroup layout过程的理解。
实例1:系统提供的LinearLayout(ViewGroup的子类)实例2:自定义View(继承了ViewGroup类)在上述流程中,对于LinearLayout的layout()的实现与上面所说是一样的,这里不作过多阐述,直接进入LinearLayout复写的onLayout()代码分析:
// 复写的逻辑和LinearLayout measure过程的`onMeasure()`类似 @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { // 先查看自身方向属性 // 不同的方向处理方式不同 if (mOrientation == VERTICAL) { layoutVertical(l, t, r, b); } else { layoutHorizontal(l, t, r, b); } } 由于垂直 / 水平方向类似,所以此处仅分析垂直方向(Vertical)的处理过程源码分析如下:(注释非常清楚) void layoutVertical(int left, int top, int right, int bottom) { // 子View的数量 final int count = getVirtualChildCount(); // 遍历子View for (int i = 0; i < count; i++) { final View child = getVirtualChildAt(i); if (child == null) { childTop += measureNullChild(i); } else if (child.getVisibility() != GONE) { // 子View的测量宽 / 高值 final int childWidth = child.getMeasuredWidth(); final int childHeight = child.getMeasuredHeight(); // 递归调用子View的setChildFrame():对子View的位置信息进行测量计算 // 实际上是调用了子View的layout(),请看下面源码分析 setChildFrame(child, childLeft, childTop + getLocationOffset(child), childWidth, childHeight); // childTop逐渐增大,即后面的子元素会被放置在靠下的位置 // 这符合垂直方向的LinearLayout的特性 childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child); i += getChildrenSkipCount(child, i); } } } /* *setChildFrame()代码分析 **/ private void setChildFrame( View child, int left, int top, int width, int height){ // setChildFrame()仅仅只是调用了子View的layout()而已 child.layout(left, top, left ++ width, top + height); } // 在layout()又通过调用setFrame()确定View的四个顶点 // 即确定了子View的位置 // 如此不断循环确定所有子View的位置,最终确定ViewGroup的位置在setFrame()实际上是调用了子View的layout()从而实现子View的位置计算,和上面类似,这里就不作过多描述。
实例的视图是一个ViewGroup(灰色视图),包含一个黄色的子View,如下图:
自定义View的视图r = Left + width + Left(因为左右间距一样)b = Top + height + Top(因为上下间距一样)
Right = width + Left;Bottom = height + Top;因为其余方法同上,这里不作过多描述,所以这里只分析复写的onLayout()
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { // 参数说明 * @param changed 当前View的大小和位置改变了 * @param l 即left,父View的左部位置 * @param t 即top,父View的顶部位置 * @param r 即right,父View的右部位置 * @param b 即bottom,父View的底部位置 // 循环所有子View // 其实就只有一个 for (int i=0; i<getChildCount(); i++) { View child = getChildAt(i); // 取出当前子View宽 / 高 int width = child.getMeasuredWidth(); int height = child.getMeasuredHeight(); // 计算当前子View的mLeft和mTop值 int mLeft = (r - width) / 2; int mTop = (b - height) / 2; // 调用子view的layout()并传递计算过的参数 // 从而计算出子View的位置 child.layout(mLeft, mTop, mLeft + width, mTop + height); } } }布局文件如下:
<?xml version="1.0" encoding="utf-8"?> <scut.carson_ho.layout_demo.Demo_ViewGroup xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="#eee998" tools:context="scut.carson_ho.layout_demo.MainActivity"> <Button android:text="ChildView" android:layout_width="200dip" android:layout_height="200dip" android:background="#333444" android:id="@+id/ChildView" /> </scut.carson_ho.layout_demo.Demo_ViewGroup >好了,你是不是发现,粘了我的代码但是画不出来?!(如下图)
实际示意图因为我还没说draw流程啊哈哈哈!
draw流程是负责将View绘制出来的。
layout()过程讲到这里讲完了,接下来我将继续将自定义View的最后一个流程draw流程,有兴趣就继续关注我啦啦!!
首先明确定义:
getWidth() ( getHeight()):View最终的宽 / 高getMeasuredWidth() (getMeasuredHeight()):View的测量的宽 / 高:先分别看下各自的源码:
// View的测量的宽 / 高: public final int getMeasuredWidth() { return mMeasuredWidth & MEASURED_SIZE_MASK; // measure过程中返回的mMeasuredWidth } public final int getMeasuredHeight() { return mMeasuredHeight & MEASURED_SIZE_MASK; // measure过程中返回的mMeasuredHeight } // View最终的宽 / 高 public final int getWidth() { return mRight - mLeft; // View最终的宽 = 子View的右边界 - 子view的左边界。 } public final int getHeight() { return mBottom - mTop; // View最终的高 = 子View的下边界 - 子view的上边界。 }二者的区别具体如下:
二者的区别上面标红:一般情况下,二者获取的宽 / 高是相等的。那么,“非一般”情况是什么?
答:人为设置。
通过重写View的layout()强行设置, @Override 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的最终宽 / 高和测量宽 / 高大100px是可以不一样。
网上流传这么一个原因描述:
实际上在当屏幕可以包裹内容的时候,他们的值是相等的;只有当view超出屏幕后,才能看出他们的区别:getMeasuredWidth()是实际View的大小,与屏幕无关,而getHeight的大小此时则是屏幕的大小。当超出屏幕后getMeasuredWidth()等于getWidth()加上屏幕之外没有显示的大小这个结论是错的!详细请看这个博客
getWidth() ( getHeight())获得的宽 / 高与getMeasuredWidth() (getMeasuredHeight())获取的宽 (高)在非人为设置的情况下,永远是相等的。
此步骤就是复写onLayout()的逻辑
如此不断循环确定所有子View的位置,直到全部确定即layout过程完毕对于View的layout过程调用layout()计算自身的位置即可。
一个图总结自定义View - Layout过程,如下图:
总结转载于:https://www.cnblogs.com/xinmengwuheng/p/7070088.html