View之Layout過程

1.作用

計算視圖(View)的位置

即計算View的四個頂點位置:LeftTopRightBottom

2.layout過程詳解

類似measure過程,layout過程根據View的類型分爲2種情況:
在這裏插入圖片描述

3.1單一View的layout過程

  • 應用場景:在無現成的控件View滿足需求、需要自己實現時,則使用自定義單一View
  • 具體使用:繼承自ViewSurfaceView、或其他View;不包含子View
  • 具體流程
    在這裏插入圖片描述
  • 源碼分析
    layout過程的入口=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大小和位置是否發生了變化 & 返回 
    // ->>分析1、分析2
    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個抽象方法,需重寫實現(後面會詳細說)
  ...

}  

/**
  * 分析1:setFrame()
  * 作用:根據傳入的4個位置值,設置View本身的四個頂點位置
  * 即:最終確定View本身的位置
  */ 
  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);

    }

/**
  * 分析2:setOpticalFrame()
  * 作用:根據傳入的4個位置值,設置View本身的四個頂點位置
  * 即:最終確定View本身的位置
  */ 
  private boolean setOpticalFrame(int left, int top, int right, int bottom) {

        Insets parentInsets = mParent instanceof View ?
                ((View) mParent).getOpticalInsets() : Insets.NONE;

        Insets childInsets = getOpticalInsets();

        // 內部實際上是調用setFrame()
        return setFrame(
                left   + parentInsets.left - childInsets.left,
                top    + parentInsets.top  - childInsets.top,
                right  + parentInsets.left + childInsets.right,
                bottom + parentInsets.top  + childInsets.bottom);
    }
    // 回到調用原處

/**
  * 分析3:onLayout()
  * 注:對於單一View的laytou過程
  *    a. 由於單一View是沒有子View的,故onLayout()是一個空實現
  *    b. 由於在layout()中已經對自身View進行了位置計算,所以單一View的layout過程在layout()後就已完成了
  */ 
 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {

   // 參數說明
   // changed 當前View的大小和位置改變了 
   // left 左部位置
   // top 頂部位置
   // right 右部位置
   // bottom 底部位置
}  

至此,單一Viewlayout過程已分析完畢。

  • 總結
    單一Viewlayout過程解析如下:
    在這裏插入圖片描述

    3.2 ViewGroup的layout過程

  • 應用場景
    利用現有的組件根據特定的佈局方式來組成新的組件

  • 具體使用
    繼承自ViewGroup或各種Layout;含有子View

  • 原理(步驟)

  1. 計算自身ViewGroup的位置:layout()
  2. 遍歷子View&確定自身子ViewViewGroup的位置(調用子Viewlayout()):onLayout()

a. 步驟 2 類似於 單一Viewlayout過程
b. 自上而下、一層層地傳遞下去,直到完成整個View樹的layout()過程
在這裏插入圖片描述

  • 流程
    在這裏插入圖片描述
    此處需注意:
    ViewGroupView同樣擁有layout()onLayout(),但二者不同的:
  • 一開始計算ViewGroup位置時,調用的是ViewGrouplayout()onLayout();
  • 當開始遍歷子View&計算子View位置時,調用的是子Viewlayout()onLayout()

類似於單一Viewlayout過程

  • 下面進行詳細分析,layout過程入口爲layout()
/**
  * 源碼分析:layout()
  * 作用:確定View本身的位置,即設置View本身的四個頂點位置
  * 注:與單一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;  
      
    // 1. 確定View的位置:setFrame() / setOpticalFrame()
    // 即初始化四個頂點的值、判斷當前View大小和位置是否發生了變化 & 返回 
    // ->>分析1、分析2
    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()是一個空實現(上面已分析完畢)
        // 對於ViewGroup的laytou過程:由於確定位置與具體佈局有關,所以onLayout()在ViewGroup爲1個抽象方法,需重寫實現 ->>分析3
  ...

}  

/**
  * 分析1:setFrame()
  * 作用:確定View本身的位置,即設置View本身的四個頂點位置
  */ 
  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);

    }

/**
  * 分析2:setOpticalFrame()
  * 作用:確定View本身的位置,即設置View本身的四個頂點位置
  */ 
  private boolean setOpticalFrame(int left, int top, int right, int bottom) {

        Insets parentInsets = mParent instanceof View ?
                ((View) mParent).getOpticalInsets() : Insets.NONE;

        Insets childInsets = getOpticalInsets();

        // 內部實際上是調用setFrame()
        return setFrame(
                left   + parentInsets.left - childInsets.left,
                top    + parentInsets.top  - childInsets.top,
                right  + parentInsets.left + childInsets.right,
                bottom + parentInsets.top  + childInsets.bottom);
    }
    // 回到調用原處

