Android View 的繪制流程解析


View 的繪制流程分為三步:measure(測量)、layout(布局)、draw(繪制)

measure是確定view的大小,layout是計算在界面中顯示的位置,draw便是最后的繪制步驟了。三者是先后執行的。

大致流程如下:

自定義 View 的第一步,肯定是明確的寬高,位置坐標,寬高是在測量階段得出。然后在布局階段,確定好位置信息,對矩形布局,之后的視覺效果就交給繪制流程了。

流程是很簡單的,但是實際的操作卻是很復雜的。

布局涉及兩個過程:測量過程和布局過程。測量過程通過 measure 方法實現,是 View 樹自頂向下的遍歷,每個 View 在循環過程中將尺寸細節往下傳遞,當測量過程完成之后,所有的 View 都存儲了自己的尺寸。第二個過程則是通過方法 layout 來實現的,也是自頂向下的。在這個過程中,每個父 View 負責通過計算好的尺寸放置它的子 View。

MeasureSpec

測量過程中,有一個很重要的類:MeasureSpec。MeasureSpec 是 View 中一個靜態類,代表測量規則,而它的手段則是用一個 int 數值來實現。我們知道一個 int 數值有 32 bit。MeasureSpec 將它的高 2 位用來代表測量模式 Mode,低 30 位用來代表數值大小 Size。

測量的尺寸好理解。說明下測量模式,測量模式可以取三個值,其含義如下:

子 View 在 xml 中的布局參數,對應的測量模式如下:

  • wrap_content ---> MeasureSpec.AT_MOST
  • match_parent -> MeasureSpec.EXACTLY
  • 具體值 -> MeasureSpec.EXACTLY

對於 UNSPECIFIED 模式,一般的 View 不會用上,在滾動組件或者列表中可能會用上。而這部分屬於比較深入的內容了,此處我們不細講。

MeasureSpec 的源碼如下:

/**
  * MeasureSpec類的源碼分析
  **/
public class MeasureSpec {
      // 進位大小 = 2的30次方
      // int的大小為32位,所以進位30位 = 使用int的32和31位做標志位
      private static final int MODE_SHIFT = 30;
      // 運算遮罩:0x3為16進制,10進制為3,二進制為11
      // 3向左進位30 = 11 00000000000(11后跟30個0)  
      // 作用:用1標注需要的值,0標注不要的值。因1與任何數做與運算都得任何數、0與任何數做與運算都得0
      private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
      // UNSPECIFIED的模式設置:0向左進位30 = 00后跟30個0,即00 00000000000
      public static final int UNSPECIFIED = 0 << MODE_SHIFT;
      // EXACTLY的模式設置:1向左進位30 = 01后跟30個0 ,即01 00000000000
      public static final int EXACTLY = 1 << MODE_SHIFT;
      // AT_MOST的模式設置:2向左進位30 = 10后跟30個0,即10 00000000000
      public static final int AT_MOST = 2 << MODE_SHIFT;

      /**
        * makeMeasureSpec()方法
        * 作用:根據提供的size和mode得到一個詳細的測量結果嗎,即measureSpec
        **/
      public static int makeMeasureSpec(int size, int mode) { 
            // 設計目的:使用一個32位的二進制數,其中:第32和第31位代表測量模式(mode)、后30位代表測量大小(size)
            // ~ 表示按位取反 
            return (size & ~MODE_MASK) | (mode & MODE_MASK);
      }
      
      /**
        * getMode()方法
        * 作用:通過measureSpec獲得測量模式(mode)
        **/    
      public static int getMode(int measureSpec) {  
          return (measureSpec & MODE_MASK);  
          // 即:測量模式(mode) = measureSpec & MODE_MASK;  
          // MODE_MASK = 運算遮罩 = 11 00000000000(11后跟30個0)
          //原理:保留measureSpec的高2位(即測量模式)、使用0替換后30位
          // 例如10 00..00100 & 11 00..00(11后跟30個0) = 10 00..00(AT_MOST),這樣就得到了mode的值
      }
      /**
        * getSize方法
        * 作用:通過measureSpec獲得測量大小size
        **/       
      public static int getSize(int measureSpec) {  
          return (measureSpec & ~MODE_MASK);  
          // size = measureSpec & ~MODE_MASK;  
          // 原理類似上面,即 將MODE_MASK取反,也就是變成了00 111111(00后跟30個1),將32,31替換成0也就是去掉mode,保留后30位的size  
      }
}

