一篇文章弄懂Android自定義viewgroup的相關(guān)難點
本文的目的
目的在于教會大家到底如何自定義viewgroup,自定義布局和自定義測量到底如何寫。很多網(wǎng)上隨便搜搜的概念和流程圖
這里不再過多描述了,建議大家看本文之前,先看看基本的自定義viewgroup流程,心中有個大概即可。本文注重于實踐
viewgroup 的測量布局流程基本梳理
稍微回顧下,基本的viewgroup繪制和布局流程中的重點:
1.view 在onMeasure()方法中進(jìn)行自我測量和保存,也就是說對于view(不是viewgroup噢)來說一定在onMeasure方法中計算出自己的尺寸并且保存下來
2.viewgroup實際上最終也是循環(huán)從上大小來調(diào)用子view的measure方法,注意子view的measure其實最終調(diào)用的是子view的onMeasure 方法。所以我們理解這個過程為:
viewgroup循環(huán)遍歷調(diào)用所有子view的onmeasure方法,利用onmeasure方法計算出來的大小,來確定這些子view最終可以占用的大小和所處的布局的位置。
3.measure方法是一個final方法,可以理解為做測量工作準(zhǔn)備工作的,既然是final方法所以我們無法重寫它,不需要過多
關(guān)注他,因為measure最終要調(diào)用onmeasure ,這個onmeasure我們是可以重寫的。要關(guān)注這個。layout和onlayout是一樣的關(guān)系。
4.父view調(diào)用子view的layout方法的時候會把之前measure階段確定的位置和大小都傳遞給子view。
5.對于自定義view/viewgroup來說 我們幾乎只需要關(guān)注下面三種需求:
- 對于已有的android自帶的view,我們只需要重寫他的onMeasure方法即可。修改一下這個尺寸即可完成需求。
- 對于android系統(tǒng)沒有的,屬于我們自定義的view,比上面那個要復(fù)雜一點,要完全重寫onMeasure方法。
- 第三種最復(fù)雜,需要重寫onmeasure和onlayout2個方法,來完成一個復(fù)雜viewgroup的測量和布局。
6.onMeasure方法的特殊說明:

7.如何理解父view對子view的限制?
onMeasure的兩個參數(shù)既然是父view對子view的限制,那么這個限制的值到底是哪來的呢?
實際上,父view對子view的限制絕大多數(shù)就來自于我們開發(fā)者所設(shè)置的layout開頭的這些屬性
比方說我們給一個imageview設(shè)置了他的layout_width和layout_height 這2個屬性,那這2個屬性其實就是我們開發(fā)者
所期望的寬高屬性,但是要注意了,
設(shè)置的這2個屬性是給父view看的,實際上對于絕大多數(shù)的layout開頭的屬性這些屬性都是設(shè)置給父view看的
為什么要給父view看?因為父view要知道這些屬性以后才知道要對子view的測量加以什么限制?
到底是不限制(UNSPECIFIED)?還是限制個最大值(AT_MOST),讓子view不超過這個值?還是直接限制死,我讓你是多少就得是多少(EXACTLY)。
自定義一個BannerImageView 修改onMeasure方法
所謂bannerImageview,就是很多電商其實都會放廣告圖,這個廣告圖的寬高比都是可變的,我們在日常開發(fā)過程中
也會經(jīng)常接觸到這種需求:imageview的寬高比 在高保真中都標(biāo)注出來,但是考慮到很多手機的屏幕寬度或者高度都不確定
所以我們通常都要手動來計算出這個imageview高度或者寬度,然后動態(tài)改變width或者h(yuǎn)eight的值。這種方法可用但是很麻煩
這里給出一個自定義的imageview,通過設(shè)置一個ratio的屬性即可動態(tài)的設(shè)置iv的高度。很是方便

看下效果

最后看下代碼,重要的部分都寫在注釋里了,不再過多講了。
public class BannerImageView extends ImageView {
//寬高比
float ratio;
public BannerImageView(Context context) {
super(context);
}
public BannerImageView(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.BannerImageView);
ratio = typedArray.getFloat(R.styleable.BannerImageView_ratio, 1.0f);
typedArray.recycle();
}
public BannerImageView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//人家自己的測量還是要自己走一遍的,因為這個方法內(nèi)部會調(diào)用setMeasuredDimension方法來保存測量結(jié)果了
//只有保存了以后 我們才能取得這個測量結(jié)果 否則你下面是取不到的
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//取測量結(jié)果
int mWidth = getMeasuredWidth();
int mHeight = (int) (mWidth * ratio);
//保存了以后,父view就可以拿到這個測量的寬高了。不保存是拿不到的噢。
setMeasuredDimension(mWidth, mHeight);
}
}
自定義view,完全自己寫onMeasure方法
首先明確一個結(jié)論:
對于完全自定義的view,完全自己寫的onMeasure方法來說,你保存的寬高必須要符合父view的限制,否則會發(fā)生bug,
保存父view對子view的限制的方法也很簡單直接調(diào)用resolveSize方法即可。


