Android自定義控件之繼承ViewGroup創(chuàng)建新容器
歡迎大家來學習本節(jié)內(nèi)容,前幾節(jié)我們已經(jīng)學習了其他幾種自定義控件,分別是Andriod 自定義控件之音頻條及 Andriod 自定義控件之創(chuàng)建可以復(fù)用的組合控件還沒有學習的同學請先去學習下,因為本節(jié)將使用到上幾節(jié)所講述的內(nèi)容。
在學習新內(nèi)容之前,我們先來弄清楚兩個問題:
1 . 什么是ViewGroup?
ViewGroup是一種容器。它包含零個或以上的View及子View。

2 . ViewGroup有什么作用?
ViewGroup內(nèi)部可以用來存放多個View控件,并且根據(jù)自身的測量模式,來測量View子控件,并且決定View子控件的位置。這在下面會逐步講解它是怎么測量及決定子控件大小和位置的。
ok,弄清楚了這兩個問題,那么下面我們來學習下自定義ViewGroup吧。
首先和之前幾節(jié)一樣,先來繼承ViewGroup,并重寫它們的構(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);
}
}
在上面兩個問題,我們知道,ViewGroup它是一個容器,它是用來存放和管理子控件的,并且子控件的測量方式是根據(jù)它的測量模式來進行的,所以我們必須重寫它的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的個數(shù),然后遍歷它所有的子View,得到每一個子View,調(diào)用measureChild()放來,來對子View進行測量。剛才提到子View的測量是根據(jù)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,然后根據(jù)ViewGroup傳遞進來的寬高屬性值和自身的LayoutParams 的寬高屬性值及自身padding屬性值分別調(diào)用getChildMeasureSpec()方法獲取到子View的測量。由該方法我們也知道ViewGroup中在測量子View的大小時,測量結(jié)果分別是由父節(jié)點的測量模式和子View本身的LayoutParams及padding所決定的。
下面我們再來看看getChildMeasureSpec()方法的源碼,看看它是怎么獲取測量結(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é)點(這里是ViewGroup)的測量模式和測量的大小,并根據(jù)測量的大小值與子View自身的padding屬性值相比較取最大值得到一個size的值。
然后根據(jù)父節(jié)點的測量模式分別再來判定子View的LayoutParams屬性值,根據(jù)LayoutParams的屬性值從而獲取到子View測量的大小和模式,知道了ziView的測量模式和大小就能決定子View的大小了。
ok,子View的測量我們已經(jīng)完全明白了,那么接下來,我們再來分析一下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的數(shù)量,然后遍歷獲取每個子View。其實在定位子View的layout()方法中,系統(tǒng)并沒有給出具體的定位方法,而是給了我們最大的限度來自己定義,下面來看下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)鍵個就是setFrame(l, t, r, b);這個方法,它主要是來定位子View的四個頂點左右坐標的,然后關(guān)鍵的定位方法是在onLayout(changed, l, t, r, b);這個方法中,跟進去看看
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
一看嚇一跳,空的,哈哈,這也就是我上面說的,系統(tǒng)給了我們最大的自由,讓我們自己根據(jù)需求去定義了。
而我這里是根據(jù)子View的高度讓它們豎直順序的排列下來。
View children = getChildAt(i);
int cHeight = children.getMeasuredHeight();
if(children.getVisibility() != View.GONE){
children.layout(l, preHeight, r,preHeight += cHeight);
定義一個記錄上一個View的高度的變量,每次遍歷以后都讓它加上當前的View高度,由此就可以豎直依次地排列了每個子View,從而實現(xiàn)了子View的定義。
好了,講了這么多,現(xiàn)在來看看效果吧,我們就拿之前做的自定義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都按照我們所說的豎直依次排列下來了呢。正開心呢,然后突然冒出來一個想法,學習過Andriod 自定義控件之音頻條這篇文章的你,會記得當時在定義全新的View時會遇到當我們的布局文件使用的是wrap_content時,View是不直接支持的,需要我們特殊的處理才能正確支持,而我們現(xiàn)在的 ViewGroup是不是也是這樣的呢,趕快嘗試一下。一嘗試,壞了,果然不支持wrap_content。
所以,在自定義ViewGroup時,我們必須要注意以下幾個問題:
1. 必須讓ViewGroup支持wrap_content的情景下的布局。
2. 也需要支持本身的padding屬性。
好,下面我們來一點一點的完善它。
1 . 我們讓它先支持wrap_content。
這需要我們在onMeasure()方法中多出一些必要的改動。讓它支持自身wrap_content那就需要我們對它驚醒測量,根據(jù)測量方式獲取到測量大小,然后再調(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);
}
}
我們再原來的基礎(chǔ)上添加了可以支持wrap_content的代碼,然后根據(jù)具體的情況進行獲取大小。分為三種情況:
- 當寬高屬性都為wrap_content時,分別獲取到子View的寬高并相加取得總寬高,在調(diào)用setMeasuredDimension(mWidth, mHeight)直接設(shè)置即可;
- 當寬屬性都為wrap_content時,分別獲取到子View的寬并獲取其中最大值,在調(diào)用setMeasuredDimension(mMaxWidth,heightSpecSize)直接設(shè)置即可;
- 當高屬性都為wrap_content時,分別獲取到子View的高并相加取得總高,在調(diào)用setMeasuredDimension(widthSpecSize,mHeight)直接設(shè)置即可。
好,來看看是否可以達到我們的要求。

