Android 自定義View學習(十一)——ViewGroup測量知識學習

學習資料

上篇學習了View的測量方法,了解一些Android UI架構圖的知識,這篇記錄學習ViewGroup的測量


1. ViewGroup <p>

A ViewGroup is a special view that can contain other views (called children.) The view group is the base class for layouts and views containers. This class also defines the ViewGroup.LayoutParams class which serves as the base class for layouts parameters.

直譯: ViewGroup是一個可以包含其他子View特殊的View。并且是那些子View或者布局的父容器。而且ViewGroup定義了ViewGroup.LayoutParams這個類

ViewGroup是一個抽象類,內部的子View可以是一個View也可以是另一個ViewGroup

例如,在LinearLayout中,可以加入一個TextView也可以加入另外一個LinearLayout


ViewGroup的職責

ViewGroup相當于一個放置View的容器,并且我們在寫布局xml的時候,會告訴容器(凡是以layout為開頭的屬性,都是為用于告訴容器的),我們的寬度(layout_width)、高度(layout_height)、對齊方式(layout_gravity)等;當然還有margin等;于是乎,ViewGroup的職能為:給childView計算出建議的寬和高和測量模式 ;決定childView的位置;為什么只是建議的寬和高,而不是直接確定呢,別忘了childView寬和高可以設置為wrap_content,這樣只有childView才能計算出自己的寬和高。

View的職責

View的職責,根據測量模式和ViewGroup給出的建議的寬和高,計算出自己的寬和高;同時還有個更重要的職責是:在ViewGroup為其指定的區域內繪制自己的形態。

以上摘抄鴻洋大神的Android 手把手教您自定義ViewGroup(一)


2. 測量方法 <p>

View的測量大小除了自身還會受父容器的影響。一般這個父容器就是一個ViewGroup。對于一個ViewGroup來說,除了完成自身的測量外,還要遍歷內部的childView的測量方法,各個childView再遞歸執行這個步驟。

ViewGroup源代碼內并沒有重寫onMeasure()方法,而是提供了幾個測量相關的方法。

原因也比較容易理解,由于ViewGroup是一個抽象類,有不同的子類childView,有不同的布局屬性,測量的細節不同。例如LinearLayputRelativeLayout。每個繼承之ViewGroupLayout,各自根據自身的布局屬性來重寫onMeasure()方法


2.1 測量的過程 <p>

ViewGroup的測量過程主要用到了三個方法

  1. measureChildren() ,遍歷所有的childView
  2. getChildMeasureSpec(),確定測量規格
  3. measureChild(),調用測量規格。這個方法內,根據2確定好的測量規格,childView調用了measure()方法,而measure()內部調用的方法就有onMeasure()

2.1.1 measureChildren() 遍歷所有的childView <p>

源碼:

/**
 * Ask all of the children of this view to measure themselves, taking into account both the MeasureSpec requirements for this view and its padding.
 *
 * We skip children that are in the GONE state The heavy liftingis done in getChildMeasureSpec.
 *
 * @param widthMeasureSpec The width requirements for this view
 * @param heightMeasureSpec The height requirements for this view
 */
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
    final int size = mChildrenCount;
    final View[] children = mChildren;
    for (int i = 0; i < size; ++i) { //進行遍歷
        final View child = children[i];
        if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {//確定childview是否可見
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
        }
    }
}

方法內主要就是遍歷了所有的chlidView,判斷每個childViewvisibility值,確定當前的這個childView可見,然后調用了measureChild(child, widthMeasureSpec, heightMeasureSpec)方法


2.1.2 measureChild(),調用測量規格 <p>

把這個方法放在getChildMeasureSpec()確定測量規格之前,是因為measureChild()內部調用了getChildMeasureSpec()

源碼:

    /**
     * Ask one of the children of this view to measure itself, taking into account both the MeasureSpec requirements for this view and its padding.
     *
     * The heavy lifting is done in getChildMeasureSpec.
     *
     * @param child The child to measure
     * @param parentWidthMeasureSpec The width requirements for this view
     * @param parentHeightMeasureSpec The height requirements for this view
     */
    protected void measureChild(View child, int parentWidthMeasureSpec,int parentHeightMeasureSpec) {
        // 獲取childView的布局參數
        final LayoutParams lp = child.getLayoutParams();

        //將ViewGroup的測量規格,上下和左右的邊距還有childView自身的寬高傳入getChildMeasureSpec方法計算最終測量規格 
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,mPaddingTop + mPaddingBottom, lp.height);

        //調用childView的measure(),measure()方法內就是回調`onMeasure()`方法
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