所以對于完全自定義的view onMeasure方法也不難寫了,
- 先算自己想要的寬高,比如你畫了個圓,那么寬高就肯定是半徑的兩倍大小, 要是圓下面還有字,
- 那么高度肯定除了半徑的兩倍還要有字體的大小。對吧。很簡單。這個純看你自定義view是啥樣的
- 算完自己想要的寬高以后 直接拿resolveSize 方法處理一下 即可。
- 最后setMeasuredDimension 保存。
范例:
public class LoadingView extends View {
//圓形的半徑
int radius;
//圓形外部矩形rect的起點
int left = 10, top = 30;
Paint mPaint = new Paint();
public LoadingView(Context context) {
super(context);
}
public LoadingView(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.LoadingView);
radius = typedArray.getInt(R.styleable.LoadingView_radius, 0);
}
public LoadingView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = left + radius * 2;
int height = top + radius * 2;
//一定要用resolveSize方法來格式化一下你的view寬高噢,否則遇到某些layout的時候一定會出現(xiàn)奇怪的bug的。
//因為不用這個 你就完全沒有父view的感受了 最后強調(diào)一遍
width = resolveSize(width, widthMeasureSpec);
height = resolveSize(height, heightMeasureSpec);
setMeasuredDimension(width, height);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
RectF oval = new RectF(left, top,
left + radius * 2, top + radius * 2);
mPaint.setColor(Color.BLUE);
canvas.drawRect(oval, mPaint);
//先畫圓弧
mPaint.setColor(Color.RED);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(2);
canvas.drawArc(oval, -90, 360, false, mPaint);
}
}
布局文件:
<LinearLayout android:layout_width="200dp" android:layout_height="200dp" android:background="#000000" android:orientation="horizontal"> <com.example.a16040657.customviewtest.LoadingView android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@mipmap/dly" app:radius="200"></com.example.a16040657.customviewtest.LoadingView> <com.example.a16040657.customviewtest.LoadingView android:layout_marginLeft="10dp" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@mipmap/dly" app:radius="200"></com.example.a16040657.customviewtest.LoadingView> </LinearLayout>
最后效果:

