一、layout 过程


 类似 measure 过程,layout 过程根据 View 的类型也分为 2 种情况:

Android 自定义view枚举 android 自定义view onlayout_Android 自定义view枚举

1.1 View 的 layout 过程

layout() 的作用是 ViewGroup 用来确定子元素的位置,当 ViewGroup 的位置被确定后,它在 onLayout 中会遍历所有的子元素并调用其 layout() 方法,在 layout() 方法中 onLayout() 方法又会被调用。layout 过程和 measure 过程相比就简单多了,layout() 方法确定 View 本身的位置,而 onLayout() 方法则会确定所有子元素的位置。先来看看 View 的 layout 方法:

/**
  * 源码分析:layout()
  * 作用:确定View本身的位置,即设置View本身的四个顶点位置
  */
public void layout(int l, int t, int r, int b) {
    // 当前视图的四个顶点
    int oldL = mLeft;
    int oldT = mTop;
    int oldB = mBottom;
    int oldR = mRight;

    // 1. 确定View的位置:setFrame() / setOpticalFrame()
    // 即初始化四个顶点的值、判断当前View大小和位置是否发生了变化 & 返回
    boolean changed = isLayoutModeOptical(mParent) ?
            setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

    // 2. 若视图的大小 & 位置发生变化
    // 会重新确定该View所有的子View在父容器的位置:onLayout()
    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
        onLayout(changed, l, t, r, b);
        // 对于单一View的laytou过程:由于单一View是没有子View的,故onLayout()是一个空实现->>分析3
        // 对于ViewGroup的laytou过程:由于确定位置与具体布局有关,所以onLayout()在ViewGroup为1个抽象方法,需重写实现
    ...
}

layout 方法的大致流程如下:首先会通过 setFrame 方法来设定 View 的四个顶点的位置,即初始化 mLeft、mRight、mTop、mBottom 这四个值,View 的四个顶点一旦确定,那么 View 在父容器中的位置也就确定了。接着会调用 onLayout 方法,这个方法的用途是确定子元素的位置,由于单一 View 是没有子元素的,所以 View 的 onLayout() 是一个空实现。

Android 自定义view枚举 android 自定义view onlayout_赋值_02

1.2 ViewGroup 的 layout 过程

而 ViewGroup 的 layout 过程确定位置与具体的布局有关,所以在 ViewGroup 中是一个抽象方法,需要重写实现。

Android 自定义view枚举 android 自定义view onlayout_Android 自定义view枚举_03

根据自身需求的布局逻辑复写 onLayout(),步骤分为 3 步:

  1. 遍历所有子 View
  2. 根据自身需求计算当前子 View 的四个位置值(需自身实现)
  3. 根据上述 4 个位置的计算值,设置子 View 的 4 个顶点:调用子 View 的 layout 方法,即确定了子 View 在父容器里的位置
/**
   * 作用:计算该ViewGroup包含所有的子View在父容器的位置()
   */ 
  protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
     // 参数说明
     // changed 当前View的大小和位置改变了 
     // left 左部位置  top 顶部位置  right 右部位置  bottom 底部位置

     // 1. 遍历子View:循环所有子View
     for (int i=0; i<getChildCount(); i++) {
         View child = getChildAt(i);   

         // 2. 计算当前子View的四个位置值
           // 2.1 位置的计算逻辑需自己实现,也是自定义View的关键
           calculate();
           // 2.2 对计算后的位置值进行赋值
           int mLeft  = Left
           int mTop  = Top
           int mRight = Right
           int mBottom = Bottom

         // 3. 根据上述4个位置的计算值设置子View的4个顶点:调用子view的layout() & 传递计算过的参数
         // 即确定了子View在父容器的位置
         child.layout(mLeft, mTop, mRight, mBottom);
         // 该过程类似于单一View的layout过程中的layout()和onLayout()
     }
  }

1.3 ViewGroup 子类(LinearLayout)的 layout 过程分析

我们直接看它的 onLayout() 方法:

@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);
      }
  }

可以看到,会根据 LinearLayout 的方向(vertical、horizontal)进入不同的布局过程,这里我们只选垂直方向的布局过程,即layoutVertical()。