/**
  * 分析3:onLayout()
  * 作用:計算該ViewGroup包含所有的子View在父容器的位置()
  * 注: 
  *      a. 定義爲抽象方法,需重寫,因:子View的確定位置與具體佈局有關,所以onLayout()在ViewGroup沒有實現
  *      b. 在自定義ViewGroup時必須複寫onLayout()!!!!!
  *      c. 複寫原理:遍歷子View 、計算當前子View的四個位置值 & 確定自身子View的位置(調用子View layout())
  */ 
  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的關鍵

                // 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(),此處不作過多描述
          }
      }
  }

總結
對於ViewGrouplayout過程,如下:
在這裏插入圖片描述

4. 實例講解

  • 爲了更好理解ViewGrouplayout過程(特別是複寫onLayout()
  • 下面,我將用2個實例來加深對ViewGroup layout過程的理解
  1. 系統提供的ViewGroup的子類:LinearLayout
  2. 自定義View(繼承了ViewGroup

4.1 實例解析1(LinearLayout)

4.1.1原理

  1. 計算出LinearLayout本身在父佈局的位置
  2. 計算出LinearLayout中所有子View在容器中的位置

4.1.2 具體流程
在這裏插入圖片描述
** 4.1.3 源碼分析**

  • 在上述流程中,對於LinearLayoutlayout()的實現與上面所說是一樣的,此處不做過多闡述
  • 故直接進入LinearLayout複寫的onLayout()分析

/**
  * 源碼分析: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)的處理過程 ->>分析1

/**
  * 分析1:layoutVertical(l, t, r, b)
  */
    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);
            }
        }
    }

/**
  * 分析2: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);

        }
    // 在子View的layout()又通過調用setFrame()確定View的四個頂點
    // 即確定了子View的位置
    // 如此不斷循環確定所有子View的位置,最終確定ViewGroup的位置

4.2 實例解析2:自定義View

  • 上面講的例子是系統提供的、已經封裝好的ViewGroup子類:LinearLayout
  • 但是,一般來說我們使用的都是自定義View
  • 接下來,我用一個簡單的例子講解下自定義Viewlayout()過程

實例視圖說明
實例視圖=1個ViewGroup(灰色視圖),包含1個黃色的子View,如下圖:
在這裏插入圖片描述
4.2.2 原理

  1. 計算出ViewGroup在父佈局的位置
  2. 計算出ViewGroup中子View在容器中的位置
    在這裏插入圖片描述
    4.2.3 具體計算邏輯
  • 具體計算邏輯是指計算子View的位置,即計算四頂點位置=計算LeftTopRightBottom
  • 主要是寫在複寫的onLayout()
  • 計算公式如下:
    在這裏插入圖片描述
r = Left + width + Left;// 因左右間距一樣
b = Top + height + Top;// 因上下間距一樣

Left = (r - width) / 2;
Top = (b - height) / 2;

Right = width + Left;
Bottom = height + Top;

4.2.3代碼分析
因爲其餘方法同上,這裏不做過多描述,這裏只分析複寫onLayout()

/**
  * 源碼分析:LinearLayout複寫的onLayout()
  * 注:複寫的邏輯 和 LinearLayout measure過程的 onMeasure()類似
  */ 
  @Override  
protected void onLayout(boolean changed, int l, int t, int r, int b) {  

     // 參數說明
     // changed 當前View的大小和位置改變了 
     // left 左部位置
     // top 頂部位置
     // right 右部位置
     // bottom 底部位置

        // 1. 遍歷子View:循環所有子View
        // 注:本例中其實只有一個
        for (int i=0; i<getChildCount(); i++) {
            View child = getChildAt(i);

            // 取出當前子View寬 / 高
            int width = child.getMeasuredWidth();
            int height = child.getMeasuredHeight();

            // 2. 計算當前子View的四個位置值
                // 2.1 位置的計算邏輯
                int mLeft = (r - width) / 2;
                int mTop = (b - height) / 2;
                int mRight =  mLeft + width;
                int mBottom = mTop + height;

            // 3. 根據上述4個位置的計算值,設置子View的4個頂點
            // 即確定了子View在父容器的位置
            child.layout(mLeft, mTop, mRight,mBottom);
        }
    }
}

佈局文件如下:

<?xml version="1.0" encoding="utf-8"?>
<scut.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.Demo_ViewGroup >

在這裏插入圖片描述

5.細節:getWidth()(getHeight())與getMeasuredWidth()(getMeasuredHeight())獲取的寬(高)有什麼區別?

首先明確定義:

  • 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的上邊界。
  }  

二者的區別:
在這裏插入圖片描述
上面標紅:一般情況下,二者獲取的寬高是相等的。那麼,"非一般"情況是什麼?
答:認爲設置:通過重寫Viewlayout()強行設置


@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的最終寬高與測量寬高是可以不一樣的。

6. 總結

  • 本文主要講解了自定義View中的Layout過程,總結如下:
    在這裏插入圖片描述
    在這裏插入圖片描述
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章