MeasureSpec 值是如何計算得來? 其實,子 View 的 MeasureSpec 值根據子 View 的布局參數(LayoutParams)和父容器的 MeasureSpec 值計算得來的,具體計算邏輯封裝在 ViewGroup 的 getChildMeasureSpec() 里。即子 view 的大小由父 view 的 MeasureSpec 值 和 子 view 自身的 LayoutParams 屬性共同決定。

下面,我們來看 getChildMeasureSpec() 的源碼分析:

/** 
  * 方法所在類:ViewGroup
  * 參數說明
  *
  * @param spec 父 view 的詳細測量值(MeasureSpec) 
  * @param padding view 當前尺寸的的內邊距 
  * @param childDimension 子視圖的尺寸(寬/高),如果子 View 未測量完成,則該值為子 View 的布局參數。測量完成則是子 View 的尺寸
  */
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
      //父view的測量模式
      int specMode = MeasureSpec.getMode(spec);
      //父view的大小
      int specSize = MeasureSpec.getSize(spec);
      //通過父view計算出的子view = 父大小-邊距(父要求的大小,但子view不一定用這個值)   
      int size = Math.max(0, specSize - padding);
      
      //子view想要的實際大小和模式(需要計算)  
      int resultSize = 0;
      int resultMode = 0;

      // 當父 View 的模式為 EXACITY 時,父 View 強加給子 View 確切的值
      //一般是父 View 設置為 match_parent 或者固定值的 ViewGroup
      switch (specMode) {
            case MeasureSpec.EXACTLY:
                  // 當子 View 測量完成,即有確切的值
                  // 子 View 大小為子自身所賦的值,模式大小為 EXACTLY
                  if (childDimension >= 0) {
                        resultSize = childDimension;
                        resultMode = MeasureSpec.EXACTLY;
                  } else if (childDimension == LayoutParams.MATCH_PARENT) {
                        // 測量未完成
                        // 當子 View 的 LayoutParams 為 MATCH_PARENT 時(-1)
                        //子 view 大小為父 view 大小,模式為 EXACTLY
                        resultSize = size;
                        resultMode = MeasureSpec.EXACTLY;
                  } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                        // 測量未完成
                        // 當子view的LayoutParams為WRAP_CONTENT時(-2)
                        // 子 view 決定自己的大小,但最大不能超過父 view,模式為 AT_MOST
                        resultSize = size;
                        resultMode = MeasureSpec.AT_MOST;
                  }
                  break;
            case MeasureSpec.AT_MOST:
                  // 當父 View 的模式為 AT_MOST 時,父 view 強加給子 View 一個最大的值。(一般是父 view 設置為 wrap_content)
                  // 代碼含義同上
                  if (childDimension >= 0) {
                      resultSize = childDimension;  
                      resultMode = MeasureSpec.EXACTLY;  
                  } else if (childDimension == LayoutParams.MATCH_PARENT) {  
                      resultSize = size;  
                      resultMode = MeasureSpec.AT_MOST;  
                  } else if (childDimension == LayoutParams.WRAP_CONTENT) {  
                      resultSize = size;  
                      resultMode = MeasureSpec.AT_MOST;  
                  }  
                  break;
            case MeasureSpec.UNSPECIFIED:
                  // 當父 View 的模式為 UNSPECIFIED 時,父容器不對 View 有任何限制,要多大給多大
                  // 多見於 ListView、GridView
                  if (childDimension >= 0) {  
                      // 子 view 大小為子自身所賦的值  
                      resultSize = childDimension;  
                      resultMode = MeasureSpec.EXACTLY;  
                  } else if (childDimension == LayoutParams.MATCH_PARENT) {  
                      // 因為父 View 為 UNSPECIFIED,API 大於23時,可以傳遞 hint 值用於測量,詳見 View.sUseZeroUnspecifiedMeasureSpec 的賦值處。通常 resultSize 為 0
                      resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; 
                      resultMode = MeasureSpec.UNSPECIFIED;  
                  } else if (childDimension == LayoutParams.WRAP_CONTENT) {  
                      // 說明同上
                      resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                      resultMode = MeasureSpec.UNSPECIFIED;  
                  }
                  break;  
      }
      return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