void layoutVertical(int left, int top, int right, int bottom) {
    // 子View的数量
    final int count = getVirtualChildCount();

    // 1. 遍历子View
    for (int i = 0; i < count; i++) {
        final View child = getVirtualChildAt(i);
        if (child == null) {
            childTop += measureNullChild(i);
        } else if (child.getVisibility() != GONE) {
            // 2. 计算子View的测量宽 / 高值
            final int childWidth = child.getMeasuredWidth();
            final int childHeight = child.getMeasuredHeight();

            // 3. 确定自身子View的位置
            // 即:递归调用子View的setChildFrame(),实际上是调用了子View的layout() ->>分析2
            setChildFrame(child, childLeft, childTop + getLocationOffset(child), childWidth, childHeight);

            // childTop逐渐增大,即后面的子元素会被放置在靠下的位置
            // 这符合垂直方向的LinearLayout的特性
            childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

            i += getChildrenSkipCount(child, i);
        }
    }
}

private void setChildFrame( View child, int left, int top, int width, int height){
    // setChildFrame()仅仅只是调用了子View的layout()而已
    child.layout(left, top, left ++ width, top + height);
    // 在子View的layout()又通过调用setFrame()确定View的四个顶点
    // 即确定了子View的位置
    // 如此不断循环确定所有子View的位置,最终确定ViewGroup的位置
}

这里分析一下 layoutVertical 的代码逻辑,可以看到此方法会遍历所有子元素并调用 setChildFrame() 方法来为子元素指定对应的位置,其中 childTop 会逐渐增大,这就意味着后面的子元素会被放置在考下的位置,这刚好符合竖直方向的 LinearLayout 的特性。至于 setChildFrame,它仅仅是调用子元素的 layout 方法而已,这样父元素在 layout 方法中完成自己的定位以后,就通过 onLayout() 方法去调用子元素的 layout() 方法,子元素又通过自己的 layout() 方法来确定自己的位置,这样一层一层的传递下去就完成了整个 View 树的 layout 过程。

我们可以看到,setChildFrame 中的 width 和 height 实际上就是子元素的测量宽/高,而在 layout() 方法中会通过 setFrame() 去设置子元素的四个顶点的位置,这样一来子元素的位置就确定了。

 

二、getMeasureWidth 和 getWidth 区别

首先明确定义:

  • getWidth() / getHeight():获得View最终的宽 / 高
  • getMeasuredWidth() / getMeasuredHeight():获得 View测量的宽 / 高
// 获得View测量的宽 / 高
  public final int getMeasuredWidth() {  
      return mMeasuredWidth & MEASURED_SIZE_MASK;  
      // measure过程中返回的mMeasuredWidth
  }  

  // 获得View最终的宽 / 高
  public final int getWidth() {  
      return mRight - mLeft;  
      // View最终的宽 = 子View的右边界 - 子view的左边界。
  }

从 getWidth 和 getHeight 的源码再结合 mLeft、mRight、mTop、mBottom 这四个变量的赋值过程来看,getWidth、getHeight 方法的返回值刚好就是 View 的测量宽/高。因此,在 View 的默认实现中,View 的测量宽/高和最终宽/高是相等的,只不过测量宽/高形成于 View 的 measure 过程,而最终宽/高形成于 View 的 layout 过程,即两者的赋值时机不同,测量宽/高的赋值时机稍微早一些。

因此在日常开发中,我们可以认为 View 的测量宽/高就等于最终宽/高,但是的确存在某些特殊情况会导致两者不一致,比如重写 View 的 layout 方法:

@Override
public void layout( int l , int t, int r , int b){
   // 改变传入的顶点位置参数
   super.layout(l,t,r+100,b+100);
}

如此一来,在任何情况下 View 的最终宽高(getWidth()、getHight())总是比测量宽高(getMeasuredWidth()、getMeasuredHeight())大 100px,虽然这样做会导致 View 的显示不正常并且没有实际意义,但证明了测量宽高的确是可以不等于最终宽高的。

另一种情况是在某些情况下,View 需要多次 measure 才能确定自己的测量宽高,在前几次的测量过程中,其得出的测量宽高有可能和最终宽高不一致,但最终来说,测量宽高还是和最终宽高相同。

Android 自定义view枚举 android 自定义view onlayout_抽象方法_04