自定義 view - 布局 onLayout

自定義 view 的3個核心方法

  • onMeasure
    根據 view 的測量模式計算確定 view 的寬高
  • onLayout
    ViewGroup 中對所有的子 view 排版,決定子 view 的位置
  • onDraw
    具體繪制 view

本節我們來說說 onLayout,view 如何決定顯示位置的


我們已經知道 view 有2種:view ,ViewGroup 。那么我們弄明白 view 和 ViewGroup 如何布局就行了,大家來看圖:


944365-6e978f448667eb52.png

view 如何布局

view 是不可再分的,view 里面不能再放 view 進去了,那么 view 的布局就是決定自己的布局,換句話說就是決定自己在父控件中的位置,但其實我們在繼承 view 的自定義 view 中很少重寫 view 的 onLayout 方法,因為 view 的布局方法不是給 view 自己用的,是給父控件用的,view 只需要計算出自己的寬高大小,然后回傳數據給父控件,父控件根據其布局特性,調用子 view 的布局方法

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大小和位置是否發生了變化 & 返回 
    // ->>分析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 的4個點的位置,注意這4個參數是對應 view 坐標系的 left ,top ,right ,bottom 4個值,在我們自己需要使用這個方法時,應該知道4個坐標值表示什么,怎么算

最后我們要知道父控件會計算出子 view 的位置坐標,然后統一遍歷所有的子 view ,執行他們的 layout 方法已決定他們的位置。


ViewGroup 如何布局

上面說了具體的 view 布局其實需要他所屬的父控件,也就是 ViewGroup 來決定。

ViewGroup 會自上而下、一層層地遍歷所有的子 view,直到完成整個View樹的layout()過程


自上而下遍歷

遍歷流程

ViewGroup 是一個容器,是用來存放具體 view 的,不同的 ViewGroup 游不同的對 view 的排列特性,這些特性決定了 期中 view 的位置,這些位置就是 ViewGroup 在 onLayout 方法中計算出來的, 那么 ViewGroup 作為 view 容器的核心就是其 onLayout 布局方法了。原理很簡單,明白了 ViewGroup 布局干了什么就行了

/**
  * 分析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 的 onLayout 方法寫的很簡單,無關的都刪了。一般我們不會自己去直接繼承 ViewGroup 自己去寫 view 容器,但是某些時候我們還是會干一些相同的事的,比如 behavior 嵌套滾動中,recycleview 的 layoutManage 布局管理中我們都會涉及到這部分知識,其實原理沒多難,計算出子 view 的位置,然后調用子 view 的 layout 方法,難的是如何計算位置,這個就不是這里討論的了


最后

我對自定義 view 的 onLayout 沒什么經驗,平時也沒用到過,這里簡單說說 onLayout 的部分,就是為了給對這里糊涂的同學捋順思路。想要來哦解更多請再去找資料吧


?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容