上述流程很簡單,可以用下面的流程圖概括:

得到了 MeasureSpec,我們就可以講講繪制流程了。不過不同的組件繪制方式不同,View 和 ViewGroup 的繪制流程又不同,下面我們會挑幾個特例,講講 View 的 measure 和 ViewGroup 的 layout 過程。

View 的繪制流程

View 的繪制流程比較簡單,我們先了解。通常在實現自定義 View 時,我們會終點關注 measure 和 draw 過程,draw 過程比較復雜,暫時不涉及。

measure

我們自定義一個 View,關鍵方法是 measure,但 measure 方法是 final 的,我們不能繼承更改,但 measure 中使用了一個 onMeasure() 方法。onMeasure() 是一個關鍵方法,也是本文重點研究內容,是官方暴露出來給我們使用的。該方法會測量 View 自己的大小,為正式布局提供建議。(注意,只是建議,至於用不用,要看onLayout)。

View 的 onMeasure 方法是默認實現,此處跳過。下面我們重點說明一下 ImageView 的測量流程,明白了 ImageView 的測量過程,也就明白了如何通過測量模式得到最終尺寸,也就明白了測量模式是怎么一回事。首先我們明確一個方法:setMeasuredDimension。使用該方法可以存儲測量出來的大小結果。

ImageView.onMeasure

代碼可以可能有點長,可以看看注釋:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // 解析為 ImageView 自動的 uri,並更新用於顯示的 Drawable
    resolveUri();
    // View 測量的寬高
    int w;
    int h;

    // View 顯示的內容的比例(不包括 padding)
    float desiredAspect = 0.0f;
    // 是否允許改變 View 的寬高
    boolean resizeWidth = false;
    boolean resizeHeight = false;
    // 布局模式
    final int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
    final int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);

    if (mDrawable == null) {
        // 沒有可顯示的 Drawable,Drawable 的寬高設置為 -1,View 的寬高設置為 0
        mDrawableWidth = -1;
        mDrawableHeight = -1;
        w = h = 0;
    } else {
        // 有可顯示的 Drawable,View 的寬高設置為 Drawable 的寬高
        w = mDrawableWidth;
        h = mDrawableHeight;
        // 寬高進行過濾操作,最小為 1(防止 Drawable 異常)
        if (w <= 0) w = 1;
        if (h <= 0) h = 1;

        // 是否需要根據 Drawable 的寬高比例更改 View 的范圍
        if (mAdjustViewBounds) {
            // View 的寬高的布局模式不為精准模式時,才能更改
            resizeWidth = widthSpecMode != MeasureSpec.EXACTLY;
            resizeHeight = heightSpecMode != MeasureSpec.EXACTLY;

            desiredAspect = (float) w / (float) h;
        }
    }
    // 上下左右的 padding
    final int pleft = mPaddingLeft;
    final int pright = mPaddingRight;
    final int ptop = mPaddingTop;
    final int pbottom = mPaddingBottom;

    int widthSize;
    int heightSize;

    if (resizeWidth || resizeHeight) {
        // View 的寬高需要二次更改

        // 首次測量,會根據最大尺寸獲取目標尺寸
        widthSize = resolveAdjustedSize(w + pleft + pright, mMaxWidth, widthMeasureSpec);
        heightSize = resolveAdjustedSize(h + ptop + pbottom, mMaxHeight, heightMeasureSpec);
        // 比例不為 0 才二次測量,避免異常情況
        if (desiredAspect != 0.0f) {
            // 圖片實際的調整比例
            final float actualAspect = (float)(widthSize - pleft - pright) /
                (heightSize - ptop - pbottom);
            // 實際的寬高比例和預期的寬高比例不等,需要重新調整尺寸
            // 注意 float 的不等於的比較方式,!= 並不一定准確
            if (Math.abs(actualAspect - desiredAspect) > 0.0000001) {
                boolean done = false;
                // 需要調整寬度
                if (resizeWidth) {
                    int newWidth = (int)(desiredAspect * (heightSize - ptop - pbottom)) +
                        pleft + pright;

                    // 此代碼 API 大於 17 時生效,否則不生效
                    if (!resizeHeight && !sCompatAdjustViewBounds) {
                        widthSize = resolveAdjustedSize(newWidth, mMaxWidth, widthMeasureSpec);
                    }
                    // 新的尺寸小於等於原尺寸,才重新賦值。因為 width 前一次測量已經得到了最大的可能寬度
                    if (newWidth <= widthSize) {
                        widthSize = newWidth;
                        // 寬度按照 desiredAspect 比例改過,此時view是符合 desiredAspect 的。
                        done = true;
                    }
                }

                // 需要調整高度,寬度按照比例該過后,就不再更改了。=
                if (!done && resizeHeight) {
                    int newHeight = (int)((widthSize - pleft - pright) / desiredAspect) +
                        ptop + pbottom;

                    // 說明同上
                    if (!resizeWidth && !sCompatAdjustViewBounds) {
                        heightSize = resolveAdjustedSize(newHeight, mMaxHeight, heightMeasureSpec);
                    }

                    if (newHeight <= heightSize) {
                        heightSize = newHeight;
                    }
                }
            }
        }
    } else {
        // View 的寬高不能更改,走正常的測量流程

        // View 的寬高加上 padding
        w += pleft + pright;
        h += ptop + pbottom;
        // 測量出來的寬高不能小於設置的 View 的最小值
        // 該最小寬度由 minWidth(minHeight) 和背景 Drawable 決定
        w = Math.max(w, getSuggestedMinimumWidth());
        h = Math.max(h, getSuggestedMinimumHeight());
        // 根據結果,獲取最終的值
        widthSize = resolveSizeAndState(w, widthMeasureSpec, 0);
        heightSize = resolveSizeAndState(h, heightMeasureSpec, 0);
    }
    // 保存存儲的結果
    setMeasuredDimension(widthSize, heightSize);
}

