Android自定义ViewGroup的OnMeasure和onLayout详解
前一篇文章主要講了自定義View為什么要重載onMeasure()方法http://blog.csdn.net/tuke_tuke/article/details/73302595
那么,自定義ViewGroup又都有哪些方法需要重載或者實現呢 ?
Android開發中,對于自定義View,分為兩種,一種是自定義控件(繼承View類)。一種是自定義布局容器(繼承ViewGroup類)。如果是自定義控件,則一般要重寫兩個方法,一個是onMeasure(),用來測量尺寸,本質是該控件的父控件來測量該控件的尺寸,并通過widthMeasureSpec和heightMeasureSpec傳到onMeasure的兩個參數里供新控件參考,另一個是onDraw(),用來繪制控件的UI。
而自定義布局容器,則一般要重寫三個方法,一個是onMeasure,也是來測量尺寸,但是它有兩個任務一定要完成,就是“測量所有子控件的尺寸”和“設置自己的尺寸”(設置自己的尺寸過程其實和上篇自定View的中onMeasure過程相同,因為ViewGroup也是繼承view);一個是onLayout(),用來布局子控件;還有一個是dispatchDraw(),用來繪制UI。
onLayout(),用來布局子控件具體是什么意思呢?怎樣來實現布局子控件的呢?
本文先分析一個自定義ViewGroup的例子,然后在分析View,ViewGroup的源碼,LinearLayout,RelativeLayout的源碼是怎樣使用和實現onMeasure和onLayout的。
ViewGroup類的onLayout()函數是abstract類型,繼承者必須實現。由于ViewGroup的定位是一個容器,用來盛放子控件的,所以就必須要以什么方式來盛放,比如LinearLayout就是以橫向或者縱向順序存放,而RelativeLayout則以相對位置來擺放子控件,同樣,我們的自定義ViewGroup也必須給出我們期望的布局方式,而這個定義就通過onLayout()函數來實現。
我們通過實現一個水平優先布局的視圖容器來更加深入地了解onLayout()的實現吧,
1. 自定義ViewGroup的派生類
第一步,則是自定ViewGroup的派生類,繼承默認的構造函數。
/*** 作者:tuke on 2017/6/17 11:31* 郵箱:2297535832@qq.com*/ public class CustomViewGroup extends ViewGroup {public CustomViewGroup(Context context) {super(context);}public CustomViewGroup(Context context, AttributeSet attrs) {super(context, attrs);}public CustomViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);} 2. 重載onMeasure()方法
為什么要重載onMeasure()方法這里就不贅述了,上一篇文章已經講過,這里需要注意的是,自定義ViewGroup的onMeasure()方法中,除了計算自身的尺寸外,還需要調用measureChildren()函數來計算子控件的尺寸。
onMeasure()的定義不是本文的討論重點,因此這里我直接使用默認的onMeasure()定義,當然measureChildren()是必須得加的。
@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {//測量所有子控件的寬和高measureChildren(widthMeasureSpec,heightMeasureSpec);//調用系統的onMeasure一般是測量自己(當前ViewGroup)的寬和高super.onMeasure(widthMeasureSpec, heightMeasureSpec);}
? ? ? ? ? 在ViewGroup類中只有抽象的onLayout函數,OnMeasure函數是在view中,因為ViewGroup繼承了View所以這里的super.onMeasure就是系統默認的onMeasure(見上一篇文章)測量并設置此自定義ViewGroup的寬和高,因為layout文件中設置的是match_parent,所以就是全屏。當然如果對此自定義ViewGroup寬和高有要求,layout文件中設置的是wrap_content,就不使用super.onMeasure,而是根據所有子控件的寬高,計算出開發者需要的寬高,然后同樣使用setMeasureDemension設置此自定義ViewGroup的寬高。
? ? ?
? ? ? 小結一下:就是layout文件中如果是match_parent和固定值,就可以用系統的onMeasure,如果設置的是wrap_content,就需要重寫onMeasure,根據需要計算出寬和高,用setMeasureDemension設置進去。對自定義View和自定義ViewGroup都適用。
3. 實現onLayout()方法
由于我們希望優先橫向布局子控件,那么,首先,我們知道總寬度是多少,這個值可以通過getMeasuredWidth()來得到,當然子控件的寬度也可以通過子控件對象的getMeasuredWidth()來得到。
4. 布局文件測試
下面我們就嘗試寫一個簡單的xml文件,來測試一下我們的自定義ViewGroup,我們把子View的背景顏色都設置為黑色,方便我們辨識。
<?xml version="1.0" encoding="utf-8"?> <com.example.customviewgroup.CustomViewGroupxmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"android:background="#D6D6D6"><Viewandroid:layout_width="100dp"android:layout_height="100dp"android:layout_margin="10dp"android:background="#000000"/><Viewandroid:layout_width="100dp"android:layout_height="100dp"android:layout_margin="10dp"android:background="#000000"/><Viewandroid:layout_width="100dp"android:layout_height="100dp"android:layout_margin="10dp"android:background="#000000"/><Viewandroid:layout_width="100dp"android:layout_height="100dp"android:layout_margin="10dp"android:background="#000000"/></com.example.customviewgroup.CustomViewGroup> 結果
這時可能會疑惑,為什么View中的margin屬性沒有效果,所以子控件一個個緊挨著排列,中間沒有空隙。那么,下面我們來研究下為什么沒有效果?如何添加margin效果?
1,為什么沒有效果?
其實一個ViewGroup要支持子控件的layout_margin參數,則自定義的ViewGroup類必須重寫generateLayoutParams()函數,并且在該函數中返回一個ViewGroup.MarginLayoutParams派生類對象,也就是說還有定義一個靜態內部類繼承ViewGroup.MarginLayoutParams,這樣布局才能使用mergin參數
說白了,1,定義一個內部類繼承ViewGroup.MarginLayoutParams,2,重寫generateLayoutParams()函數
ViewGroup.MarginLayoutParams的定義關鍵部分如下,它記錄了子控件的layout_margin值:
你可以跟蹤源碼看看,其實XML文件中View的layout_xxx參數都是被傳遞到了各種自定義ViewGroup.LayoutParams派生類對象中,就是說自定義的ViewGroup或者LinearLayout等(都是ViewGroup的子類),要想使用期包含的子控件的layout_xxx參數,必須有一個靜態內部類繼承ViewGroup.LayoutParams,并在該內部類中進行擴展。
例如LinearLayout的LayoutParams定義的關鍵部分如下:
public static class LayoutParams extends ViewGroup.MarginLayoutParams {public float weight;public int gravity = -1;public LayoutParams(Context c, AttributeSet attrs) {super(c, attrs);TypedArray a = c.obtainStyledAttributes(attrs, com.android.internal.R.styleable.LinearLayout_Layout);weight = a.getFloat(com.android.internal.R.styleable.LinearLayout_Layout_layout_weight, 0);gravity = a.getInt(com.android.internal.R.styleable.LinearLayout_Layout_layout_gravity, -1);a.recycle();}}@Overridepublic LayoutParams generateLayoutParams(AttributeSet attrs) {return new LinearLayout.LayoutParams(getContext(), attrs);} } 這樣你大概就可以理解為什么LinearLayout的子控件支持weight和gravity的設置了吧,當然我們也可以這樣自定義一些屬于我們ViewGroup特有的params,這里就不詳細討論了,我們只繼承MarginLayoutParams來獲取子控件的margin值
//要使子控件的margin屬性有效,必須定義靜態內部類,繼承ViewGroup.MarginLayoutParamspublic static class LayoutParams extends ViewGroup.MarginLayoutParams{public LayoutParams(Context c, AttributeSet attrs) {super(c, attrs);}}//要使子控件的margin屬性有效,必須重寫該函數,返回內部類實例@Overridepublic ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {return new CustomViewGroup.LayoutParams(getContext(),attrs);}
這樣修改之后,我們就可以在onLayout()函數中獲取子控件的layout_margin值了,添加了layout_margin的onLayout()函數實現如下所示:
@Overrideprotected void onLayout(boolean changed, int l, int t, int r, int b) {int mViewGroupWidth=getMeasuredWidth(); //當前ViewGroup的總寬度int mPainterPosX=l;//當前繪制光標X坐標int mPainterPosY=t;//當前繪制光標Y坐標int childCount=getChildCount();//子控件的數量//遍歷所有子控件,并在其位置上繪制子控件for (int i=0;i<childCount;i++){View childView=getChildAt(i);//子控件的寬和高int width=childView.getMeasuredWidth();int height=childView.getMeasuredHeight();CustomViewGroup.LayoutParams params= (CustomViewGroup.LayoutParams) childView.getLayoutParams();//在onLayout中就可以獲取子控件的mergin值了//ChildView占用的width = width+leftMargin+rightMargin//ChildView占用的height = height+topMargin+bottomMargin//如果剩余控件不夠,則移到下一行開始位置if(mPainterPosX+width+params.leftMargin+params.rightMargin>mViewGroupWidth){mPainterPosX=l;mPainterPosY+=height+params.topMargin+params.bottomMargin;}//執行childView的繪制childView.layout(mPainterPosX+params.leftMargin,mPainterPosY+params.topMargin,mPainterPosX+width+params.leftMargin+params.rightMargin,mPainterPosY+height+params.topMargin+params.bottomMargin);//下一次繪制的X坐標mPainterPosX+=width+params.leftMargin+params.rightMargin;}}
整個代碼: package com.example.customviewgroup;import android.content.Context; import android.util.AttributeSet; import android.view.View; import android.view.ViewGroup;/*** 作者:tuke on 2017/6/17 11:31* 郵箱:2297535832@qq.com*/ public class CustomViewGroup extends ViewGroup {public CustomViewGroup(Context context) {super(context);}public CustomViewGroup(Context context, AttributeSet attrs) {super(context, attrs);}public CustomViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);}@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {//測量所有子控件的寬和高,只有先測量了所有子控件的尺寸,后面才能使用child.getMeasuredWidth()measureChildren(widthMeasureSpec,heightMeasureSpec);//調用系統的onMeasure一般是測量自己(當前ViewGroup)的寬和高super.onMeasure(widthMeasureSpec, heightMeasureSpec);}/*** @param changed 該參數支出當前ViewGroup的尺寸或者位置是否發生了改變* @param l,t,r,b 當前ViewGroup相對于父控件的坐標位置,注意 ,一定是相對于父控件。* 函數的參數l,t,r,b,也是由該VieGroup的父控件傳過來的*/@Overrideprotected void onLayout(boolean changed, int l, int t, int r, int b) {int mViewGroupWidth=getMeasuredWidth(); //當前ViewGroup的總寬度int mPainterPosX=l;//當前繪制光標X坐標int mPainterPosY=t;//當前繪制光標Y坐標int childCount=getChildCount();//子控件的數量//遍歷所有子控件,并在其位置上繪制子控件for (int i=0;i<childCount;i++){View childView=getChildAt(i);//子控件的寬和高int width=childView.getMeasuredWidth();int height=childView.getMeasuredHeight();CustomViewGroup.LayoutParams params= (CustomViewGroup.LayoutParams) childView.getLayoutParams();//在onLayout中就可以獲取子控件的mergin值了//ChildView占用的width = width+leftMargin+rightMargin//ChildView占用的height = height+topMargin+bottomMargin//如果剩余控件不夠,則移到下一行開始位置if(mPainterPosX+width+params.leftMargin+params.rightMargin>mViewGroupWidth){mPainterPosX=l;mPainterPosY+=height+params.topMargin+params.bottomMargin;}//執行childView的繪制childView.layout(mPainterPosX+params.leftMargin,mPainterPosY+params.topMargin,mPainterPosX+width+params.leftMargin+params.rightMargin,mPainterPosY+height+params.topMargin+params.bottomMargin);//下一次繪制的X坐標mPainterPosX+=width+params.leftMargin+params.rightMargin;}}//要使子控件的margin屬性有效,必須定義靜態內部類,繼承ViewGroup.MarginLayoutParamspublic static class LayoutParams extends ViewGroup.MarginLayoutParams{public LayoutParams(Context c, AttributeSet attrs) {super(c, attrs);}}//要使子控件的margin屬性有效,必須重寫該函數,返回內部類實例@Overridepublic ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {return new CustomViewGroup.LayoutParams(getContext(),attrs);} }
總結:說白了,onLayout函數主要是確定每個子控件的位置,并調用childview.layout()繪制。
二,ViewGroup的OnMeasure詳解 通過上面的介紹,我們知道,如果要自定義ViewGroup就必須重寫onMeasure方法,在這里完成兩個任務,1,測量所有子控件的尺寸,2,設置自己的尺寸。
子控件的尺寸怎么測量呢?ViewGroup中提供了三個關于測量子控件的方法:
1,measureChildren方法 /***遍歷ViewGroup中所有的子控件,調用measuireChild測量寬高*/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) {//測量某一個子控件寬高measureChild(child, widthMeasureSpec, heightMeasureSpec);}} }2,measureChild 方法 /** * 測量某一個child的寬高 */ protected void measureChild (View child, int parentWidthMeasureSpec,int parentHeightMeasureSpec) {final LayoutParams lp = child.getLayoutParams();//獲取子控件的寬高約束規則final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,mPaddingLeft + mPaddingRight, lp. width);final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,mPaddingTop + mPaddingBottom, lp. height);child.measure(childWidthMeasureSpec, childHeightMeasureSpec); }
3,measureChildWithMargins 方法 /** * 測量某一個child的寬高,考慮margin值 */ protected void measureChildWithMargins (View child,int parentWidthMeasureSpec, int widthUsed,int parentHeightMeasureSpec, int heightUsed) {final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();//獲取子控件的寬高約束規則final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,mPaddingLeft + mPaddingRight + lp. leftMargin + lp.rightMargin+ widthUsed, lp. width);final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,mPaddingTop + mPaddingBottom + lp. topMargin + lp.bottomMargin+ heightUsed, lp. height);//測量子控件child.measure(childWidthMeasureSpec, childHeightMeasureSpec); }三個方法分別做了那些工作大家應該比較清楚了吧?measureChildren 就是遍歷所有子控件挨個測量,最終測量子控件的方法就是measureChild 和measureChildWithMargins 了
三,例子 在上面“2,重寫onMeasure方法”小節中,直接使用的是super.onMeasure(見上一篇文章)測量并設置此自定ViewGroup的寬和高。如果我們將layout文件中此自定義ViewGroup的設置成wrap_content ,則需要重寫onMeasure,根據子控件的大小尺寸來設置此自定義ViewGroup的尺寸。
?在ViewGroup類中只有抽象的onLayout函數,OnMeasure函數是在view中,因為ViewGroup繼承了View所以這里的super.onMeasure就是系統默認的onMeasure(見上一篇文章)測量并設置此自定義ViewGroup的寬和高,因為layout文件中設置的是match_parent,所以就是全屏。當然如果對此自定義ViewGroup寬和高有要求,layout文件中設置的是wrap_content,就不使用super.onMeasure,而是根據所有子控件的寬高,計算出開發者需要的寬高,然后同樣使用setMeasureDemension設置此自定義ViewGroup的寬高。
? ? ?
? ??? 小結一下:就是layout文件中如果是match_parent和固定值,就可以用系統的onMeasure,如果設置的是wrap_content,就需要重寫onMeasure,根據需要計算出寬和高,用setMeasureDemension設置進去。對自定義View和自定義ViewGroup都適用。
下面的例子:待續。。。
總結
以上是生活随笔為你收集整理的Android自定义ViewGroup的OnMeasure和onLayout详解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Andoid自定义View的OnMeas
- 下一篇: Android踩坑日记:Android字