Viewmeasure()測量方法調用過程,在上篇View的測量方法學習過程中,只是用文字簡單概括了幾句,并沒有記錄學習源碼方法的調用過程,可以去愛哥的自定義控件其實很簡單7/12進行補充學習 : )


2.1.3 getChildMeasureSpec(),確定childview的測量規格 <p>

源碼:

    /**
     * Does the hard part of measureChildren: figuring out the MeasureSpec to pass to a particular child. This method figures out the right MeasureSpec for one dimension (height or width) of one child view.
     *
     * The goal is to combine information from our MeasureSpec with the LayoutParams of the child to get the best possible results. For example, if the this view knows its size (because its MeasureSpec has a mode of EXACTLY), and the child has indicated in its LayoutParams that it wants to be the same size as the parent, the parent should ask the child to layout given an exact size.
     *
     * @param spec The requirements for this view
     * @param padding The padding of this view for the current dimension and margins, if applicable
     * @param childDimension How big the child wants to be in the current dimension
     * @return a MeasureSpec integer for the child
     */
    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        //ViewGroup的測量模式及大小
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);

        //將ViewGroup的測量大小減去內邊距
        int size = Math.max(0, specSize - padding);

        // 聲明臨時變量存值  
        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
        case MeasureSpec.EXACTLY://ViewGroup的測量模式為精確模式
            //根據childView的布局參數判斷 
            if (childDimension >= 0) {//如果childDimension是一個具體的值  
                // 將childDimension賦予resultSize ,作為結果
                resultSize = childDimension;
                //將臨時resultMode 也設置為精確模式
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {//childView的布局參數為精確模式  
               //將ViewGroup的大小做為結果
                resultSize = size;
                //因為ViewGroup的大小是受到限制值的限制所以childView的大小也應該受到父容器的限制  
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {//childView的布局參數為最大值模式 
                //ViewGroup的大小作為結果  
                resultSize = size;
              //將臨時resultMode 也設置為最大值模式
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        case MeasureSpec.AT_MOST://ViewGroup的測量模式為精確模式
            //根據childView的布局參數判斷 
            if (childDimension >= 0) {//如果childDimension是一個具體的值  
                 // 將childDimension賦予resultSize ,作為結果
                resultSize = childDimension;
                //將臨時resultMode 也設置為精確模式
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {//如果childDimension是精確模式 
                //因為ViewGroup的大小是受到限制值的限制所以chidlView的大小也應該受到父容器的限制 
                 //ViewGroup的大小作為結果  
                resultSize = size;
                 //將臨時resultMode 也設置為最大值模式
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {// 如果childDimension是最大值模式 
                
                 //ViewGroup的大小作為結果  
                resultSize = size;
                 //將臨時resultMode 也設置為最大值模式
                //childView的大小包裹了其內容后不能超過ViewgGroup               
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        case MeasureSpec.UNSPECIFIED://ViewGroup尺寸大小未受限制  
            if (childDimension >= 0) {//如果childDimension是一個具體的值  
                 // 將childDimension賦予resultSize ,作為結果
                resultSize = childDimension;
                 // 將臨時resultMode 也設置為精確模式 
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {如果childDimension是精確模式
               //ViewGroup大小不受限制,對childView來說也可以是任意大小,所以不指定也不限制childView的大小
               //對是否總是返回0進行判斷 sUseZeroUnspecifiedMeasureSpec受版本影響
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                 // 將臨時resultMode 也設置為UNSPECIFIED,無限制摸式 
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {如果childDimension是最大值
                //ViewGroup大小不受限制,對childView來說也可以是任意大小,所以不指定也不限制childView的大小
                //sUseZeroUnspecifiedMeasureSpec = targetSdkVersion < M
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                 // 將臨時resultMode 也設置為UNSPECIFIED,無限制摸式 
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //返回封裝后的測量規格  
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

至此我們可以看到一個View的大小由其父容器的測量規格MeasureSpecView本身的布局參數LayoutParams共同決定,但是即便如此,最終封裝的測量規格也是一個期望值,究竟有多大還是我們調用setMeasuredDimension方法設置的。上面的代碼中有些朋友看了可能會有疑問為什么childDimension >= 0就表示一個確切值呢?原因很簡單,因為在LayoutParams中MATCH_PARENTWRAP_CONTENT均為負數、哈哈??!正是基于這點,Android巧妙地將實際值和相對的布局參數分離開來。

以上摘自愛哥的自定義控件其實很簡單7/12


3. 布局方法 <p>

ViewGorup是個抽象類,繼承ViewGroup,肯定就有必須要實現的抽象方法,這個抽象方法就是onLayout()

代碼:

public class CustomLayout extends ViewGroup {
    public CustomLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {

    }
}

經過了onMeasure()方法后,確定ViewGroup的位置和childView寬高后,在ViewGrouponLayout()方法內,遍歷ViewGroup內所有的childView,并讓每個childView調用Viewlayout()方法,在layout()方法內,首先會確定每個childView的頂點的位置,之后又調用childViewonLayout()方法


3.1 簡單實現CustomLayout <p>

public class CustomLayout extends ViewGroup {
    public CustomLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    /**
     * 測量
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        final int count = getChildCount();
        if (count > 0) {
            measureChildren(widthMeasureSpec, heightMeasureSpec);
        }
    }

    /**
     * 布局
     */
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        final int count = getChildCount();
        if (count > 0) {
            // 遍歷內部的childView
            for (int i = 0; i < count; i++) {
                View child = getChildAt(i);                                    
                child.layout(0, 0, child.getMeasuredWidth(), child.getMeasuredHeight());
            }
        }
    }
}

代碼很簡單,就是先遍歷測量,在遍歷布局


布局xml:

<com.szlk.customview.custom.CustomLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="英勇青銅5"
        android:textColor="@color/colorAccent"
        android:textSize="30sp" />

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAllCaps="false"
        android:text="@string/view_group__name" />
</com.szlk.customview.custom.CustomLayout>
CustomLayout

雖然TextViewButtonCustomLayout都已經繪制出來,但ButtonTextView給蓋住了。原因很明顯,在繪制第2個子控件Button時,依然從CustomView(0,0)點開始繪制,并沒有考慮TextView的高度


3.2 進行優化修改 <p>

修改需要考慮的就是已經繪制過的childView的高度

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    final int count = getChildCount();
    if (count > 0) {
        int mHeight = 0;
        // 遍歷內部的childView
        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            child.layout(0, mHeight, child.getMeasuredWidth(), child.getMeasuredHeight()+mHeight);
            mHeight +=  child.getMeasuredHeight();
        }
    }
}

增加一個臨時變量int mHeight = 0,繪制過TextView就將高度加起來,就等于繪制Button時,開始繪制的點便是(0,mHeight),于是,Button也就在TextView下方

考慮已經繪制過的childView的高

有點像一個超級簡單的VerticalLinearLayout

Horizontal的,就可以考慮child.layout()時,改變開始繪制時,x軸的坐標點


3.3 getMeasuredWidth()和getWidth() <p>

onLayout()方法中

child.layout(0, 0, child.getMeasuredWidth(),child.getMeasuredHeight())

使用的是child.getMeasuredWidth(),而不是child.getWidth()


child.getWidth()源碼:

/**
 * Return the width of the your view.
 *
 * @return The width of your view, in pixels.
 */
@ViewDebug.ExportedProperty(category = "layout")
public final int getWidth() {
    return mRight - mLeft;
}

其中mRightmleft值,是在onLayout()方法后拿到的,在onLayout()方法中,返回的是0


child.getMeasuredWidth()源碼:

    /**
     * Like {@link #getMeasuredWidthAndState()}, but only returns the raw width component (that is the result is masked by {@link #MEASURED_SIZE_MASK}).
     *
     * @return The raw measured width of this view.
     */
    public final int getMeasuredWidth() {
        return mMeasuredWidth & MEASURED_SIZE_MASK;
    }

之后便是追著mMeasuredWidth這個值走,經過一系列的測量方法后,最終來到onMeasure()

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(...);
}

mMeasuredWidth則在onMeasure()方法后便可以到了,拿到的時間比getWidth()要早


使用場景:

  • getMeasuredWidth():onLayout()方法內
  • getWidth():除了onLayout()方法,其他之外

使用場景絕大部分情況下都是符合的,這兩個方法拿到的值,絕大多數時候也是一樣的

可以看看Android開發之getMeasuredWidth和getWidth區別從源碼分析


4.考慮Padding,Margins <p>

有了上篇onMeasure()經驗,知道PaddingMargins,也需要優化處理的


4.1 Padding

xml文件中加入padding之后

Padding將內容吃掉

修改代碼:

protected void onLayout(boolean changed, int l, int t, int r, int b) {
   final int count = getChildCount();
   final int parentPaddingLeft = getPaddingLeft();
   final int parentPaddingTop = getPaddingTop();
   if (count > 0) {
       int mHeight = 0;
       // 遍歷內部的childView
       for (int i = 0; i < count; i++) {
           View child = getChildAt(i);
           final int left = parentPaddingLeft;
           final int top = mHeight + parentPaddingTop;
           final int right = child.getMeasuredWidth() + parentPaddingLeft;
           final int bottom = child.getMeasuredHeight() + mHeight + parentPaddingTop;
    child.layout(left, top, right, bottom);
            mHeight += child.getMeasuredHeight();
        }
    }
}

主要就是考慮getPaddingLeft()getPaddingTop()

簡單優化Padding

這樣也只是做了最簡單的優化,一旦Padding大到了一定程度,還是會吃掉內部的childView


4.2 Margins <p>

CustomLayout內加Margins有效,可內部的childView加了卻無效。上篇提到過,ViewMargins是封裝在LayoutParams后由ViewGroup來處理的

自定義LayoutParams:

public static class LayoutParams extends MarginLayoutParams {

    public LayoutParams(Context c, AttributeSet attrs) {
        super(c, attrs);
    }

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

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

并沒有做任何設置,還對更多屬性進行設置,以后再學習


完整代碼:

public class CustomLayout extends ViewGroup {


    public CustomLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    /**
     * 測量
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        final int count = getChildCount();
        // 臨時ViewGroup大小值
        int viewGroupWidth = 0;
        int viewGroupHeight = 0;
        if (count > 0) {
            // 遍歷childView
            for (int i = 0; i < count; i++) {
                // childView
                View child = getChildAt(i);
                LayoutParams lp = (LayoutParams) child.getLayoutParams();
                //測量childView包含外邊距
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
                // 計算父容器的期望值
                viewGroupWidth += child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
                viewGroupHeight += child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
            }

            // ViewGroup內邊距
            viewGroupWidth += getPaddingLeft() + getPaddingRight();
            viewGroupHeight += getPaddingTop() + getPaddingBottom();

            //和建議最小值進行比較
            viewGroupWidth = Math.max(viewGroupWidth, getSuggestedMinimumWidth());
            viewGroupHeight = Math.max(viewGroupHeight, getSuggestedMinimumHeight());
        }
        setMeasuredDimension(resolveSize(viewGroupWidth, widthMeasureSpec), resolveSize(viewGroupHeight, heightMeasureSpec));
    }


    /**
     * 布局
     */
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        // ViewGroup的內邊距
        int parentPaddingLeft = getPaddingLeft();
        int parentPaddingTop = getPaddingTop();
        if (getChildCount() > 0) {
            int mHeight = 0;
            for (int i = 0; i < getChildCount(); i++) {
                View child = getChildAt(i);
                //獲取 LayoutParams
                LayoutParams lp = (LayoutParams) child.getLayoutParams();
                //childView的四個頂點
                final int left = parentPaddingLeft + lp.leftMargin;
                final int top = mHeight + parentPaddingTop + lp.topMargin;
                final int right = child.getMeasuredWidth() + parentPaddingLeft + lp.leftMargin;
                final int bottom = child.getMeasuredHeight() + mHeight + parentPaddingTop + lp.topMargin;

                child.layout(left, top, right, bottom);
                // 累加已經繪制的childView的高
                mHeight += child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
            }
        }
    }

    /**
     *  獲取布局文件中的布局參數
     */
    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new CustomLayout.LayoutParams(getContext(), attrs);
    }

    /**
     *  獲取默認的布局參數
     */
    @Override
    protected LayoutParams generateDefaultLayoutParams() {
        return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
    }

    /**
     *  生成自己的布局參數
     */
    @Override
    protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
        return new LayoutParams(p);
    }

    /**
     *  檢查當前布局參數是否是我們定義的類型
     */
    @Override
    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
        return p instanceof LayoutParams;
    }

    /**
     * 自定義LayoutParams
     */
    public static class LayoutParams extends MarginLayoutParams {

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

        }

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

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

代碼基本照搬的愛哥的。。。。


xml布局文件

<com.szlk.customview.custom.CustomLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_margin="50dp"
    android:background="@android:color/holo_blue_bright"
    android:padding="10dp">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="20dp"
        android:background="@color/colorPrimary"
        android:text="英勇青銅5"
        android:textColor="@color/colorAccent"
        android:textSize="30sp" />

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="20dp"
        android:text="@string/view_group__name"
        android:textAllCaps="false" />

</com.szlk.customview.custom.CustomLayout>
支持Margin

這時,CustomLayout和內部控件的Margin都已經支持,但真正以后實際開發,要優化考慮的要比這嚴謹。這里只是了解學習


5.最后 <p>

重點是理解ViewGroup的測量過程,理解后,接下來再學習View的工作流程就會比較容易理解

本人很菜,有錯誤,請指出

共勉 : )

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

推薦閱讀更多精彩內容