1. 程式人生 > >Android自定義View-Layout原理篇

Android自定義View-Layout原理篇

Android自定義View通常需要經過measure、layout和draw過程,如果你沒有了解過measure過程,可以先看看這篇文章

一、Layout的作用:計算檢視的位置,即Left、Top、Right、Bottom四點的位置

二、layout過程:跟measure類似,layout也會根據View的型別分成兩種情況進行處理。

View型別 layout過程
單一View 只計算View本身的位置
ViewGroup 確定View本身及子View在父容器中的位置

接下來我們對這兩種情況分別進行分析。

(1)單一View的layout過程

具體流程:layout()→onLayout()

相關原始碼分析如下:

/**
  * 原始碼分析: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 底部位置

}

我們對單一View的layout過程總結一下:

單一View layout過程

(2)ViewGroup的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 layout過程

最後,說一個比較重要的問題:getWidth()、getHeight()與getMeasureWidth()、getMeasureHeight()獲取的寬高的區別是什麼?

首先,我們先看下兩者的定義,

getWidth()/getHeight():獲取View最終的寬高

getMeasureWidth()/getMeasureHeight():獲取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()強行設定,這種情況下,測量的值與最終的值是不一樣的。