Android開發中,總會遇到這樣和那樣的需求。雖然官方已經給咱們提供了豐富的ViewGroup
和View
的實現,可是總有無法知足需求的時候。這個時候咱們該怎麼辦呢? 首先遇事不決能夠先Google一下,看看有無現成的輪子。若是有輪子,那麼恭喜,扒來改改就好啦。若是沒有輪子,那能咋辦,只能本身造輪子咯。其實使用輪子更多時候是追求穩定和節約時間,咱們仍是須要對輪子的原理有必定的瞭解的。java
流式佈局
在Android開發中使用的場景應該仍是比較多的,好比標籤展現、搜索歷史記錄展現等等。這種樣式的佈局Android目前是沒有原生的ViewGroup
的,固然你要找輪子確定也是很容易找到的,不過今天我仍是想以自定義ViewGroup
的方式來實現這麼一個容器。web
首先咱們得弄清楚ViewGroup
是什麼,還有它的職責。markdown
ViewGroup
繼承自View
,並實現了ViewManager
和ViewParent
接口。按照官方的定義,ViewGroup
是一個特別的View
,它能夠容納其餘的View
,它實現了一系列添加和刪除View
的方法。同時ViewGroup
還定義了LayoutParams
,LayoutParams
會影響View
在ViewGroup
的位置和大小相關屬性。app
ViewGroup
也是個抽象類,須要咱們重寫onLayout
方法,固然僅僅重寫這麼一個方法是不夠的。ViewGroup
自己只是實現了容納View
的能力,實現一個ViewGroup
咱們須要完成對自身的測量、對child的測量、child的佈局等一系列的操做。ide
這是自定義View
實現的一個很是重要的方法,無論咱們是自定義View
也好,仍是自定義ViewGroup
都須要實現它。這個方法來自於View
,ViewGroup
自己沒有去處理這個方法。這個方法會傳遞兩個參數,分別是widthMeasureSpec
和heightMeasureSpec
。這兩個數值實際上是個混合的信息,他們包含了具體的寬高數值和寬高的模式。這裏須要說一下MeasureSpec
。oop
MeasureSpec
是View
的內部類,他是父容器給孩子傳遞的佈局信息的一個壓縮體。上文提到的傳遞的數值,實際上是經過MeasureSpec
的makeMeasureSpec
方法生成的:佈局
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
@IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
@Retention(RetentionPolicy.SOURCE)
public @interface MeasureSpecMode {}
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
public static final int EXACTLY = 1 << MODE_SHIFT;
public static final int AT_MOST = 2 << MODE_SHIFT;
public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size, @MeasureSpecMode int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
//...
複製代碼
其實MeasureSpec表明一個32位的int值,高2位表示SpecMode,低30位表示SpecSize,咱們能夠分別經過getMode
和getSize
獲取對應的信息。表示什麼信息算是搞清楚了,那麼這些信息又是如何確認的呢?this
在ViewGroup
中有個getChildMeasureSpec
方法,這個方法的實現基本能夠解答咱們的疑問google
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;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
複製代碼
代碼長度仍是有點長,可是邏輯並不複雜。spec參數爲ViewGroup的相關信息,padding則爲ViewGroup的leftPadding+rightPadding+childLeftMargin+childRightMargin+usedWidth,childDimension爲child的LayoutParams中指定的寬高信息。spa
child的具體的MeasureSpec
會受到父容器的影響,也和自身的佈局信息有關,具體以下:
這個specMode,簡單的來講EXACTLY就表明寬高信息是比較確認的,AT_MOST則是會告訴你一個最大寬度,實際寬度由你本身確認,UNSPECIFIED也是會告訴你一個父容器寬度,你也能夠設置爲任意高度。
上面說了一堆關於MeasureSpec的,如今再來講一下onMeasure方法裏應該作什麼。
若是是自定義View,咱們須要根據父容器傳遞的MeasureSpec來確認自身的寬高。若是是MeasureMode是EXACTLY,則這個View的寬高就是傳遞過來的size,若是是AT_MOST和UNSPECIFIED,則須要咱們自行處理了。在咱們計算獲得了一個想要的寬高信息後,須要調用setMeasuredDimension
的方法來保存信息。
若是是自定義ViewGroup,那咱們須要作的事情可能就要多一點了,首先咱們也仍是同樣,須要確認ViewGroup自身的寬高信息,若是都是EXACTLY拿很好辦,直接設置對應的size便可。若是想要支持WRAP_CONTENT,這時候可能就會比較麻煩一點了。首先咱們得想好一點,這個ViewGroup是如何爲child佈局的。這很重要,由於不一樣的佈局方式,child的排布不一樣,都會影響實際佔用的空間。
仍是以LinearLayout
舉例吧,LinearLayout
支持橫向排列和縱向排列,他們須要執行的測量邏輯都是不同的。若是是縱向排列,則須要遍歷child,測量child,並累加他們的高度和margin,最後還要加上自身高度,這樣累加出來的數值就是WRAP_CONTENT下,自身應該佔用的高度。若是是橫向排列,則須要遍歷和累加child,並累加他們的寬度和margin等,原理都是差很少的。
總結一下,onMeasure
方法須要ViewGroup
結合父容器傳遞的MeasureSpec
,測量child,配合child的排布方式,確認自身的寬高。
onLayout
方法傳遞了5個參數,changed表示自身的位置或大小是否發生了改變,剩下的分別爲left,top,right,bottom,決定了他在父容器的位置。這是一個相對座標,起點並非屏幕的左上角。
那在這個方法裏咱們應該作什麼呢?若是是自定義View
的時候,咱們能夠不用管這個方法。由於View
自己沒有容納child的能力,若是是ViewGroup
,這時候咱們就須要爲child執行佈局操做了。咱們須要遍歷child,執行它們的layout方法。經過調用layout
方法,咱們能夠傳遞left,top,right,bottom,肯定child在ViewGroup中的位置。一樣的,這也是一個相對座標,是依賴於父容器的。
事實上,onLayout
方法是在自身的layout
方法被調用後調用的。Android總體的佈局體系自上而下一層層的調用,傳遞佈局信息,最終確認了各個View在屏幕上的位置。
一般來講,自定義ViewGroup
並不須要重寫這個方法。這個方法用來作一些繪製操做,若是是自定義View
,那咱們則須要重寫這個方法,實現一些繪製邏輯。
這兩個概念仍是要說一下,理解一下它們的做用和實現原理。
View
自身的屬性。若是須要讓這個屬性生效,在繪製和佈局時候,咱們須要基於這個屬性的數值作必定的偏移,在測量的時候,咱們也須要考慮它的數值,爲最終測量結果添加上。View
在ViewGroup
中的佈局,它一般是由LayoutParams
所定義的。有這個屬性的時候,咱們在測量時候須要考慮到它,而且累加上,在佈局的時候,須要根據響應的屬性,進行必定的偏移。 道理都理清楚了,寫代碼就會簡單不少了。流式佈局大概的效果就是添加的VIew按一行或者一列有序排列,若是一行或者一列放不下了,則換到下一行排列。下面就簡單實現一個流式佈局來加深一下理解。
首先須要定義一個類,繼承自ViewGroup:
public class FlowLayout extends ViewGroup {
public FlowLayout(Context context) {
this(context, null);
}
public FlowLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//todo 實現測量邏輯
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
//todo 實現child的佈局邏輯
}
}
複製代碼
由於咱們須要支持margin
屬性,因此咱們還須要這樣一個LayoutParams
。ViewGroup
中已經定義了這樣一個MarginLayoutParams
,咱們建立一個內部類,繼承此類實現:
public static class LayoutParams extends ViewGroup.MarginLayoutParams {
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
}
public LayoutParams(int width, int height) {
super(width, height);
}
public LayoutParams(ViewGroup.LayoutParams source) {
super(source);
}
}
複製代碼
LayoutParams
中還能夠本身去定義一些個性化的佈局參數,這裏就簡單處理了。同時咱們還得注意如下幾個方法:
/** * 直接調用 {@link #addView(View view)}的時候 用來生成默認的LayoutParams * * @return */
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(-2, -2);
}
/** * {@link #addView(View child, ViewGroup.LayoutParams params)}時候,用來檢查佈局參數是否正確 * * @param p * @return */
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof LayoutParams;
}
/** * 若是{@link #checkLayoutParams(ViewGroup.LayoutParams p)}返回false,會調用此方法生成LayoutParams * * @param p * @return */
@Override
protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
if (p == null) {
return generateDefaultLayoutParams();
}
return new LayoutParams(p);
}
/** * 若是xml中的child,會調用此方法生成佈局參數 * @param attrs * @return */
@Override
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
複製代碼
註釋我都寫了,主要是用來用戶addView時候的默認佈局信息生成和檢測,若是沒處理好,可能會引發崩潰啥的。
接下來是測量方法:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Log.d(TAG, "onMeasure");
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthMode == MeasureSpec.EXACTLY) {
//橫向寬度固定
int lineMaxHeight = 0;//當前行最高的行高
int currentLeft = getPaddingLeft();//當前child的起點left
int currentTop = getPaddingTop();//當前child的起點top
//去除paddingLeft 和 paddingRight即爲可用寬度
int availableWidth = widthSize - getPaddingLeft() - getPaddingRight();
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if (child.getVisibility() == GONE) {//gone的child 不處理
continue;
}
//測量child
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
int decoratedWidth = getDecoratedWidth(child);
int decoratedHeight = getDecoratedHeight(child);
if (currentLeft + decoratedWidth > availableWidth) {
//寬度超了 換行
currentLeft = decoratedWidth + getPaddingLeft();
currentTop += lineMaxHeight;//高度加上以前的最大高度
lineMaxHeight = decoratedHeight;
} else {
//若是不須要換行 只記錄當前的最大高度。
currentLeft += decoratedWidth;
lineMaxHeight = Math.max(lineMaxHeight, decoratedHeight);
}
if (i == getChildCount() - 1) {
//最後一個元素了 咱們須要累加高度
currentTop += lineMaxHeight;
}
}
//保存寬高信息
setMeasuredDimension(widthSize, currentTop + getPaddingBottom());
} else if (heightMode == MeasureSpec.EXACTLY) {
//todo 實現縱向固定的流式佈局
} else {
//todo 實現寬高都固定的流式佈局
}
}
複製代碼
測量邏輯並不複雜,首先判斷ViewGroup的寬高模式,這裏實現了寬度固定的流式佈局的處理邏輯。咱們須要遍歷全部的child,並調用測量方法肯定他們的寬高。同時要注意的是child若是不可見則須要跳過。由於寬度是固定的,因此咱們須要計算出自身的高度。getDecoratedWidth
獲取的是child自身的寬度與自身的左右的margin
的和。遍歷過程當中依此排列child,若是一行排不下了,則執行換行邏輯,並累加高度,最後得出高度,保存。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
Log.d(TAG, "onLayout l :" + l + " t :" + t + " r :" + r + " b :" + b);
int lineMaxHeight = 0;
int currentLeft = getPaddingLeft();//當前child的起點left
int currentTop = getPaddingTop();//當前child的起點top
int availableWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if (child.getVisibility() == GONE) {//gone的child 不處理
continue;
}
int decoratedWidth = getDecoratedWidth(child);
int decoratedHeight = getDecoratedHeight(child);
LayoutParams layoutParams = (LayoutParams) child.getLayoutParams();
int childLeft, childTop;
if (currentLeft + decoratedWidth > availableWidth) {
//寬度超了 換行
currentLeft = decoratedWidth + getPaddingLeft();
currentTop += lineMaxHeight;//高度加上以前的最大高度
lineMaxHeight = decoratedHeight;
childLeft = getPaddingLeft() + +layoutParams.leftMargin;
childTop = currentTop + layoutParams.topMargin;
} else {
//若是不須要換行 只記錄當前的最大高度。
childLeft = currentLeft + layoutParams.leftMargin;
childTop = currentTop + layoutParams.topMargin;
currentLeft += decoratedWidth;
lineMaxHeight = Math.max(lineMaxHeight, decoratedHeight);
}
child.layout(childLeft, childTop,
childLeft + child.getMeasuredWidth(), childTop + child.getMeasuredHeight());
}
}
複製代碼
onLayout方法裏我也只是實現了寬度固定下的邏輯。邏輯和測量時候的思路同樣,在測量的時候咱們已經爲每一個child確認了自身的寬高,在這裏咱們就只須要調用layout
方法爲每一個child執行佈局邏輯便可。
最後上運行效果,由於是demo因此樣式比較隨意,不要在乎這些細節(#^.^#)
自定義ViewGroup大體的流程就是這樣了,若是還有什麼困惑還不解能夠留言,我會用心解答。