上面 ImageView 的測量流程其實很簡單,可以用下面的流程圖描述:

其實,上面的測量邏輯,還是很簡單的。並不復雜,主要是賦值過程的計算,是在當前的測量結果和限定值之間的取舍(自身設置的最大/最小值,父類給予的限定值)。而賦值過程的重點其實在 resolveAdjustedSize 這個方法。從代碼中的使用可以看出來,ImageView的寬高是在 測量值/自身最大值/父類限定值 三者間得出的。

// 值通過測量值(帶上padding)/自身設置的最大值/父類的布局要求,三者計算
widthSize = resolveAdjustedSize(w + pleft + pright, mMaxWidth, widthMeasureSpec);
heightSize = resolveAdjustedSize(h + ptop + pbottom, mMaxHeight, heightMeasureSpec);

下面我們來看看這個方法的具體實現,注意:可以看看這個方法的源碼,官方給出的注釋說明是 measure 過程的核心思想的體現。

/**
 * 解析得到最終的結果
 *
 * @param desiredSize ImageView 自身測量出的尺寸
 * @param maxSize     ImageView 布局參數傳入的最大尺寸
 * @param measureSpec 父布局對 ImageView 的測量要求
 */
private int resolveAdjustedSize(int desiredSize, int maxSize, int measureSpec) {
    int result = desiredSize;
    final int specMode = MeasureSpec.getMode(measureSpec);
    // 父 View  對子 View 的尺寸限制
    final int specSize =  MeasureSpec.getSize(measureSpec);
    switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            // 父布局對測量無限制,則用自身測量的尺寸,但不應該超過最大值
            result = Math.min(desiredSize, maxSize);
            break;
        case MeasureSpec.AT_MOST:
            // 父布局對測量規定了最大值,測量的結果可以盡可能的大,但是不能超過 specSize,
            // 也不能超過自身規定的最大尺寸 maxSize。則在三者中取最小值
            result = Math.min(Math.min(desiredSize, specSize), maxSize);
            break;
        case MeasureSpec.EXACTLY:
            // 父布局對測量要求是精確的,沒得選,只能使用父布局傳入的值
            result = specSize;
            break;
    }
    return result;
}

講解了 ImageView 的 measure 過程,我們來看看 View Group 的布局過程。ViewGroup 的繪制和布局過程主要是對子 View 操作。可以理解成它並不太會關注自己的事,因為它是它父 View 的子 View,他的測量是在其父 View 中調用的,當然,會有一個根布局。這就是一個遞歸調用的過程。

按照流程,我們知道 View 的布局,最終會走到 onLayout 方法,此處就以 FrameLayout 為例,講解下布局操作。