自定義一個viewgroup
這個其實也就是稍微復(fù)雜了一點,但是還是有跡可循的,只是稍微需要一點額外的耐心。
自定義一個viewgroup 需要注意的點如下:
- 一定是先重寫onMeasure確定子view的寬高和自己的寬高以后 才可以繼續(xù)寫onlayout 對這些子view進(jìn)行布局噢~~
- viewgroup 的onMeasure其實就是遍歷自己的view 對自己的每一個子view進(jìn)行measure,絕大多數(shù)時候?qū)ψ觱iew的measure都可以直接用 measureChild()這個方法來替代,簡化我們的寫法,如果你的viewgroup很復(fù)雜的話無法就是自己寫一遍measureChild 而不是調(diào)用measureChild 罷了。
- 計算出viewgroup自己的尺寸并且保存,保存的方法還是哪個setMeasuredDimension 不要忘記了
- 逼不得已要重寫measureChild方法的時候,其實也不難無非就是對父view的測量和子view的測量 做一個取舍關(guān)系而已,你看懂了基礎(chǔ)的measureChild方法,以后就肯定會寫自己的復(fù)雜的measureChild方法了。
下面是一個極簡的例子,一個很簡單的flowlayout的實現(xiàn),沒有對margin paddding做處理,也假設(shè)了每一個tag的高度
是固定的,可以說是極為簡單了,但是麻雀雖小 五臟俱全,足夠你們好好理解自定義viewgroup的關(guān)鍵點了。
/**
* 寫一個簡單的flowlayout 從左到右的簡單layout,如果寬度不夠放 就直接另起一行l(wèi)ayout
* 這個類似的開源控件有很多,有很多寫的出色的,我這里只僅僅實現(xiàn)一個初級的flowlayout
* 也是最簡單的,目的是為了理解自定義viewgroup的關(guān)鍵核心點。
* <p>
* 比方說這里并沒有對padding或者margin做特殊處理,你們自己寫viewgroup的時候 記得把這些屬性的處理都加上
* 否則一旦有人用了這些屬性 發(fā)現(xiàn)沒有生效就比較難看了。。。。。。
*/
public class SimpleFlowLayout extends ViewGroup {
public SimpleFlowLayout(Context context) {
super(context);
}
public SimpleFlowLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SimpleFlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
/**
* layout的算法 其實就是 不夠放剩下一行 那另外放一行 這個過程一定要自己寫一遍才能體會,
* 個人有個人的寫法,說不定你的寫法比開源的項目還要好
* 其實也沒什么夸張的,無法就是前面onMeasure結(jié)束以后 你可以拿到所有子view和自己的 測量寬高 然后就算唄
*
* @param changed
* @param l
* @param t
* @param r
* @param b
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childTop = 0;
int childLeft = 0;
int childRight = 0;
int childBottom = 0;
//已使用 width
int usedWidth = 0;
//customlayout 自己可使用的寬度
int layoutWidth = getMeasuredWidth();
Log.v("wuyue", "layoutWidth==" + layoutWidth);
for (int i = 0; i < getChildCount(); i++) {
View childView = getChildAt(i);
//取得這個子view要求的寬度和高度
int childWidth = childView.getMeasuredWidth();
int childHeight = childView.getMeasuredHeight();
//如果寬度不夠了 就另外啟動一行
if (layoutWidth - usedWidth < childWidth) {
childLeft = 0;
usedWidth = 0;
childTop += childHeight;
childRight = childWidth;
childBottom = childTop + childHeight;
childView.layout(0, childTop, childRight, childBottom);
usedWidth = usedWidth + childWidth;
childLeft = childWidth;
continue;
}
childRight = childLeft + childWidth;
childBottom = childTop + childHeight;
childView.layout(childLeft, childTop, childRight, childBottom);
childLeft = childLeft + childWidth;
usedWidth = usedWidth + childWidth;
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//先取出SimpleFlowLayout的父view 對SimpleFlowLayout 的測量限制 這一步很重要噢。
//你只有知道自己的寬高 才能限制你子view的寬高
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int usedWidth = 0; //已使用的寬度
int remaining = 0; //剩余可用寬度
int totalHeight = 0; //總高度
int lineHeight = 0; //當(dāng)前行高
for (int i = 0; i < getChildCount(); i++) {
View childView = getChildAt(i);
LayoutParams lp = childView.getLayoutParams();
//先測量子view
measureChild(childView, widthMeasureSpec, heightMeasureSpec);
//然后計算一下寬度里面 還有多少是可用的 也就是剩余可用寬度
remaining = widthSize - usedWidth;
//如果一行不夠放了,也就是說這個子view測量的寬度 大于 這一行 剩下的寬度的時候 我們就要另外啟一行了
if (childView.getMeasuredWidth() > remaining) {
//另外啟動一行的時候,使用過的寬度 當(dāng)然要設(shè)置為0
usedWidth = 0;
//另外啟動一行了 我們的總高度也要加一下,不然高度就不對了
totalHeight = totalHeight + lineHeight;
}
//已使用 width 進(jìn)行 累加
usedWidth = usedWidth + childView.getMeasuredWidth();
//當(dāng)前 view 的高度
lineHeight = childView.getMeasuredHeight();
}
//如果SimpleFlowLayout 的高度 為wrap cotent的時候 才用我們疊加的高度,否則,我們當(dāng)然用父view對如果SimpleFlowLayout 限制的高度
if (heightMode == MeasureSpec.AT_MOST) {
heightSize = totalHeight;
}
setMeasuredDimension(widthSize, heightSize);
}
}
最后看下效果

總結(jié)
以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,如果有疑問大家可以留言交流,謝謝大家對腳本之家的支持。
相關(guān)文章
Android Notification使用方法總結(jié)
這篇文章主要介紹了Android Notification使用方法總結(jié)的相關(guān)資料,這里提供了四種使用方法,需要的朋友可以參考下2017-09-09
Android6.0編程實現(xiàn)雙向通話自動錄音功能的方法詳解
這篇文章主要介紹了Android6.0編程實現(xiàn)雙向通話自動錄音功能的方法,結(jié)合實例形式分析了Android錄音功能的原理、實現(xiàn)技巧與相關(guān)注意事項,需要的朋友可以參考下2017-07-07
使用RecyclerView添加Header和Footer的方法
RecyclerView雖然作為ListView的替代者有著較好的性能提升,但是ListView的一些常用功能卻沒有提供,比如我們平時會經(jīng)常用到的addHeaderView,addFooterView,既然RecyclerView沒有提供這個方法,我們應(yīng)該如何為列表添加頭部和底部呢,接下來通過本文給大家介紹2016-03-03
淺談Android獲取ImageView上的圖片,和一個有可能遇到的問題
下面小編就為大家?guī)硪黄獪\談Android獲取ImageView上的圖片,和一個有可能遇到的問題。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-04-04
android中DownloadManager實現(xiàn)版本更新,監(jiān)聽下載進(jìn)度實例
本篇文章主要介紹了android中DownloadManager實現(xiàn)版本更新,監(jiān)聽下載進(jìn)度實例。具有一定的參考價值,感興趣的小伙伴們可以參考一下。2017-03-03

