Android进阶之深入理解View的布局(Layout)流程原理

时间:2022-07-01 08:32:06

Android进阶之深入理解View的布局(Layout)流程原理

前言

前一篇我们讲解了View的Measure过程,那今天我们来讲解下Layout;

View的layout方法作用是确定View的位置,ViewGroup的layout方法不仅要确定自身的位置,还有确定子View的位置;

Android进阶之深入理解View的测量(Measure)流程机制

一、Layout流程源码详解

Android进阶之深入理解View的布局(Layout)流程原理

1、performLayout

View三大工作流程是从ViewRootImpl#performTraversals开始的,其中performMeasure、performLayout、performDraw方法分别对应了View的测量、布局、绘制;

从performLayout开始分析View布局流程;

  1. privatevoidperformLayout(WindowManager.LayoutParamslp,intdesiredWindowWidth,
  2. intdesiredWindowHeight){
  3. mLayoutRequested=false;
  4. mScrollMayChange=true;
  5. mInLayout=true;
  6. finalViewhost=mView;
  7. Trace.traceBegin(Trace.TRACE_TAG_VIEW,"layout");
  8. try{
  9. host.layout(0,0,host.getMeasuredWidth(),host.getMeasuredHeight());
  10. //省略...
  11. }finally{
  12. Trace.traceEnd(Trace.TRACE_TAG_VIEW);
  13. }
  14. mInLayout=false;
  15. }

方法中的mView其实就是DecorView,那么host也就代表了DecorView,DecorView其实是个FrameLayout,ViewGroup并没有重写layout方法,所以我们来看下View#layout方法

