View绘制详解,layout过程

1.View中的layout

2.在ViewGroup中对View进行排列

3.以LinearLayout为例来看看layout过程

4.根布局的layout


1.View中的layout

要说layout过程,首先我们得先来看看View中的layout方法,如下:

[java]  view plain  copy
 print ? 在CODE上查看代码片 派生到我的代码片
  1. public void layout(int l, int t, int r, int b) {  
  2.         if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {  
  3.             onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);  
  4.             mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;  
  5.         }  
  6.   
  7.         int oldL = mLeft;  
  8.         int oldT = mTop;  
  9.         int oldB = mBottom;  
  10.         int oldR = mRight;  
  11.   
  12.         boolean changed = isLayoutModeOptical(mParent) ?  
  13.                 setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);  
  14.   
  15.         if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {  
  16.             onLayout(changed, l, t, r, b);  
  17.             mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;  
  18.   
  19.             ListenerInfo li = mListenerInfo;  
  20.             if (li != null && li.mOnLayoutChangeListeners != null) {  
  21.                 ArrayList<OnLayoutChangeListener> listenersCopy =  
  22.                         (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();  
  23.                 int numListeners = listenersCopy.size();  
  24.                 for (int i = 0; i < numListeners; ++i) {  
  25.                     listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);  
  26.                 }  
  27.             }  
  28.         }  
  29.   
  30.         mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;  
  31.         mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;  
  32.     }  

View中有layout方法,就是对该View的摆放,该方法接收四个参数,这四个参数分别表示该View上下左右四个方向的位置。通过这四个参数来确定该View在它的父容器中的位置。首先在该方法的第12行,调用了setFrame方法将参数保存到mLeft、mTop、mRight、mBottom中(小伙伴们注意一般来说如果我们没有在xml文件中设置layoutMode属性,isLayoutModeOptical方法的返回值为false),在保存的同时调用了invalidate方法进行View的绘制。setFrame过程完成之后,接下来就是回调onLayout方法。View类中的onLayout方法只是一个空方法,里边并没有任何实现。关于onLayout方法的重点其实是在ViewGroup中。这个我们后面再述。一般来说,我们在自定义View的时候是没有必要重写layout或者onLayout方法的,只有自定义ViewGroup时才需要重写onLayout方法,这个时候再来看layout方法才会发现它的意义。说到这里小伙伴们可能会有另外一个疑问,那就是一个View的layout方法在什么时候调用,在哪里调用?其实,layout方法是在该View的父容器中调用 的,具体请看下一小节。

2.在ViewGroup中对View进行排列

我们都知道,ViewGroup继承自View,而ViewGroup也重写了View中的layout和onLayout方法,而且这里还有一点点变化,我们来看看ViewGroup中的这两个方法:

[java]  view plain  copy
 print ? 在CODE上查看代码片 派生到我的代码片
  1. @Override  
  2. public final void layout(int l, int t, int r, int b) {  
  3.     if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {  
  4.         if (mTransition != null) {  
  5.             mTransition.layoutChange(this);  
  6.         }  
  7.         super.layout(l, t, r, b);  
  8.     } else {  
  9.         // record the fact that we noop'd it; request layout when transition finishes  
  10.         mLayoutCalledWhileSuppressed = true;  
  11.     }  
  12. }  
  13.   
  14. @Override  
  15. protected abstract void onLayout(boolean changed,  
  16.         int l, int t, int r, int b);  

首先ViewGroup中实现了layout方法,但是小伙伴们注意,这个时候layout方法已经变成了final类型的,表示该方法不可以再被ViewGroup的子类重写,那怎么办呢?首先第7行调用了父类的layout方法,也就是第一小节我们会看到的layout方法。其次,ViewGroup中也实现了onLayout方法,但是onLayout的修饰符变为了abstract,这个表示所有继承自ViewGroup的类都需要重写该方法。实际上,所有继承自ViewGroup的容器都重写了这个方法,如果我们自定义ViewGroup时也需要重写这个方法,这里我举一个简单的例子:

[java]  view plain  copy
 print ? 在CODE上查看代码片 派生到我的代码片
  1. @Override  
  2. protected void onLayout(boolean changed, int l, int t, int r, int b) {  
  3.     //获取容器中的控件(假设只有一个)  
  4.     View view = getChildAt(0);  
  5.     //设置该View上下左右四个点的坐标,对View进行摆放  
  6.     view.layout(00100100);  
  7. }  

大致的思路就是这样,我们需要在onLayout中遍历所有的View,并计算每一个View的上下左右四个点的坐标,然后调用该View的layout方法进行摆放即可,摆放完成之后再调用onDraw方法进行绘制,绘制成功之后这个View就可以显示出来了。很简单吧。这里我们以LinearLayout为例来看看LinearLayout这个容器是如何遍历子控件并摆放的。

3.以LinearLayout为例来看看layout过程

这里我们以LinearLayout为例,来看看View到底是如何摆放在一个容器中的。因为我们说过,凡是继承自ViewGroup的类都是不能重写layout方法的,但是同时又必须重写onLayout方法,所以这里我们就先来看看LinearLayout的onLayout方法:

[java]  view plain  copy
 print ? 在CODE上查看代码片 派生到我的代码片
  1. @Override  
  2.     protected void onLayout(boolean changed, int l, int t, int r, int b) {  
  3.         if (mOrientation == VERTICAL) {  
  4.             layoutVertical(l, t, r, b);  
  5.         } else {  
  6.             layoutHorizontal(l, t, r, b);  
  7.         }  
  8.     }  

这个方法倒是很简单,就是根据View的排列顺序来进行View的摆放,那么这里我就以竖直排列为例,我们来看看摆放的过程:

