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

學(xué)習(xí)資料

上篇學(xué)習(xí)了View的測量方法,了解一些Android UI架構(gòu)圖的知識,這篇記錄學(xué)習(xí)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是一個抽象類,內(nèi)部的子View可以是一個View也可以是另一個ViewGroup

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


ViewGroup的職責(zé)

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

View的職責(zé)

View的職責(zé),根據(jù)測量模式和ViewGroup給出的建議的寬和高,計算出自己的寬和高;同時還有個更重要的職責(zé)是:在ViewGroup為其指定的區(qū)域內(nèi)繪制自己的形態(tài)。

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


2. 測量方法 <p>

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

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

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


2.1 測量的過程 <p>

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

  1. measureChildren() ,遍歷所有的childView
  2. getChildMeasureSpec(),確定測量規(guī)格
  3. measureChild(),調(diào)用測量規(guī)格。這個方法內(nèi),根據(jù)2確定好的測量規(guī)格,childView調(diào)用了measure()方法,而measure()內(nèi)部調(diào)用的方法就有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);
        }
    }
}

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


2.1.2 measureChild(),調(diào)用測量規(guī)格 <p>

把這個方法放在getChildMeasureSpec()確定測量規(guī)格之前,是因為measureChild()內(nèi)部調(diào)用了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的布局參數(shù)
        final LayoutParams lp = child.getLayoutParams();

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

        //調(diào)用childView的measure(),measure()方法內(nèi)就是回調(diào)`onMeasure()`方法
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

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


2.1.3 getChildMeasureSpec(),確定childview的測量規(guī)格 <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的測量大小減去內(nèi)邊距
        int size = Math.max(0, specSize - padding);

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

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

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

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

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

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


3. 布局方法 <p>

ViewGorup是個抽象類,繼承ViewGroup,肯定就有必須要實現(xiàn)的抽象方法,這個抽象方法就是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) {

    }
}

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


3.1 簡單實現(xiàn)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) {
            // 遍歷內(nèi)部的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都已經(jīng)繪制出來,但ButtonTextView給蓋住了。原因很明顯,在繪制第2個子控件Button時,依然從CustomView(0,0)點開始繪制,并沒有考慮TextView的高度


3.2 進行優(yōu)化修改 <p>

修改需要考慮的就是已經(jīng)繪制過的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;
        // 遍歷內(nèi)部的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下方

考慮已經(jīng)繪制過的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這個值走,經(jīng)過一系列的測量方法后,最終來到onMeasure()

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

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


使用場景:

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

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

可以看看Android開發(fā)之getMeasuredWidth和getWidth區(qū)別從源碼分析


4.考慮Padding,Margins <p>

有了上篇onMeasure()經(jīng)驗,知道PaddingMargins,也需要優(yōu)化處理的


4.1 Padding

xml文件中加入padding之后

Padding將內(nèi)容吃掉

修改代碼:

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;
       // 遍歷內(nèi)部的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()

簡單優(yōu)化Padding

這樣也只是做了最簡單的優(yōu)化,一旦Padding大到了一定程度,還是會吃掉內(nèi)部的childView


4.2 Margins <p>

CustomLayout內(nèi)加Margins有效,可內(nèi)部的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);
    }
}

并沒有做任何設(shè)置,還對更多屬性進行設(shè)置,以后再學(xué)習(xí)


完整代碼:

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內(nèi)邊距
            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的內(nèi)邊距
        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);
                // 累加已經(jīng)繪制的childView的高
                mHeight += child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
            }
        }
    }

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

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

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

    /**
     *  檢查當(dāng)前布局參數(shù)是否是我們定義的類型
     */
    @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和內(nèi)部控件的Margin都已經(jīng)支持,但真正以后實際開發(fā),要優(yōu)化考慮的要比這嚴謹。這里只是了解學(xué)習(xí)


5.最后 <p>

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

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

共勉 : )

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容