Android自定義控件之繼承ViewGroup創(chuàng)建新容器
歡迎大家來(lái)學(xué)習(xí)本節(jié)內(nèi)容,前幾節(jié)我們已經(jīng)學(xué)習(xí)了其他幾種自定義控件,分別是Andriod 自定義控件之音頻條及 Andriod 自定義控件之創(chuàng)建可以復(fù)用的組合控件還沒(méi)有學(xué)習(xí)的同學(xué)請(qǐng)先去學(xué)習(xí)下,因?yàn)楸竟?jié)將使用到上幾節(jié)所講述的內(nèi)容。
在學(xué)習(xí)新內(nèi)容之前,我們先來(lái)弄清楚兩個(gè)問(wèn)題:
1 . 什么是ViewGroup?
ViewGroup是一種容器。它包含零個(gè)或以上的View及子View。
2 . ViewGroup有什么作用?
ViewGroup內(nèi)部可以用來(lái)存放多個(gè)View控件,并且根據(jù)自身的測(cè)量模式,來(lái)測(cè)量View子控件,并且決定View子控件的位置。這在下面會(huì)逐步講解它是怎么測(cè)量及決定子控件大小和位置的。
ok,弄清楚了這兩個(gè)問(wèn)題,那么下面我們來(lái)學(xué)習(xí)下自定義ViewGroup吧。
首先和之前幾節(jié)一樣,先來(lái)繼承ViewGroup,并重寫(xiě)它們的構(gòu)造方法。
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); } }
在上面兩個(gè)問(wèn)題,我們知道,ViewGroup它是一個(gè)容器,它是用來(lái)存放和管理子控件的,并且子控件的測(cè)量方式是根據(jù)它的測(cè)量模式來(lái)進(jìn)行的,所以我們必須重寫(xiě)它的onMeasure(),在該方法中進(jìn)行對(duì)子View的大小進(jìn)行測(cè)量,代碼如下:
@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); } }
其上代碼,我們重寫(xiě)了onMeasure(),在方法里面,我們首先先獲取ViewGroup中的子View的個(gè)數(shù),然后遍歷它所有的子View,得到每一個(gè)子View,調(diào)用measureChild()放來(lái),來(lái)對(duì)子View進(jìn)行測(cè)量。剛才提到子View的測(cè)量是根據(jù)ViewGroup所提供的測(cè)量模式來(lái)進(jìn)行來(lái),所以在measureChild()方法中,把ViewGroup的widthMeasureSpec 和 heightMeasureSpec和子View一起傳進(jìn)去了,我們可以跟進(jìn)去看看是不是和我們所說(shuō)的一樣。
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,然后根據(jù)ViewGroup傳遞進(jìn)來(lái)的寬高屬性值和自身的LayoutParams 的寬高屬性值及自身padding屬性值分別調(diào)用getChildMeasureSpec()方法獲取到子View的測(cè)量。由該方法我們也知道ViewGroup中在測(cè)量子View的大小時(shí),測(cè)量結(jié)果分別是由父節(jié)點(diǎn)的測(cè)量模式和子View本身的LayoutParams及padding所決定的。
下面我們?cè)賮?lái)看看getChildMeasureSpec()方法的源碼,看看它是怎么獲取測(cè)量結(jié)果的。
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) { // Parent has imposed an exact size on us case MeasureSpec.EXACTLY: if (childDimension >= 0) { resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // Child wants to be our size. So be it. resultSize = size; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // Child wants to determine its own size. It can't be // bigger than us. resultSize = size; resultMode = MeasureSpec.AT_MOST; } break; // Parent has imposed a maximum size on us case MeasureSpec.AT_MOST: if (childDimension >= 0) { // Child wants a specific size... so be it resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // Child wants to be our size, but our size is not fixed. // Constrain child to not be bigger than us. resultSize = size; resultMode = MeasureSpec.AT_MOST; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // Child wants to determine its own size. It can't be // bigger than us. resultSize = size; resultMode = MeasureSpec.AT_MOST; } break; // Parent asked to see how big we want to be case MeasureSpec.UNSPECIFIED: if (childDimension >= 0) { // Child wants a specific size... let him have it resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { // Child wants to be our size... find out how big it should // be resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = MeasureSpec.UNSPECIFIED; } else if (childDimension == LayoutParams.WRAP_CONTENT) { // Child wants to determine its own size.... find out how // big it should be resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = MeasureSpec.UNSPECIFIED; } break; } return MeasureSpec.makeMeasureSpec(resultSize, resultMode); }
該方法也很好理解:首先是獲取父節(jié)點(diǎn)(這里是ViewGroup)的測(cè)量模式和測(cè)量的大小,并根據(jù)測(cè)量的大小值與子View自身的padding屬性值相比較取最大值得到一個(gè)size的值。
然后根據(jù)父節(jié)點(diǎn)的測(cè)量模式分別再來(lái)判定子View的LayoutParams屬性值,根據(jù)LayoutParams的屬性值從而獲取到子View測(cè)量的大小和模式,知道了ziView的測(cè)量模式和大小就能決定子View的大小了。
ok,子View的測(cè)量我們已經(jīng)完全明白了,那么接下來(lái),我們?cè)賮?lái)分析一下ViewGroup是怎樣給子View定位的,首先我們也是必須先重寫(xiě)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定位,首先必須知道有多少個(gè)子View才行,所以我們先得到子View的數(shù)量,然后遍歷獲取每個(gè)子View。其實(shí)在定位子View的layout()方法中,系統(tǒng)并沒(méi)有給出具體的定位方法,而是給了我們最大的限度來(lái)自己定義,下面來(lái)看下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; }
在上面一段代碼中,最關(guān)鍵個(gè)就是setFrame(l, t, r, b);這個(gè)方法,它主要是來(lái)定位子View的四個(gè)頂點(diǎn)左右坐標(biāo)的,然后關(guān)鍵的定位方法是在onLayout(changed, l, t, r, b);這個(gè)方法中,跟進(jìn)去看看
protected void onLayout(boolean changed, int left, int top, int right, int bottom) { }
一看嚇一跳,空的,哈哈,這也就是我上面說(shuō)的,系統(tǒng)給了我們最大的自由,讓我們自己根據(jù)需求去定義了。
而我這里是根據(jù)子View的高度讓它們豎直順序的排列下來(lái)。
View children = getChildAt(i); int cHeight = children.getMeasuredHeight(); if(children.getVisibility() != View.GONE){ children.layout(l, preHeight, r,preHeight += cHeight);
定義一個(gè)記錄上一個(gè)View的高度的變量,每次遍歷以后都讓它加上當(dāng)前的View高度,由此就可以豎直依次地排列了每個(gè)子View,從而實(shí)現(xiàn)了子View的定義。
好了,講了這么多,現(xiàn)在來(lái)看看效果吧,我們就拿之前做的自定義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,"您點(diǎn)擊了返回鍵",Toast.LENGTH_SHORT).show(); } @Override public void rightClickListener() { ToastUtil.makeText(MainActivity.this,"您點(diǎn)擊了搜索鍵",Toast.LENGTH_SHORT).show(); } }); } }
效果圖:
哈哈,是不是每個(gè)子View都按照我們所說(shuō)的豎直依次排列下來(lái)了呢。正開(kāi)心呢,然后突然冒出來(lái)一個(gè)想法,學(xué)習(xí)過(guò)Andriod 自定義控件之音頻條這篇文章的你,會(huì)記得當(dāng)時(shí)在定義全新的View時(shí)會(huì)遇到當(dāng)我們的布局文件使用的是wrap_content時(shí),View是不直接支持的,需要我們特殊的處理才能正確支持,而我們現(xiàn)在的 ViewGroup是不是也是這樣的呢,趕快嘗試一下。一嘗試,壞了,果然不支持wrap_content。
所以,在自定義ViewGroup時(shí),我們必須要注意以下幾個(gè)問(wèn)題:
1. 必須讓ViewGroup支持wrap_content的情景下的布局。
2. 也需要支持本身的padding屬性。
好,下面我們來(lái)一點(diǎn)一點(diǎn)的完善它。
1 . 我們讓它先支持wrap_content。
這需要我們?cè)趏nMeasure()方法中多出一些必要的改動(dòng)。讓它支持自身wrap_content那就需要我們對(duì)它驚醒測(cè)量,根據(jù)測(cè)量方式獲取到測(cè)量大小,然后再調(diào)用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); } }
我們?cè)僭瓉?lái)的基礎(chǔ)上添加了可以支持wrap_content的代碼,然后根據(jù)具體的情況進(jìn)行獲取大小。分為三種情況:
- 當(dāng)寬高屬性都為wrap_content時(shí),分別獲取到子View的寬高并相加取得總寬高,在調(diào)用setMeasuredDimension(mWidth, mHeight)直接設(shè)置即可;
- 當(dāng)寬屬性都為wrap_content時(shí),分別獲取到子View的寬并獲取其中最大值,在調(diào)用setMeasuredDimension(mMaxWidth,heightSpecSize)直接設(shè)置即可;
- 當(dāng)高屬性都為wrap_content時(shí),分別獲取到子View的高并相加取得總高,在調(diào)用setMeasuredDimension(widthSpecSize,mHeight)直接設(shè)置即可。
好,來(lái)看看是否可以達(dá)到我們的要求。
顯然已達(dá)到目標(biāo)。
2 . 需要支持本身的padding屬性。
首先我們先獲取到padding值,如下:
leftPadding = getPaddingLeft(); topPadding = getPaddingTop(); rightPadding = getPaddingRight(); bottomPadding = getPaddingBottom();
然后分別在設(shè)置大小的地方給加上這些屬性值,如下:
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); } } }
代碼很簡(jiǎn)單,不再讓preHeight = 0 了,而是直接設(shè)置為topPadding,最后在layout中也把屬性值添加進(jìn)來(lái),看看結(jié)果。
其實(shí)除了以上兩個(gè)問(wèn)題需要注意的,還有其他也是需要關(guān)注的,比如說(shuō)是支持子View的margin屬性等,大致和解決padding屬性一樣的思路,大家可以嘗試實(shí)現(xiàn)下。
好了,整個(gè)自定義ViewGroup的內(nèi)容都講完了,當(dāng)然我們只是講述了UI的顯示,并沒(méi)有談及到功能的添加和實(shí)現(xiàn)。從上面可以看出,自定義ViewGroup要比自定義View復(fù)雜很多,但是只要一步一步的來(lái)完善還是可以實(shí)現(xiàn)不同的UI展示的。
從這幾節(jié)自定義控件學(xué)習(xí)中,大家一定學(xué)到了很多知識(shí),然后對(duì)自定義控件也不是那么怕了,同時(shí)也可以實(shí)現(xiàn)自己想要的各種UI啦,接下來(lái)我會(huì)總結(jié)下自定義控件中所需要使用的其他技術(shù)和知識(shí)下,讓大家更好的加深印象。
好,今天就學(xué)習(xí)到這里吧,happy!
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- 從源碼解析Android中View的容器ViewGroup
- Android中標(biāo)簽容器控件的實(shí)例詳解
- Android應(yīng)用開(kāi)發(fā)中自定義ViewGroup視圖容器的教程
- Android自定義ViewGroup實(shí)現(xiàn)標(biāo)簽流容器FlowLayout
- Android中實(shí)現(xiàn)多行、水平滾動(dòng)的分頁(yè)的Gridview實(shí)例源碼
- android listview 水平滾動(dòng)和垂直滾動(dòng)的小例子
- Android使用RecyclerView實(shí)現(xiàn)水平滾動(dòng)控件
- Android實(shí)現(xiàn)Activity水平和垂直滾動(dòng)條的方法
- 詳解Android使GridView橫向水平滾動(dòng)的實(shí)現(xiàn)方式
- Android使用Recyclerview實(shí)現(xiàn)圖片水平自動(dòng)循環(huán)滾動(dòng)效果
- Android開(kāi)發(fā)實(shí)現(xiàn)自定義水平滾動(dòng)的容器示例
相關(guān)文章
Android實(shí)現(xiàn)雙層ViewPager嵌套
這篇文章主要介紹了Android實(shí)現(xiàn)雙層ViewPager嵌套,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-04-04Android中Handler、Thread、HandlerThread三者的區(qū)別
本文主要介紹了Android中Handler、Thread、HandlerThread三者的區(qū)別,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-10-10Android ItemDecoration 實(shí)現(xiàn)分組索引列表的示例代碼
本篇文章主要介紹了Android ItemDecoration 實(shí)現(xiàn)分組索引列表的示例代碼,具有一定的參考價(jià)值,有興趣的可以了解一下2017-10-10Android TV開(kāi)發(fā):使用RecycleView實(shí)現(xiàn)橫向的Listview并響應(yīng)點(diǎn)擊事件的代碼
這篇文章主要介紹了Android TV開(kāi)發(fā):使用RecycleView實(shí)現(xiàn)橫向的Listview并響應(yīng)點(diǎn)擊事件的代碼,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,一起跟隨小編過(guò)來(lái)看看吧2018-05-05使用adb命令向Android模擬器中導(dǎo)入通訊錄聯(lián)系人的方法
這篇文章主要介紹了使用adb命令向Android模擬器中導(dǎo)入通訊錄聯(lián)系人的方法,實(shí)例分析了導(dǎo)入通訊錄存儲(chǔ)文件的技巧,需要的朋友可以參考下2015-01-01Android無(wú)障礙監(jiān)聽(tīng)通知的實(shí)戰(zhàn)過(guò)程
開(kāi)發(fā)微動(dòng)手勢(shì)的時(shí)候,做了一個(gè)通知觸發(fā)的功能,就是在收到某個(gè)預(yù)設(shè)的通知的時(shí)候,自動(dòng)觸發(fā)某個(gè)動(dòng)作,因此需要監(jiān)聽(tīng)通知消息,這篇文章主要給大家介紹了關(guān)于Android無(wú)障礙監(jiān)聽(tīng)通知的相關(guān)資料,需要的朋友可以參考下2022-07-07Android實(shí)現(xiàn)底部狀態(tài)欄切換的兩種方式
這篇文章主要介紹了Android實(shí)現(xiàn)底部狀態(tài)欄切換功能,在文中給大家提到了兩種實(shí)現(xiàn)方式,本文分步驟給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-06-06Android DrawerLayout帶有側(cè)滑功能的布局類(lèi)(2)
這篇文章主要為大家詳細(xì)介紹了Android DrawerLayout帶有側(cè)滑功能的布局類(lèi),感興趣的小伙伴們可以參考一下2016-07-07