[java]  view plain  copy
 print ? 在CODE上查看代码片 派生到我的代码片
  1. void layoutVertical(int left, int top, int right, int bottom) {  
  2.     final int paddingLeft = mPaddingLeft;  
  3.   
  4.     int childTop;  
  5.     int childLeft;  
  6.      
  7.     // Where right end of child should go  
  8.     final int width = right - left;  
  9.     int childRight = width - mPaddingRight;  
  10.      
  11.     // Space available for child  
  12.     //容器中子控件的可用宽度(父容器总宽度减去父容器的左右内边距)  
  13.     int childSpace = width - paddingLeft - mPaddingRight;  
  14.     //获取子控件总个数  
  15.     final int count = getVirtualChildCount();  
  16.     //获取父容器的Gravity  
  17.     final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;  
  18.     final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;  
  19.      //根据底部对齐、垂直居中、顶部对齐分别来计算控件顶部的起始位置  
  20.      //注意mTotalLength参数是我们在上一篇博客中提到的LinearLayout测量时子View的总高度  
  21.     switch (majorGravity) {  
  22.        case Gravity.BOTTOM:  
  23.            // mTotalLength contains the padding already  
  24.            childTop = mPaddingTop + bottom - top - mTotalLength;  
  25.            break;  
  26.   
  27.            // mTotalLength contains the padding already  
  28.        case Gravity.CENTER_VERTICAL:  
  29.            childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;  
  30.            break;  
  31.   
  32.        case Gravity.TOP:  
  33.        default:  
  34.            childTop = mPaddingTop;  
  35.            break;  
  36.     }  
  37.   
  38.     for (int i = 0; i < count; i++) {  
  39.         final View child = getVirtualChildAt(i);  
  40.         if (child == null) {  
  41.             childTop += measureNullChild(i);  
  42.         } else if (child.getVisibility() != GONE) {  
  43.                //获取子View的测量宽高  
  44.             final int childWidth = child.getMeasuredWidth();  
  45.             final int childHeight = child.getMeasuredHeight();  
  46.              
  47.             final LinearLayout.LayoutParams lp =  
  48.                     (LinearLayout.LayoutParams) child.getLayoutParams();  
  49.             //从子控件的LayoutParams总获取gravity属性,但是这个gravity是指子View的android:layout_gravity属性而不是android:gravity属性  
  50.             int gravity = lp.gravity;  
  51.             if (gravity < 0) {  
  52.                 gravity = minorGravity;  
  53.             }  
  54.             final int layoutDirection = getLayoutDirection();  
  55.             final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);  
  56.              //根据子控件的layout_gravity属性来计算子控件显示时的childLeft的值  
  57.             switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {  
  58.                 case Gravity.CENTER_HORIZONTAL:  
  59.                     childLeft = paddingLeft + ((childSpace - childWidth) / 2)  
  60.                             + lp.leftMargin - lp.rightMargin;  
  61.                     break;  
  62.   
  63.                 case Gravity.RIGHT:  
  64.                     childLeft = childRight - childWidth - lp.rightMargin;  
  65.                     break;  
  66.   
  67.                 case Gravity.LEFT:  
  68.                 default:  
  69.                     childLeft = paddingLeft + lp.leftMargin;  
  70.                     break;  
  71.             }  
  72.   
  73.             if (hasDividerBeforeChildAt(i)) {  
  74.                 childTop += mDividerHeight;  
  75.             }  
  76.             //子控件上面的位置再加上上边距  
  77.             childTop += lp.topMargin;  
  78.                          //该方法中实际上调用了layout方法进行控件的摆放  
  79.             setChildFrame(child, childLeft, childTop + getLocationOffset(child),  
  80.                     childWidth, childHeight);  
  81.             //对于下一个控件而言,它的起始高度是已经摆放好的View的高度之和  
  82.             childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);  
  83.   
  84.             i += getChildrenSkipCount(child, i);  
  85.         }  
  86.     }  
  87. }  

这个方法的整体过程可以分为两步,第一步,根据容器的android:gravity属性来计算第一个子控件的顶部起始坐标,第二步,遍历所有子View,根据子控件的android:layout_gravity属性来计算子控件左边的坐标(这个时候小伙伴们应该明白了,为什么当我的LinearLayout的排列方向设置为垂直之后,LinearLayout的子控件的layout_gravity属性设置为垂直居中会没有效果),计算出来之后,再算出子控件顶部的坐标,然后调用setChildFrame方法对子控件进行摆放。setChildFrame方法内部也是调用了layout方法来进行控件的摆放,如下:

[java]  view plain  copy
 print ? 在CODE上查看代码片 派生到我的代码片
  1. private void setChildFrame(View child, int left, int top, int width, int height) {          
  2.         child.layout(left, top, left + width, top + height);  
  3.     }  

如此,我们的子控件就成功的在容器中摆放出来了。

说到这里,小伙伴们应该明白了,其实每一个控件包括容器都是在它的父容器中进行摆放的,那么这个时候小伙伴们可能会有另外一个疑问,那么我们的控件总有一个是没有父容器的,就是那个DecorView,那么DecorView又是在哪里进行摆放的呢?请看下文。

4.根布局的layout

OK,控件摆放还剩一个小问题,就是DecorView是在哪里摆放?其实和我们之前说的DecorView是在哪里进行测量是同一个问题,对于这个问题,我们还是得回到ViewRootImpl中去寻找答案。我们都知道View绘制过程的启动是从performTraversals方法开始的,在这个方法中系统首先进行了View的测量,然后调用了performLayout方法进行View的摆放,performLayout中又调用了layout方法来进行控件的摆放,整个流程基本就是这样。这里的方法略长,我就不贴出来了,有兴趣的小伙伴们可以自行查看。


OK,这就是layout的一个简单的摆放过程。


以上。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值