FrameLayout.onLayout

按照慣例,先上源碼,再上圖。FrameLayout.onLayout 的主要代碼是在 layoutChildren 這個方法中,下面我們講講 layoutChildren 這個方法。

/**
 * 布局 FrameLayout
 * 
 * @param left              當前 ViewGroup 距父布局左邊界的距離
 × @param top               當前 ViewGroup 距父布局上邊界的距離
 * @param right             當前 ViewGroup 距父布局有邊界的距離
 × @param bottom            當前 ViewGroup 距父布局下邊界的距離
 * @param forceLeftGravity  暫未用上的參數
 * */
void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
    // 獲得子 View 的數量
    final int count = getChildCount();
    // 當前布局的左側布局起點(加上左 padding)
    final int parentLeft = getPaddingLeftWithForeground();
    // 當前布局的右側布局終點(減去右 padding)
    final int parentRight = right - left - getPaddingRightWithForeground();
    // 當前布局的上側布局起點(加上上 padding)
    final int parentTop = getPaddingTopWithForeground();
    // 當前布局的下側布局終點(減去下 padding)
    final int parentBottom = bottom - top - getPaddingBottomWithForeground();

    for (int i = 0; i < count; i++) {
        final View child = getChildAt(i);
        // 子 View 不設置為 GONE,才進行布局。即 GONE 屬性不占用任何空間
        if (child.getVisibility() != GONE) {
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            // 獲取測量出的尺寸,布局前已先測量
            final int width = child.getMeasuredWidth();
            final int height = child.getMeasuredHeight();

            int childLeft;
            int childTop;

            int gravity = lp.gravity;
            if (gravity == -1) {
                // 對齊方式默認是左上角
                gravity = DEFAULT_CHILD_GRAVITY;
            }
            // 獲取布局方向,RTL 還是 LTR
            final int layoutDirection = getLayoutDirection();
            // 獲取水平方向上的對齊方式
            final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
            // 獲取豎直方向上的對齊樣式
            final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
            // 先判斷水平方向上的對齊方式
            switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                    // 子 View 居中
                case Gravity.CENTER_HORIZONTAL:
                    // 參見代碼下的圖,有助於理解
                    // parentRight - parentLeft - width 可以簡單的理解為左右 margin 和
                    childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
                        lp.leftMargin - lp.rightMargin;
                    break;
                    // 子 View 右對齊
                case Gravity.RIGHT:
                    if (!forceLeftGravity) {
                        childLeft = parentRight - width - lp.rightMargin;
                        break;
                    }
                    // 子 View 左對齊
                case Gravity.LEFT:
                default:
                    childLeft = parentLeft + lp.leftMargin;
            }
            // 再判斷垂直方向上的對齊方式
            switch (verticalGravity) {
                    // 頂部對齊
                case Gravity.TOP:
                    childTop = parentTop + lp.topMargin;
                    break;
                    // 豎直居中對齊
                case Gravity.CENTER_VERTICAL:
                    childTop = parentTop + (parentBottom - parentTop - height) / 2 +
                        lp.topMargin - lp.bottomMargin;
                    break;
                    // 底部對齊
                case Gravity.BOTTOM:
                    childTop = parentBottom - height - lp.bottomMargin;
                    break;
                default:
                    childTop = parentTop + lp.topMargin;
            }
            // 計算出了子 View 的位置,布局子 View(即調用子 View 的布局方法)
            child.layout(childLeft, childTop, childLeft + width, childTop + height);
        }
    }
}

上面的代碼,結合圖片便能很輕松的理解。就不詳講了。此處聊點其他的---子 View 的對齊方式哪里來的?從上面的代碼中,可以看出,是從 View 的布局參數中取的,而 View 的布局參數是怎么來的呢?

要想了解布局參數怎么來的,我們就得首先了解下系統是怎么向一個 ViewGroup 中添加 View 的。我們知道,一個界面的布局,我們通常是在 xml 中設計的。而在所有的 ViewGroup 中,我們都可以加入子 View,並為子 View 加入約束。即 加入 View ---> 加入約束的流程。此處加入約束的流程便是加入布局參數的流程。布局參數是子 View 告訴父 View 自己如何布局的途徑。