2、layout

  1. /**
  2. *源码分析起始点:layout()
  3. *作用:确定View本身的位置,即设置View本身的四个顶点位置
  4. */
  5. publicvoidlayout(intl,intt,intr,intb){
  6. //当前视图的四个顶点
  7. intoldL=mLeft;
  8. intoldT=mTop;
  9. intoldB=mBottom;
  10. intoldR=mRight;
  11. //1.确定View的位置:setFrame()/setOpticalFrame()
  12. //即初始化四个顶点的值、判断当前View大小和位置是否发生了变化&返回
  13. //setFrame()->分析1
  14. //setOpticalFrame()->分析2
  15. booleanchanged=isLayoutModeOptical(mParent)?setOpticalFrame(l,t,r,b):setFrame(l,t,r,b);
  16. //2.若视图的大小&位置发生变化
  17. //会重新确定该View所有的子View在父容器的位置:onLayout()
  18. if(changed||(mPrivateFlags&PFLAG_LAYOUT_REQUIRED)==PFLAG_LAYOUT_REQUIRED){
  19. onLayout(changed,l,t,r,b);
  20. //对于单一View的laytou过程:由于单一View是没有子View的,故onLayout()是一个空实现->分析3
  21. //对于ViewGroup的laytou过程:由于确定位置与具体布局有关,所以onLayout()在ViewGroup为1个抽象方法,需自定义重写实现(下面的章节会详细说明)
  22. }
  23. /**
  24. *分析1:setFrame()
  25. *作用:根据传入的4个位置值,设置View本身的四个顶点位置
  26. *即:最终确定View本身的位置
  27. */
  28. protectedbooleansetFrame(intleft,inttop,intright,intbottom){
  29. //通过以下赋值语句记录下了视图的位置信息,即确定View的四个顶点
  30. //从而确定了视图的位置
  31. mLeft=left;
  32. mTop=top;
  33. mRight=right;
  34. mBottom=bottom;
  35. mRenderNode.setLeftTopRightBottom(mLeft,mTop,mRight,mBottom);
  36. }
  37. /**
  38. *分析2:setOpticalFrame()
  39. *作用:根据传入的4个位置值,设置View本身的四个顶点位置
  40. *即:最终确定View本身的位置
  41. */
  42. privatebooleansetOpticalFrame(intleft,inttop,intright,intbottom){
  43. InsetsparentInsets=mParentinstanceofView?
  44. ((View)mParent).getOpticalInsets():Insets.NONE;
  45. InsetschildInsets=getOpticalInsets();
  46. //内部实际上是调用setFrame()
  47. returnsetFrame(
  48. left+parentInsets.left-childInsets.left,
  49. top+parentInsets.top-childInsets.top,
  50. right+parentInsets.left+childInsets.right,
  51. bottom+parentInsets.top+childInsets.bottom);
  52. }
  53. //回到调用原处
  54. /**
  55. *分析3:onLayout()
  56. *注:对于单一View的laytou过程
  57. *1.由于单一View是没有子View的,故onLayout()是一个空实现
  58. *2.由于在layout()中已经对自身View进行了位置计算:setFrame()/setOpticalFrame()
  59. *3.所以单一View的layout过程在layout()后就已完成了
  60. */
  61. protectedvoidonLayout(booleanchanged,intleft,inttop,intright,intbottom){
  62. //参数说明
  63. //changed当前View的大小和位置改变了
  64. //left左部位置
  65. //top顶部位置
  66. //right右部位置
  67. //bottom底部位置
  68. }

3、setFrame

layout方法是用来确定自身位置的,其内部调用了setOpticalFrame、setFrame和onLayout方法,setOpticalFrame内部又会调用setFrame。所以我们先来看setFrame方法,如下

  1. protectedbooleansetFrame(intleft,inttop,intright,intbottom){
  2. booleanchanged=false;
  3. if(mLeft!=left||mRight!=right||mTop!=top||mBottom!=bottom){
  4. //判断View的位置是否发生改变
  5. changed=true;
  6. //Rememberourdrawnbit
  7. intdrawn=mPrivateFlags&PFLAG_DRAWN;
  8. intoldWidth=mRight-mLeft;//获取原来的宽度
  9. intoldHeight=mBottom-mTop;//获取原来的高度
  10. intnewWidth=right-left;//获取新的宽度
  11. intnewHeight=bottom-top;//获取新的高度
  12. //判断View的尺寸是否发生改变
  13. booleansizeChanged=(newWidth!=oldWidth)||(newHeight!=oldHeight);
  14. //Invalidateouroldposition
  15. invalidate(sizeChanged);
  16. //对mLeft、mTop、mRight、mBottom初始化,View自身的位置也就确定了。
  17. mLeft=left;
  18. mTop=top;
  19. mRight=right;
  20. mBottom=bottom;
  21. mRenderNode.setLeftTopRightBottom(mLeft,mTop,mRight,mBottom);
  22. mPrivateFlags|=PFLAG_HAS_BOUNDS;
  23. //如果View尺寸发生改变,将执行View#sizeChange方法,在sizeChange方法内部会调用View#onSizeChanged方法。
  24. if(sizeChanged){
  25. sizeChange(newWidth,newHeight,oldWidth,oldHeight);
  26. }
  27. //省略...
  28. }
  29. returnchanged;
  30. }

在setFrame方法中对mLeft、mTop、mRight 、mBottom进行初始化,mLeft、mTop分别对应View左上角的横坐标和纵坐标,mRight 、mBottom分别对应了View右下角的横坐标和纵坐标,View的四个顶点的坐标确定了,View自身的位置也就确定了;

Android进阶之深入理解View的布局(Layout)流程原理

4、FrameLayout#onLayout

再回到layout方法,在通过setFrame方法确定了自身位置后,接下来会调用onLayout方法,这个方法其实用来确定子View的位置的;

不过View和ViewGroup都没有真正实现onLayout,因为onLayout和onMeasure类似,其过程都与具体的布局有关;

以FrameLayout为例来分析onLayout过程,FrameLayout#onLayout

  1. @Override
  2. protectedvoidonLayout(booleanchanged,intleft,inttop,intright,intbottom){
  3. layoutChildren(left,top,right,bottom,false/*noforceleftgravity*/);
  4. }
  5. 其内部调用了layoutChildren方法
  6. voidlayoutChildren(intleft,inttop,intright,intbottom,
  7. booleanforceLeftGravity){
  8. finalintcount=getChildCount();//获取子View的数量
  9. //parentLeft、parentTop分别代表子View所占区域左上角的横坐标和纵坐标
  10. //parentRight、parentBottom分别代表子View所占区域右下角的横坐标和纵坐标
  11. finalintparentLeft=getPaddingLeftWithForeground();
  12. finalintparentRight=right-left-getPaddingRightWithForeground();
  13. finalintparentTop=getPaddingTopWithForeground();
  14. finalintparentBottom=bottom-top-getPaddingBottomWithForeground();
  15. mForegroundBoundsChanged=true;
  16. //遍历子View
  17. for(inti=0;i<count;i++){
  18. finalViewchild=getChildAt(i);
  19. if(child.getVisibility()!=GONE){
  20. finalLayoutParamslp=(LayoutParams)child.getLayoutParams();
  21. //获取子View的测量宽、高
  22. finalintwidth=child.getMeasuredWidth();
  23. finalintheight=child.getMeasuredHeight();
  24. intchildLeft;
  25. intchildTop;
  26. //获取子View设置的Gravity,如果子View没有设置Gravity,则用默认的Gravity:DEFAULT_CHILD_GRAVITY。
  27. intgravity=lp.gravity;
  28. if(gravity==-1){
  29. gravity=DEFAULT_CHILD_GRAVITY;
  30. }
  31. finalintlayoutDirection=getLayoutDirection();
  32. finalintabsoluteGravity=Gravity.getAbsoluteGravity(gravity,layoutDirection);
  33. finalintverticalGravity=gravity&Gravity.VERTICAL_GRAVITY_MASK;
  34. //水平方向上,通过设置的Gravity,来确定childLeft,即每个子View左上角的横坐标
  35. switch(absoluteGravity&Gravity.HORIZONTAL_GRAVITY_MASK){
  36. caseGravity.CENTER_HORIZONTAL:
  37. childLeft=parentLeft+(parentRight-parentLeft-width)/2+
  38. lp.leftMargin-lp.rightMargin;
  39. break;
  40. caseGravity.RIGHT:
  41. if(!forceLeftGravity){
  42. childLeft=parentRight-width-lp.rightMargin;
  43. break;
  44. }
  45. caseGravity.LEFT:
  46. default:
  47. childLeft=parentLeft+lp.leftMargin;
  48. }
  49. //竖直方向上,通过设置的Gravity,来确定childTop,即每个子View左上角的纵坐标
  50. switch(verticalGravity){
  51. caseGravity.TOP:
  52. childTop=parentTop+lp.topMargin;
  53. break;
  54. caseGravity.CENTER_VERTICAL:
  55. childTop=parentTop+(parentBottom-parentTop-height)/2+
  56. lp.topMargin-lp.bottomMargin;
  57. break;
  58. caseGravity.BOTTOM:
  59. childTop=parentBottom-height-lp.bottomMargin;
  60. break;
  61. default:
  62. childTop=parentTop+lp.topMargin;
  63. }
  64. //调用子View的layout方法
  65. child.layout(childLeft,childTop,childLeft+width,childTop+height);
  66. }
  67. }
  68. }

在该方法内部遍历所有子View过程中,通过子View设置的Gravity,获去其childLeft、childTop即子View的左上角的横坐标和纵坐标,最后执行子View的layout方法,来确定子View的位置

5、LinearLayout#onLayout

LinearLayout复写的onLayout()分析

  1. /**
  2. *源码分析:LinearLayout复写的onLayout()
  3. *注:复写的逻辑和LinearLayoutmeasure过程的onMeasure()类似
  4. */
  5. @Override
  6. protectedvoidonLayout(booleanchanged,intl,intt,intr,intb){
  7. //根据自身方向属性,而选择不同的处理方式
  8. if(mOrientation==VERTICAL){
  9. layoutVertical(l,t,r,b);
  10. }else{
  11. layoutHorizontal(l,t,r,b);
  12. }
  13. }
  14. //由于垂直/水平方向类似,所以此处仅分析垂直方向(Vertical)的处理过程->分析1
  15. /**
  16. *分析1:layoutVertical(l,t,r,b)
  17. */
  18. voidlayoutVertical(intleft,inttop,intright,intbottom){
  19. //子View的数量
  20. finalintcount=getVirtualChildCount();
  21. //1.遍历子View
  22. for(inti=0;i<count;i++){
  23. finalViewchild=getVirtualChildAt(i);
  24. if(child==null){
  25. childTop+=measureNullChild(i);
  26. }elseif(child.getVisibility()!=GONE){
  27. //2.计算子View的测量宽/高值
  28. finalintchildWidth=child.getMeasuredWidth();
  29. finalintchildHeight=child.getMeasuredHeight();
  30. //3.确定自身子View的位置
  31. //即:递归调用子View的setChildFrame(),实际上是调用了子View的layout()->分析2
  32. setChildFrame(child,childLeft,childTop+getLocationOffset(child),
  33. childWidth,childHeight);
  34. //childTop逐渐增大,即后面的子元素会被放置在靠下的位置
  35. //这符合垂直方向的LinearLayout的特性
  36. childTop+=childHeight+lp.bottomMargin+getNextLocationOffset(child);
  37. i+=getChildrenSkipCount(child,i);
  38. }
  39. }
  40. }
  41. /**
  42. *分析2:setChildFrame()
  43. */
  44. privatevoidsetChildFrame(Viewchild,intleft,inttop,intwidth,intheight){
  45. child.layout(left,top,left++width,top+height);
  46. //setChildFrame()仅仅只是调用了子View的layout()而已
  47. //在子View的layout()又通过调用setFrame()确定View的四个顶点
  48. //即确定了子View的位置
  49. //如此不断循环确定所有子View的位置,最终确定ViewGroup的位置
  50. }

总结

View的layout流程核心在于覆写ViewGroup的onLayout方法,它的流程是拿到子View的宽高,然后实现自己的布局子View的逻辑,它一般结合onMeasure方法使用。

原文链接:https://mp.weixin.qq.com/s/tJwWblrSglqBpY54FbHewQ