閱讀 3311收藏 1522016-4-24android
原文連接:http://blog.csdn.net/guiman/article/details/51225809app
做者叫你如何自定義一個 ViewGroup —— 由 hanks 分享ide
歡迎你們來學習本節內容,前幾節咱們已經學習了其餘幾種自定義控件,分別是自定義控件之對現有控件拓展(一)、自定義控件之直接繼承View建立全新視圖(二)及 自定義控件之建立能夠複用的組合控件(三)尚未學習的同窗請先去學習下,由於本節將使用到上幾節所講述的內容。佈局
在學習新內容以前,咱們先來弄清楚兩個問題:
1 . 什麼是ViewGroup?學習
ViewGroup是一種容器。它包含零個或以上的View及子View。
ui
2 . ViewGroup有什麼做用?this
ViewGroup內部能夠用來存放多個View控件,而且根據自身的測量模式,來測量View子控件,而且決定View子控件的位置。這在下面會逐步講解它是怎麼測量及決定子控件大小和位置的。spa
ok,弄清楚了這兩個問題,那麼下面咱們來學習下自定義ViewGroup吧。.net
首先和以前幾節同樣,先來繼承ViewGroup,並重寫它們的構造方法。3d
public class CustomViewGroup extends ViewGroup{ public CustomViewGroup(Context context) { this(context,null); } public CustomViewGroup(Context context, AttributeSet attrs) { this(context, attrs,0); } public CustomViewGroup(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } }
在上面兩個問題,咱們知道,ViewGroup它是一個容器,它是用來存放和管理子控件的,而且子控件的測量方式是根據它的測量模式來進行的,因此咱們必須重寫它的onMeasure(),在該方法中進行對子View的大小進行測量,代碼以下:
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int childCount = getChildCount(); for(int i = 0 ; i < childCount ; i ++){ View children = getChildAt(i); measureChild(children,widthMeasureSpec,heightMeasureSpec); } }
其上代碼,咱們重寫了onMeasure(),在方法裏面,咱們首先先獲取ViewGroup中的子View的個數,而後遍歷它全部的子View,獲得每個子View,調用measureChild()放來,來對子View進行測量。剛纔提到子View的測量是根據ViewGroup所提供的測量模式來進行來,因此在measureChild()方法中,把ViewGroup的widthMeasureSpec 和 heightMeasureSpec和子View一塊兒傳進去了,咱們能夠跟進去看看是否是和咱們所說的同樣。
measureChild()方法源碼:
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); }
measureChild()源碼方法裏面很好理解,它首先獲得子View的LayoutParams,而後根據ViewGroup傳遞進來的寬高屬性值和自身的LayoutParams 的寬高屬性值及自身padding屬性值分別調用getChildMeasureSpec()方法獲取到子View的測量。由該方法咱們也知道ViewGroup中在測量子View的大小時,測量結果分別是由父節點的測量模式和子View自己的LayoutParams及padding所決定的。
下面咱們再來看看getChildMeasureSpec()方法的源碼,看看它是怎麼獲取測量結果的。
getChildMeasureSpec()方法源碼:
public static int getChildMeasureSpec(int spec, int padding, int childDimension) { int specMode = MeasureSpec.getMode(spec); int specSize = MeasureSpec.getSize(spec); int size = Math.max(0, specSize - padding); int resultSize = 0; int resultMode = 0; switch (specMode) { case MeasureSpec.EXACTLY: if (childDimension >= 0) { resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { resultSize = size; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.WRAP_CONTENT) { resultSize = size; resultMode = MeasureSpec.AT_MOST; } break; case MeasureSpec.AT_MOST: 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: if (childDimension >= 0) { resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { 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); }
該方法也很好理解:首先是獲取父節點(這裏是ViewGroup)的測量模式和測量的大小,並根據測量的大小值與子View自身的padding屬性值相比較取最大值獲得一個size的值。
而後根據父節點的測量模式分別再來斷定子View的LayoutParams屬性值,根據LayoutParams的屬性值從而獲取到子View測量的大小和模式,知道了ziView的測量模式和大小就能決定子View的大小了。
ok,子View的測量咱們已經徹底明白了,那麼接下來,咱們再來分析一下ViewGroup是怎樣給子View定位的,首先咱們也是必須先重寫onLayout()方法,代碼以下:
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int childCount = getChildCount(); int preHeight = 0; for(int i = 0 ; i < childCount ; i ++){ View children = getChildAt(i); int cHeight = children.getMeasuredHeight(); if(children.getVisibility() != View.GONE){ children.layout(l, preHeight, r,preHeight += cHeight); } } }
很好理解,給子View定位,首先必須知道有多少個子View才行,因此咱們先獲得子View的數量,而後遍歷獲取每一個子View。其實在定位子View的layout()方法中,系統並無給出具體的定位方法,而是給了咱們最大的限度來本身定義,下面來看下layout源碼:
public void layout(int l, int t, int r, int b) { if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) { onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec); mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT; } int oldL = mLeft; int oldT = mTop; int oldB = mBottom; int oldR = mRight; boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b); if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) { onLayout(changed, l, t, r, b); mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED; ListenerInfo li = mListenerInfo; if (li != null && li.mOnLayoutChangeListeners != null) { ArrayList<OnLayoutChangeListener> listenersCopy = (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone(); int numListeners = listenersCopy.size(); for (int i = 0; i < numListeners; ++i) { listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB); } } } mPrivateFlags &= ~PFLAG_FORCE_LAYOUT; mPrivateFlags3 |= PFLAG3_IS_LAID_OUT; }
在上面一段代碼中,最關鍵個就是setFrame(l, t, r, b);這個方法,它主要是來定位子View的四個頂點左右座標的,而後關鍵的定位方法是在onLayout(changed, l, t, r, b);這個方法中,跟進去看看
protected void onLayout(boolean changed, int left, int top, int right, int bottom) { }
一看嚇一跳,空的,哈哈,這也就是我上面說的,系統給了咱們最大的自由,讓咱們本身根據需求去定義了。
而我這裏是根據子View的高度讓它們豎直順序的排列下來。
View children = getChildAt(i); int cHeight = children.getMeasuredHeight(); if(children.getVisibility() != View.GONE){ children.layout(l, preHeight, r,preHeight += cHeight);
定義一個記錄上一個View的高度的變量,每次遍歷之後都讓它加上當前的View高度,由此就能夠豎直依次地排列了每一個子View,從而實現了子View的定義。
好了,講了這麼多,如今來看看效果吧,咱們就拿以前作的自定義View做爲它的子View吧:
custom_viewgroup.xml文件:
<?xml version="1.0" encoding="utf-8"?> <com.sanhuimusic.mycustomview.view.CustomViewGroup android:background="#999999" xmlns:android="http://schemas.android.com/apk/res/android" xmlns:custom="http://schemas.android.com/apk/res-auto" android:id="@+id/customViewGroup" android:layout_width="match_parent" android:layout_height="match_parent"> <com.sanhuimusic.mycustomview.view.CompositeViews android:background="#999999" android:id="@+id/topBar" android:layout_width="wrap_content" android:layout_height="wrap_content" custom:titleText="@string/titleText" custom:titleColor="#000000" custom:titleTextSize="@dimen/titleTextSize" custom:titleBackground="#999999" custom:leftText="@string/leftText" custom:leftTextColor="#FFFFFF" custom:leftBackground="#666666" custom:leftTextSize="@dimen/leftTextSize" custom:rightText="@string/rightText" custom:rightTextColor="#FFFFFF" custom:rightBackground="#666666" custom:rightTextSize="@dimen/rightTextSize" /> <com.sanhuimusic.mycustomview.view.AudioBar android:layout_width="match_parent" android:layout_height="wrap_content" /> </com.sanhuimusic.mycustomview.view.CustomViewGroup>
MainActivity:
public class MainActivity extends AppCompatActivity { private CompositeViews topBar; private Context mContext; private CustomViewGroup mViewGroupContainer; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.custom_viewgroup); mContext = this; init(); } private void init() { mViewGroupContainer = (CustomViewGroup) findViewById(R.id.customViewGroup); topBar = (CompositeViews)findViewById(R.id.topBar); topBar.setOnTopBarClickListener(new CompositeViews.TopBarClickListener(){ @Override public void leftClickListener() { ToastUtil.makeText(MainActivity.this,"您點擊了返回鍵",Toast.LENGTH_SHORT).show(); } @Override public void rightClickListener() { ToastUtil.makeText(MainActivity.this,"您點擊了搜索鍵",Toast.LENGTH_SHORT).show(); } }); } }
效果圖:
哈哈,是否是每一個子View都按照咱們所說的豎直依次排列下來了呢。正開心呢,而後忽然冒出來一個想法,學習過自定義控件之直接繼承View建立全新視圖(二)這篇文章的你,會記得當時在定義全新的View時會遇到當咱們的佈局文件使用的是wrap_content時,View是不直接支持的,須要咱們特殊的處理才能正確支持,而咱們如今的 ViewGroup是否是也是這樣的呢,趕快嘗試一下。一嘗試,壞了,果真不支持wrap_content。
因此,在自定義ViewGroup時,咱們必需要注意如下幾個問題:
1. 必須讓ViewGroup支持wrap_content的情景下的佈局。
2. 也須要支持自己的padding屬性。
好,下面咱們來一點一點的完善它。
1 . 咱們讓它先支持wrap_content。
這須要咱們在onMeasure()方法中多出一些必要的改動。讓它支持自身wrap_content那就須要咱們對它驚醒測量,根據測量方式獲取到測量大小,而後再調用setMeasuredDimension()決定顯示大小。
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int childCount = getChildCount(); for(int i = 0 ; i < childCount ; i ++){ View children = getChildAt(i); measureChild(children,widthMeasureSpec,heightMeasureSpec); } /** * 讓它支持自身wrap_content */ int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); int mWidth = 0; int mHeight = 0; int mMaxWidth = 0; if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){ for(int i = 0 ; i < childCount ; i ++){ View children = getChildAt(i); mWidth += children.getMeasuredWidth(); mHeight += children.getMeasuredHeight(); } setMeasuredDimension(mWidth, mHeight); } else if(widthSpecMode == MeasureSpec.AT_MOST){ for(int i = 0 ; i < childCount ; i ++){ View children = getChildAt(i); mMaxWidth = Math.max(mMaxWidth,children.getMeasuredWidth()); } setMeasuredDimension(mMaxWidth,heightSpecSize); } else if(heightSpecMode == MeasureSpec.AT_MOST){ for(int i = 0 ; i < childCount ; i ++){ View children = getChildAt(i); mHeight += children.getMeasuredHeight(); } setMeasuredDimension(widthSpecSize,mHeight); } }
咱們再原來的基礎上添加了能夠支持wrap_content的代碼,而後根據具體的狀況進行獲取大小。分爲三種狀況:
1. 當寬高屬性都爲wrap_content時,分別獲取到子View的寬高並相加取得總寬高,在調用setMeasuredDimension(mWidth, mHeight)直接設置便可;
2. 當寬屬性都爲wrap_content時,分別獲取到子View的寬並獲取其中最大值,在調用setMeasuredDimension(mMaxWidth,heightSpecSize)直接設置便可;
3. 當高屬性都爲wrap_content時,分別獲取到子View的高並相加取得總高,在調用setMeasuredDimension(widthSpecSize,mHeight)直接設置便可。
好,來看看是否能夠達到咱們的要求。
顯然已達到目標。
2 . 須要支持自己的padding屬性。
首先咱們先獲取到padding值,以下:
leftPadding = getPaddingLeft(); topPadding = getPaddingTop(); rightPadding = getPaddingRight(); bottomPadding = getPaddingBottom();
而後分別在設置大小的地方給加上這些屬性值,以下:
if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){ for(int i = 0 ; i < childCount ; i ++){ View children = getChildAt(i); mWidth += children.getMeasuredWidth(); mHeight += children.getMeasuredHeight(); } setMeasuredDimension(mWidth + leftPadding + rightPadding, mHeight + topPadding + bottomPadding); } else if(widthSpecMode == MeasureSpec.AT_MOST){ for(int i = 0 ; i < childCount ; i ++){ View children = getChildAt(i); mMaxWidth = Math.max(mMaxWidth,children.getMeasuredWidth()); } setMeasuredDimension(mMaxWidth + leftPadding + rightPadding, heightSpecSize + topPadding + bottomPadding); } else if(heightSpecMode == MeasureSpec.AT_MOST){ for(int i = 0 ; i < childCount ; i ++){ View children = getChildAt(i); mHeight += children.getMeasuredHeight(); } setMeasuredDimension(widthSpecSize + leftPadding + rightPadding, mHeight + topPadding + bottomPadding); }
最後在onlayout()方法中給添加屬性值:
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int childCount = getChildCount(); int preHeight = topPadding; for(int i = 0 ; i < childCount ; i ++){ View children = getChildAt(i); int cHeight = children.getMeasuredHeight(); if(children.getVisibility() != View.GONE){ children.layout(l + leftPadding, preHeight, r + rightPadding, preHeight += cHeight); } } }
代碼很簡單,再也不讓preHeight = 0 了,而是直接設置爲topPadding,最後在layout中也把屬性值添加進來,看看結果。
其實除了以上兩個問題須要注意的,還有其餘也是須要關注的,好比說是支持子View的margin屬性等,大體和解決padding屬性同樣的思路,你們能夠嘗試實現下。
好了,整個自定義ViewGroup的內容都講完了,固然咱們只是講述了UI的顯示,並無談及到功能的添加和實現。從上面能夠看出,自定義ViewGroup要比自定義View複雜不少,可是隻要一步一步的來完善仍是能夠實現不一樣的UI展現的。
從這幾節自定義控件學習中,你們必定學到了不少知識,而後對自定義控件也不是那麼怕了,同時也能夠實現本身想要的各類UI啦,接下來我會總結下自定義控件中所須要使用的其餘技術和知識下,讓你們更好的加深印象。
好,今天就學習到這裏吧,happy!