Android實(shí)現(xiàn)簡單的自定義ViewGroup流式布局
前言
前面幾篇我們簡單的復(fù)習(xí)了一下自定義 View 的測(cè)量與繪制,并且回顧了常見的一些事件的處理方式。
那么如果我們想自定義 ViewGroup 的話,它和自定義View又有什么區(qū)別呢?其實(shí)我們把 ViewGroup 當(dāng)做 View 來用的話也不是不可以。但是既然我們用到了容器 ViewGroup 當(dāng)時(shí)是想用它的一些特殊的特性了。
比如 ViewGroup 的測(cè)量,ViewGroup的布局,ViewGroup的繪制。
- ViewGroup的測(cè)量:與 View 的測(cè)量不同,ViewGroup 的測(cè)量會(huì)遍歷子 View ,獲取子 View 的大小,從而決定自己的大小。當(dāng)然我們也可以通過指定的模式來指定自身的大小。
- ViewGroup的布局:這個(gè)是 ViewGroup 核心與常用的功能。找到對(duì)于的子View 布局到指定的位置。
- ViewGroup的繪制:一般我們不會(huì)重寫這個(gè)方法,因?yàn)橐话銇碚f它本身不需要繪制,并且當(dāng)我們沒有設(shè)置ViewGroup的背景的時(shí)候,onDraw()方法都不會(huì)被調(diào)用,一般來說 ViewGroup 只是會(huì)使用 dispatchDraw()方法來繪制其子View,其過程同樣是通過遍歷所有子View,并調(diào)用子View的繪制方法來完成繪制工作。
下面我們一起復(fù)習(xí)一下ViewGroup的測(cè)量布局方式。我們以入門級(jí)的 FlowLayout 為例,看看流式布局是如何測(cè)量與布局的。
話不多說,Let's go
一、基本的測(cè)量與布局
我們先回顧一下ViewGroup的
一個(gè)經(jīng)典的ViewGroup測(cè)量是怎樣實(shí)現(xiàn)?一般來說,最簡單的測(cè)量如下:
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); for(int i = 0; i < getChildCount(); i++){ View childView = getChildAt(i); measureChild(childView,widthMeasureSpec,heightMeasureSpec); } }
或者我們直接使用封裝之后的默認(rèn)方法
measureChildren(widthMeasureSpec,heightMeasureSpec);
其內(nèi)部也是遍歷子View來實(shí)現(xiàn)的。當(dāng)然如果有自定義的一些寬高測(cè)量規(guī)則,就不能使用這個(gè)方法,就需要自己遍歷找到View自定義實(shí)現(xiàn)了。
需要注意的是,這里我們測(cè)量子布局傳遞的 widthMeasureSpec 和 heightMeasureSpec 是父布局的測(cè)量模式。
當(dāng)父布局設(shè)置為固定寬度的時(shí)候,子View是不能超過這個(gè)寬度的,比如父控件設(shè)置為match_parent,自定義View無論是match_parent 還是 wrap_content 都是一樣的,充滿整個(gè)父控件。
相當(dāng)于父布局調(diào)用子控件的onMeasure方法的時(shí)候告訴子控件,我就這么大,你看著辦,不能超過它。
而父布局傳遞的是自適應(yīng)AT_MOST模式,那么就是由子View來決定父布局的寬高。
相當(dāng)于父布局調(diào)用子控件的onMeasure方法的時(shí)候問子控件,我也不知道我多大,你需要多大的位置?我又需要多大的地方才能容納你?
其實(shí)也很好理解。那么一個(gè)經(jīng)典的ViewGroup布局又是怎樣實(shí)現(xiàn)?重寫 onLayout 并且遍歷拿到每一個(gè)View,進(jìn)行Layout操作。
比如如下的代碼,我們每一個(gè)View的高度設(shè)置為固定高度,并且垂直排列,類似一個(gè)ListView 的布局:
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int childCount = getChildCount(); //設(shè)置子View的高度 MarginLayoutParams params = (MarginLayoutParams) getLayoutParams(); params.height = mFixedHeight * childCount; setLayoutParams(params); for (int i = 0; i < childCount; i++) { View child = getChildAt(i); if (child.getVisibility() != View.GONE) { child.layout(l, i * mFixedHeight, r, (i + 1) * mFixedHeight); } } }
注意我們 onLayout() 的參數(shù)
展示的效果就是這樣:
二、流式的布局的layout
首先我們先不管測(cè)量,我們先指定ViewGroup的寬高為固定寬高,指定為match_parent。我們先做布局的操作:
我們自定義 ViewGroup 中重寫測(cè)量與布局的方法:
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { measureChildren(widthMeasureSpec,heightMeasureSpec); super.onMeasure(widthMeasureSpec, heightMeasureSpec); } /** * @param changed 當(dāng)前ViewGroup的尺寸或者位置是否發(fā)生了改變 * @param l,t,r,b 當(dāng)前ViewGroup相對(duì)于父控件的坐標(biāo)位置, */ @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int mViewGroupWidth = getMeasuredWidth(); //當(dāng)前ViewGroup的總寬度 int layoutChildViewCurX = l; //當(dāng)前繪制View的X坐標(biāo) int layoutChildViewCurY = t; //當(dāng)前繪制View的Y坐標(biāo) int childCount = getChildCount(); //子控件的數(shù)量 //遍歷所有子控件,并在其位置上繪制子控件 for (int i = 0; i < childCount; i++) { View childView = getChildAt(i); //子控件的寬和高 int width = childView.getMeasuredWidth(); int height = childView.getMeasuredHeight(); //如果剩余控件不夠,則移到下一行開始位置 if (layoutChildViewCurX + width > mViewGroupWidth) { layoutChildViewCurX = l; //如果換行,則需要修改當(dāng)前繪制的高度位置 layoutChildViewCurY += height; } //執(zhí)行childView的布局與繪制(右和下的位置加上自身的寬高即可) childView.layout(layoutChildViewCurX, layoutChildViewCurY, layoutChildViewCurX + width, layoutChildViewCurY + height); //布局完成之后,下一次繪制的X坐標(biāo)需要加上寬度 layoutChildViewCurX += width; } }
最后我們就能得到對(duì)應(yīng)的換行效果,如下:
通過上面我們的基礎(chǔ)學(xué)習(xí),我們應(yīng)該能理解這樣的布局方式,跟上面的基礎(chǔ)布局方式相比,就是多了一個(gè) layoutChildViewCurX 和 layoutChildViewCurY 。關(guān)于其它的邏輯這里已經(jīng)注釋的非常清楚了。
但是這樣的效果好丑,我們加上間距 margin 試試?
并沒有效果,其實(shí)是內(nèi)部 View 的 LayoutParams 就不支持 margin,我們需要定義一個(gè)內(nèi)部類繼承 ViewGroup.MarginLayoutParams,并重寫generateLayoutParams() 方法。
//要使子控件的margin屬性有效必須繼承此LayoutParams,內(nèi)部還可以定制一些別的屬性 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 layoutParams) { super(layoutParams); } } @Override public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { return new ViewGroup2.LayoutParams(getContext(), attrs); } @Override protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { return new LayoutParams(p); }
然后修改一下代碼,在 layout 子布局的時(shí)候我們手動(dòng)的把 margin 加上。
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int mViewGroupWidth = getMeasuredWidth(); //當(dāng)前ViewGroup的總寬度 int layoutChildViewCurX = l; //當(dāng)前繪制View的X坐標(biāo) int layoutChildViewCurY = t; //當(dāng)前繪制View的Y坐標(biāo) int childCount = getChildCount(); //子控件的數(shù)量 //遍歷所有子控件,并在其位置上繪制子控件 for (int i = 0; i < childCount; i++) { View childView = getChildAt(i); //子控件的寬和高 int width = childView.getMeasuredWidth(); int height = childView.getMeasuredHeight(); final LayoutParams lp = (LayoutParams) childView.getLayoutParams(); //如果剩余控件不夠,則移到下一行開始位置 if (layoutChildViewCurX + width + lp.leftMargin + lp.rightMargin > mViewGroupWidth) { layoutChildViewCurX = l; //如果換行,則需要修改當(dāng)前繪制的高度位置 layoutChildViewCurY += height + lp.topMargin + lp.bottomMargin; } //執(zhí)行childView的布局與繪制(右和下的位置加上自身的寬高即可) childView.layout( layoutChildViewCurX + lp.leftMargin, layoutChildViewCurY + lp.topMargin, layoutChildViewCurX + width + lp.leftMargin + lp.rightMargin, layoutChildViewCurY + height + lp.topMargin + lp.bottomMargin); //布局完成之后,下一次繪制的X坐標(biāo)需要加上寬度 layoutChildViewCurX += width + lp.leftMargin + lp.rightMargin; } }
此時(shí)的效果就能生效了:
三、流式的布局的Measure
前面的設(shè)置我們都是使用的寬高 match_parent。那我們修改 ViewGroup 的高度為 wrap_content ,能實(shí)現(xiàn)高度自適應(yīng)嗎?
這...并不是我們想要的效果。并沒有自適應(yīng)高度。因?yàn)槲覀儧]有寫測(cè)量的邏輯。
我們想一下,如果我們的寬度是固定的,想要高度自適應(yīng),那么我們就需要測(cè)量每一個(gè)子View的高度,計(jì)算出對(duì)應(yīng)的高度,當(dāng)換行之后我們?cè)偌由闲械母叨取?/p>
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { final int sizeWidth = MeasureSpec.getSize(widthMeasureSpec) - this.getPaddingRight() - this.getPaddingLeft(); final int modeWidth = MeasureSpec.getMode(widthMeasureSpec); final int sizeHeight = MeasureSpec.getSize(heightMeasureSpec) - this.getPaddingTop() - this.getPaddingBottom(); final int modeHeight = MeasureSpec.getMode(heightMeasureSpec); if (modeWidth == MeasureSpec.EXACTLY && modeHeight == MeasureSpec.EXACTLY) { measureChildren(widthMeasureSpec, heightMeasureSpec); super.onMeasure(widthMeasureSpec, heightMeasureSpec); } else if (modeWidth == MeasureSpec.EXACTLY && modeHeight == MeasureSpec.AT_MOST) { int layoutChildViewCurX = this.getPaddingLeft(); int totalControlHeight = 0; for (int i = 0; i < getChildCount(); i++) { final View childView = this.getChildAt(i); if (childView.getVisibility() == GONE) { continue; } final LayoutParams lp = (LayoutParams) childView.getLayoutParams(); childView.measure( getChildMeasureSpec(widthMeasureSpec, this.getPaddingLeft() + this.getPaddingRight(), lp.width), getChildMeasureSpec(heightMeasureSpec, this.getPaddingTop() + this.getPaddingBottom(), lp.height) ); int width = childView.getMeasuredWidth(); int height = childView.getMeasuredHeight(); if (totalControlHeight == 0) { totalControlHeight = height + lp.topMargin + lp.bottomMargin; } //如果剩余控件不夠,則移到下一行開始位置 if (layoutChildViewCurX + width + lp.leftMargin + lp.rightMargin > sizeWidth) { layoutChildViewCurX = this.getPaddingLeft(); totalControlHeight += height + lp.topMargin + lp.bottomMargin; } layoutChildViewCurX += width + lp.leftMargin + lp.rightMargin; } //最后確定整個(gè)布局的高度和寬度 int cachedTotalWith = resolveSize(sizeWidth, widthMeasureSpec); int cachedTotalHeight = resolveSize(totalControlHeight, heightMeasureSpec); this.setMeasuredDimension(cachedTotalWith, cachedTotalHeight); }
寬度固定和高度自適應(yīng)的情況下,我們是這么處理的。計(jì)算出子View的總高度,然后設(shè)置 setMeasuredDimension 為ViewGroup的測(cè)量寬度和子View的總高度。即為最終 ViewGroup 的寬高。
這樣我們就能實(shí)現(xiàn)高度的自適應(yīng)了。那么寬度能不能自適應(yīng)呢?
當(dāng)然可以,我們只需要記錄每一行的寬度,然后最終 setMeasuredDimension 的時(shí)候傳入所有行中的最大寬度,就是 ViewGroup 的最終寬度,而高度的計(jì)算是和上面的方式一樣的。
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { ... else if (modeWidth == MeasureSpec.AT_MOST && modeHeight == MeasureSpec.AT_MOST) { //如果寬高都是Wrap-Content int layoutChildViewCurX = this.getPaddingLeft(); //總寬度和總高度 int totalControlWidth = 0; int totalControlHeight = 0; //由于寬度是非固定的,所以用一個(gè)List接收每一行的最大寬度 List<Integer> lineLenghts = new ArrayList<>(); for (int i = 0; i < getChildCount(); i++) { final View childView = this.getChildAt(i); if (childView.getVisibility() == GONE) { continue; } final LayoutParams lp = (LayoutParams) childView.getLayoutParams(); childView.measure( getChildMeasureSpec(widthMeasureSpec, this.getPaddingLeft() + this.getPaddingRight(), lp.width), getChildMeasureSpec(heightMeasureSpec, this.getPaddingTop() + this.getPaddingBottom(), lp.height) ); int width = childView.getMeasuredWidth(); int height = childView.getMeasuredHeight(); if (totalControlHeight == 0) { totalControlHeight = height + lp.topMargin + lp.bottomMargin; } //如果剩余控件不夠,則移到下一行開始位置 if (layoutChildViewCurX + width + lp.leftMargin + lp.rightMargin > sizeWidth) { lineLenghts.add(layoutChildViewCurX); layoutChildViewCurX = this.getPaddingLeft(); totalControlHeight += height + lp.topMargin + lp.bottomMargin; } layoutChildViewCurX += width + lp.leftMargin + lp.rightMargin; } //計(jì)算每一行的寬度,選出最大值 YYLogUtils.w("每一行的寬度 :" + lineLenghts.toString()); totalControlWidth = Collections.max(lineLenghts); YYLogUtils.w("選出最大寬度 :" + totalControlWidth); //最后確定整個(gè)布局的高度和寬度 int cachedTotalWith = resolveSize(totalControlWidth, widthMeasureSpec); int cachedTotalHeight = resolveSize(totalControlHeight, heightMeasureSpec); this.setMeasuredDimension(cachedTotalWith, cachedTotalHeight); } }
為了效果,我們把第一行的最后一個(gè)View寬度多一點(diǎn),方便查看效果。
這樣就可以得到ViewGroup自適應(yīng)的寬度和高度了。并不復(fù)雜對(duì)不對(duì)!
后記
這樣是不是就能實(shí)現(xiàn)一個(gè)簡單的流式布局了呢?當(dāng)然這些只是為方便學(xué)習(xí)和理解,真正的實(shí)戰(zhàn)中并不推薦直接這樣使用,因?yàn)閮?nèi)部還有一些兼容的邏輯沒處理,一些邏輯沒有封裝,屬性沒有抽取。甚至連每一個(gè)View的高度,和每一行的最大高度也沒有處理,其實(shí)這樣健壯性并不好。
以上就是Android實(shí)現(xiàn)簡單的自定義ViewGroup流式布局的詳細(xì)內(nèi)容,更多關(guān)于Android ViewGroup流式布局的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android PopupWindow使用方法小結(jié)
這篇文章主要為大家詳細(xì)介紹了Android PopupWindow使用方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-06-06解決Android studio 2.3升級(jí)到Android studio 3.0 后apt報(bào)錯(cuò)問題
原來項(xiàng)目在Android studio 2.3一切正常,升級(jí)到了3.0之后報(bào)錯(cuò),不支持apt了,其實(shí)解決這個(gè)問題很簡單,只需要修改兩點(diǎn)內(nèi)容就可以,下面腳本之家小編帶領(lǐng)大家通過本文學(xué)習(xí)吧2017-12-12Android使用Handler實(shí)現(xiàn)定時(shí)器與倒計(jì)時(shí)器功能
Handler的最常見應(yīng)用場(chǎng)景之一便是通過Handler在子線程中間接更新UI。這篇文章主要介紹了Android使用Handler實(shí)現(xiàn)定時(shí)器與倒計(jì)時(shí)器功能,需要的朋友可以參考下2018-02-02Android自定義ViewGroup實(shí)現(xiàn)朋友圈九宮格控件
在我們的實(shí)際應(yīng)用中,經(jīng)常需要用到自定義控件,比如自定義圓形頭像,自定義計(jì)步器等等,這篇文章主要給大家介紹了關(guān)于Android自定義ViewGroup實(shí)現(xiàn)朋友圈九宮格控件的相關(guān)資料,需要的朋友可以參考下2021-07-07簡介Android應(yīng)用中sharedPreferences類存儲(chǔ)數(shù)據(jù)的用法
這篇文章主要介紹了Android應(yīng)用中使用sharedPreferences類存儲(chǔ)數(shù)據(jù)的方法,文中舉了用SharedPreferences保存數(shù)據(jù)和讀取數(shù)據(jù)的例子,需要的朋友可以參考下2016-02-02Android Activity啟動(dòng)模式之singleTop實(shí)例詳解
這篇文章主要介紹了Android Activity啟動(dòng)模式之singleTop,結(jié)合實(shí)例形式較為詳細(xì)的分析了singleTop模式的功能、使用方法與相關(guān)注意事項(xiàng),需要的朋友可以參考下2016-01-01Android上傳文件到服務(wù)端并顯示進(jìn)度條
這篇文章主要為大家詳細(xì)介紹了Android上傳文件到服務(wù)端,并顯示進(jìn)度條,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-11-11Android啟動(dòng)初始化方案App StartUp的應(yīng)用詳解
這篇文章主要介紹了Android啟動(dòng)初始化方案App StartUp的使用方法,StartUp是為了App的啟動(dòng)提供的一套簡單、高效的初始化方案,下面我們來詳細(xì)了解2022-09-09