顯然已達到目標。
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);
}
}
}
代碼很簡單,不再讓preHeight = 0 了,而是直接設(shè)置為topPadding,最后在layout中也把屬性值添加進來,看看結(jié)果。

其實除了以上兩個問題需要注意的,還有其他也是需要關(guān)注的,比如說是支持子View的margin屬性等,大致和解決padding屬性一樣的思路,大家可以嘗試實現(xiàn)下。
好了,整個自定義ViewGroup的內(nèi)容都講完了,當然我們只是講述了UI的顯示,并沒有談及到功能的添加和實現(xiàn)。從上面可以看出,自定義ViewGroup要比自定義View復(fù)雜很多,但是只要一步一步的來完善還是可以實現(xiàn)不同的UI展示的。
從這幾節(jié)自定義控件學習中,大家一定學到了很多知識,然后對自定義控件也不是那么怕了,同時也可以實現(xiàn)自己想要的各種UI啦,接下來我會總結(jié)下自定義控件中所需要使用的其他技術(shù)和知識下,讓大家更好的加深印象。
好,今天就學習到這里吧,happy!
以上就是本文的全部內(nèi)容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。
- 從源碼解析Android中View的容器ViewGroup
- Android中標簽容器控件的實例詳解
- Android應(yīng)用開發(fā)中自定義ViewGroup視圖容器的教程
- Android自定義ViewGroup實現(xiàn)標簽流容器FlowLayout
- Android中實現(xiàn)多行、水平滾動的分頁的Gridview實例源碼
- android listview 水平滾動和垂直滾動的小例子
- Android使用RecyclerView實現(xiàn)水平滾動控件
- Android實現(xiàn)Activity水平和垂直滾動條的方法
- 詳解Android使GridView橫向水平滾動的實現(xiàn)方式
- Android使用Recyclerview實現(xiàn)圖片水平自動循環(huán)滾動效果
- Android開發(fā)實現(xiàn)自定義水平滾動的容器示例
相關(guān)文章
Android 實現(xiàn)無網(wǎng)絡(luò)傳輸文件的示例代碼
本篇文章主要介紹了Android 實現(xiàn)無網(wǎng)絡(luò)傳輸文件的示例代碼,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-02-02
Android實現(xiàn)Gesture手勢識別用法分析
這篇文章主要介紹了Android實現(xiàn)Gesture手勢識別用法,結(jié)合實例形式較為詳細的分析了Android基于Gesture實現(xiàn)手勢識別的原理與具體實現(xiàn)技巧,需要的朋友可以參考下2016-09-09
Android入門之實現(xiàn)手工發(fā)送一個BroadCast
這篇文章主要通過手工來發(fā)送一條BroadCast進一步來帶大家深入了解BroadCast,文中的示例代碼講解詳細,對我們學習Android有一定幫助,感興趣的可以收藏一下2022-12-12
Android OKHttp框架的分發(fā)器與攔截器源碼刨析
okhttp是一個第三方類庫,用于android中請求網(wǎng)絡(luò)。這是一個開源項目,是安卓端最火熱的輕量級框架,由移動支付Square公司貢獻(該公司還貢獻了Picasso和LeakCanary) 。用于替代HttpUrlConnection和Apache HttpClient2022-11-11
recycleview實現(xiàn)拼多多首頁水平滑動效果
這篇文章主要為大家詳細介紹了recycleview實現(xiàn)拼多多首頁水平滑動效,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-05-05
Flexbox+ReclyclerView實現(xiàn)流式布局
這篇文章主要為大家詳細介紹了Flexbox+ReclyclerView實現(xiàn)流式布局,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-11-11
Kotlin協(xié)程啟動createCoroutine及創(chuàng)建startCoroutine原理
這篇文章主要為大家介紹了Kotlin協(xié)程啟動createCoroutine及創(chuàng)建startCoroutine原理詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-08-08
Android 架構(gòu)之數(shù)據(jù)庫框架搭建
這篇文章主要給大家介紹的是Android 架構(gòu)之數(shù)據(jù)庫框架搭建,在本篇中,將會讓你一點一滴從無到有創(chuàng)建一個不再為數(shù)據(jù)庫而煩惱的框架。需要的朋友可以參考下面文章的具體內(nèi)容2021-09-09