讓我們來看看加入 View 的流程,向 ViewGroup 中加入 View 是調用了 ViewGroup 的 addView 方法,addView 有好幾個同名方法。我們來看看。

ViewGroup.addView

我們在 ViewGroup 的源碼中搜索,會首先搜索到一個單參的 addView 方法。

// 很簡單,點擊進入雙參的方法
public void addView(View child) {
    addView(child, -1);
}

public void addView(View child, int index) {
    // 待添加的 View 不能為空
    if (child == null) {
        throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
    }
    // 獲取布局參數
    LayoutParams params = child.getLayoutParams();
    if (params == null) {
        // 取布局參數為空,則生成默認的布局參數
        params = generateDefaultLayoutParams();
        if (params == null) {
            throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
        }
    }
    addView(child, index, params);
}

雙參的方法,其實很簡單。主要就是做了兩個限制:

  • 加入 ViewGroup 的 View 不能為空
  • View 如果是無布局參數,會生成一個默認的。如果無法生成默認的布局參數,則會拋異常,無法加入 ViewGroup 中。

即待加入 ViewGroup 中的 View,不能為空,且布局參數不能為空。從上面的代碼中,我們可以知道,加入約束,是在加入 View 的過程中便加入了。下面讓我們來看看 ViewGroup 的 generateDefaultLayoutParams 方法。

protected LayoutParams generateDefaultLayoutParams() {
    return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}

上面代碼中的 LayoutParams 是何方神聖?其實 LayoutParams 是 ViewGroup 的一個公共內部類,它描述了 ViewGroup 的子 View 的尺寸(寬/高)。而它還有個子類:MarginLayoutParams。顧名思義,其是在 LayoutParams 加入了子 View 的 margin 描述。ViewGroup 中的子類就這兩個了。也沒有看到對齊方式的相關描述呀?不急,LayoutParams 旁邊是有箭頭的

點擊箭頭,我們找到了熟悉的身影---FrameLayout。讓我們點擊進去看看。

public static class LayoutParams extends MarginLayoutParams {
    
    public static final int UNSPECIFIED_GRAVITY = -1;

    @InspectableProperty(name = "layout_gravity", 
                         valueType = InspectableProperty.ValueType.GRAVITY)
    public int gravity = UNSPECIFIED_GRAVITY;

    public LayoutParams(@NonNull Context c, @Nullable AttributeSet attrs) {
        super(c, attrs);

        final TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.FrameLayout_Layout);
        gravity = a.getInt(R.styleable.FrameLayout_Layout_layout_gravity, UNSPECIFIED_GRAVITY);
        a.recycle();
    }

    public LayoutParams(int width, int height) {
        super(width, height);
    }

    public LayoutParams(int width, int height, int gravity) {
        super(width, height);
        this.gravity = gravity;
    }

    public LayoutParams(@NonNull ViewGroup.LayoutParams source) {
        super(source);
    }

    public LayoutParams(@NonNull ViewGroup.MarginLayoutParams source) {
        super(source);
    }
    // 此處的 LayoutParams 是FrameLayout 中的,不是 ViewGroup 中的
    public LayoutParams(@NonNull LayoutParams source) {
        super(source);

        this.gravity = source.gravity;
    }
}

上面便是 FrameLayout 中的 LayoutParams,我們發現其實際上是繼承自 MarginLayoutParams 的。在 Margin 的基礎上,增加了對齊方式的描述。實際上,每一個繼承自 ViewGroup 的容器類,如果想要實現自己的布局規則,都必須照着這個模版,先在 LayoutParams 中定義自己的布局參數,再在 onLayout 方法中定義自己的規則。每個容器類都是照着這個模版來的。

可以看出,如何精進自己,最好的方式還是閱讀源碼。但是,閱讀源碼也要有條件。

  1. 你會用了。再去讀源碼,了解為什么要這樣。否則就很容易事倍功半,效果奇差。
  2. 帶着目的讀源碼,比如我這次,就是為了了解 View 的繪制流程,才找了很簡單的兩個官方實現:ImageView 和 FrameLayout。讀了源碼,一下子就明白了 measure 和 layout 在干什么,以及怎么干。

實際上,上面的 LayoutParam部分,已屬於自定義 ViewGroup 的內容了。這里算是小試牛刀,拋磚引玉。下一講,我們來講講如何自定義 View 和 ViewGroup。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM