Android整理筆記——1.3.2.3自定義View Layout過程

1.3.2.3自定義View Layout過程 

目錄

 


1. 作用

計算視圖(View)的位置

即計算View的四個頂點位置:LeftTopRight 和 Bottom


2. 知識儲備

具體請看文章:(1)自定義View基礎 - 最易懂的自定義View原理系列


3. layout過程詳解

類似measure過程,layout過程根據View的類型分爲2種情況:

接下來,我將詳細分析這2種情況下的layout過程

3.1 單一View的layout過程

  • 應用場景
    在無現成的控件View滿足需求、需自己實現時,則使用自定義單一View
  1. 如:製作一個支持加載網絡圖片的ImageView控件
  2. 注:自定義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

如:底部導航條中的條目,一般都是上圖標(ImageView)、下文字(TextView),那麼這兩個就可以用自定義ViewGroup組合成爲一個Veiw,提供兩個屬性分別用來設置文字和圖片,使用起來會更加方便。

 

 

  • 原理(步驟)
    1. 計算自身ViewGroup的位置:layout()
    2. 遍歷子View & 確定自身子View在ViewGroup的位置(調用子View 的 layout()):onLayout()

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

 

  • 流程

 

此處需注意:
ViewGroup 和 View 同樣擁有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(),此處不作過多描述
          }
      }
  }

總結

對於ViewGroup的layout過程,如下:

示意圖
此處需注意:
ViewGroup 和 View 同樣擁有layout()onLayout(),但二者不同的:

 

  • 一開始計算ViewGroup位置時,調用的是ViewGrouplayout()onLayout()
  • 當開始遍歷子View & 計算子View位置時,調用的是子Viewlayout()onLayout()

類似於單一Viewlayout過程

至此,ViewGroup的 layout過程已講解完畢。


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.2 源碼分析

  • 在上述流程中,對於LinearLayout的layout()的實現與上面所說是一樣的,此處不作過多闡述
  • 故直接進入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()過程

4.2.1 實例視圖說明

實例視圖 = 1個ViewGroup(灰色視圖),包含1個黃色的子View,如下圖:

 

 

4.2.2 原理

  1. 計算出ViewGroup在父佈局的位置
  2. 計算出ViewGroup中子View在容器中的位置

 

4.2.3 具體計算邏輯

  • 具體計算邏輯是指計算子View的位置,即計算四頂點位置 = 計算Left、Top、Right和Bottom;
  • 主要是寫在複寫的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 =  mLeft + width;

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

佈局文件如下:

<?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流程,有興趣就繼續關注我啦啦!!


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

二者的區別:

示意圖

上面標紅:一般情況下,二者獲取的寬 / 高是相等的。那麼,“非一般”情況是什麼?

答:人爲設置:通過重寫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的最終寬 / 高 與 測量寬 / 高是可以不一樣

特別注意

網上流傳這麼一個原因描述:

  • 實際上在當屏幕可包裹內容時,他們的值是相等的;
  • 只有當view超出屏幕後,才能看出他們的區別:getMeasuredWidth()是實際View的大小,與屏幕無關,而getHeight的大小此時則是屏幕的大小。當超出屏幕後getMeasuredWidth()等於getWidth()加上屏幕之外沒有顯示的大小

這個結論是錯的!詳細請點擊文章

結論

在非人爲設置的情況下,View的最終寬/高(getWidth() / getHeight()
與 View的測量寬/高 (getMeasuredWidth() / getMeasuredHeight())永遠是相等


6. 總結

  • 本文主要講解了自定義View中的Layout過程,總結如下:

 

原文地址:原文地址:https://www.jianshu.com/p/158736a2549d

下一節:1.3.2.4自定義View 繪畫過程

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章