時(shí)間過(guò)得真快救湖,又到了寫(xiě)博客的時(shí)候了(/▽╲)逆瑞。這次按照計(jì)劃記錄一個(gè)簡(jiǎn)單的自定義ViewGroup:流布局FlowLayout
的實(shí)現(xiàn)過(guò)程糠雨,將自定義控件知識(shí)儲(chǔ)備-View的繪制流程和自定義控件知識(shí)儲(chǔ)備-LayoutParams的那些事里的知識(shí)點(diǎn)結(jié)合起來(lái)蛙奖,付諸實(shí)踐亮垫。
1. 前言
早在學(xué)習(xí)Java的Swing基礎(chǔ)知識(shí)的時(shí)候,就見(jiàn)到過(guò)里面的流布局FlowLayout躺涝,基本的效果就是讓加入此容器的控件自左往右依次排列厨钻,如果當(dāng)前行的寬度不足以容納下一個(gè)控件,就會(huì)將此控件放置到下一行坚嗜。其實(shí)這也跟css里向左浮動(dòng)的效果很相似夯膀。
在Android的世界里,系統(tǒng)是沒(méi)有提供類(lèi)似FlowLayout布局的容器的苍蔬。當(dāng)然了诱建,現(xiàn)在官方給我們提供了更強(qiáng)大也更復(fù)雜的FlexLayout
了。不過(guò)嘛碟绑,本篇博客是總結(jié)一個(gè)自定義ViewGroup的實(shí)現(xiàn)流程俺猿,所以需要找一個(gè)難易適中的實(shí)例來(lái)進(jìn)行分析,也就是FlowLayout了格仲。(是的押袍,我就是挑軟柿子捏︿( ̄︶ ̄)︿)。
2. 效果
閑話(huà)少說(shuō)凯肋,還是先來(lái)看看蘑菇君寫(xiě)的FlowLayout的功能:
- 支持最基本的從左至右的排序谊惭,空間不足則換行
- 支持設(shè)置子控件間的水平和豎直的間隔(也可以通過(guò)給每個(gè)child設(shè)置margin來(lái)實(shí)現(xiàn),不過(guò)沒(méi)有統(tǒng)一設(shè)置來(lái)的方便)
- 支持繪制行之間的分割線(xiàn)
- 支持FlowLayout本身的
Gravity
和child views的Gravity
- 處理好FlowLayout的padding和child views的margin
這些都是FlowLayout基本的功能侮东,效果如下圖所示:
是不是感覺(jué)還行圈盔?至少一般的情況下是能滿(mǎn)足大部分人的需求滴。o( ̄▽?zhuān)?d
3. 分析
列舉一下自定義ViewGroup的流程:
- 自定義屬性:如果ViewGroup需要用到自定義屬性悄雅,則需要聲明驱敲、設(shè)置、解析并獲取自定義屬性值煤伟。
- 測(cè)量:在
onMeasure
方法里處理AT_MOST
和EXACTLY
兩種測(cè)量模式下ViewGroup的寬高和children的寬高癌佩。(UNSPECIFIED
模式可以暫不考慮) - 布局:在
onLayout
方法里確定children的位置木缝。 - 繪制:如果ViewGroup里需要繪制,則重寫(xiě)onDraw方法围辙,按邏輯繪制我碟。比如FlowLayout可以在每一行之間繪制一條分隔線(xiàn)。
- 處理LayoutParams:如果要為children定義布局屬性姚建,如
layout_gravity
矫俺,則需要自定義LayoutParams,并且重寫(xiě)ViewGroup相關(guān)的方法掸冤。 - 處理滑動(dòng)事件:在本FlowLayout里暫時(shí)用不上...( ╯▽╰)
上面的步驟可能有所遺漏厘托,不過(guò)也差不多啦。下面蘑菇君要根據(jù)上述的流程來(lái)一步一步的分析FlowLayout的源碼稿湿,源碼可能有點(diǎn)長(zhǎng)铅匹,有些細(xì)節(jié)上的邏輯看不懂也莫方,只要了解流程對(duì)應(yīng)的實(shí)現(xiàn)方式和注意事項(xiàng)就好饺藤,有興趣的話(huà)可以稍后自己下載源碼分析具體的邏輯實(shí)現(xiàn)包斑。
好滴,那就讓我們來(lái)一步一步的看涕俗,這個(gè)FlowLayout是如何在我手里...被玩殘的...
3.1 自定義屬性
3.1.1 聲明屬性
首先罗丰,自定義屬性的第一步當(dāng)然是聲明屬性,而最常使用的方式當(dāng)然是在xml資源文件里(一般來(lái)說(shuō)就是attrs.xml文件)聲明需要使用的屬性:
<declare-styleable name="FlowLayout">
<attr name="android:gravity"/>
<attr name="horizonSpacing" format="dimension|reference"/>
<attr name="verticalSpacing" format="dimension|reference"/>
<attr name="dividerColor" format="color|reference"/>
<attr name="dividerWidth" format="dimension|reference"/>
</declare-styleable>
<declare-styleable name="FlowLayout_Layout">
<attr name="android:layout_gravity"/>
</declare-styleable>
這里需要注意兩個(gè)地方:
我們聲明了兩個(gè)
declare-styleable
再姑,一個(gè)是為FlowLayout
自身設(shè)置自定義屬性萌抵;另一個(gè)是為孩子們提供額外屬性,需要在自定義的LayoutParams
里解析獲取屬性值元镀。大家都知道绍填,我們?cè)趚ml布局文件里使用自定義屬性時(shí),需要引入命名空間
xmlns:app="http://schemas.android.com/apk/res-auto"
使用自定義屬性時(shí)栖疑,需要加上前綴app(或者是其它命名沐兰,只要一一對(duì)應(yīng))。但是有時(shí)候啊蔽挠,我們自定義的屬性名已經(jīng)在系統(tǒng)中存在了,而且語(yǔ)義與我們想要的也很符合瓜浸,比如如andrioid:text
澳淑、android:gravity
等等。這個(gè)時(shí)候估計(jì)誰(shuí)都會(huì)有一種“拿來(lái)主義”的沖動(dòng):直接使用系統(tǒng)里已經(jīng)存在的屬性名就好了嘛插佛,多“原生”杠巡!既然有這種“邪惡”的需求,那Google工程師自然是要滿(mǎn)足滴(~ ̄▽?zhuān)?~雇寇。
以gravity
屬性為例氢拥,我們只要在declare-styleable
里直接寫(xiě)上<attr name="android:gravity"/>
即可蚌铜,不過(guò)這里要注意的是不需要也不能再加上format
屬性,加上format
屬性就代表著這是在聲明一個(gè)新的屬性嫩海,不加則代表這是在使用已存在的一個(gè)屬性冬殃。
3.1.2 使用屬性
使用屬性就比較簡(jiǎn)單了:
<wang.mogujun.widget.FlowLayout
android:id="@+id/flow2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="#6A6A6A"
android:gravity="start"
android:padding="8dp"
app:horizonSpacing="8dp"
app:verticalSpacing="12dp"
app:dividerColor="#cccccc"
app:dividerWidth="2dp"
>
3.1.3 解析并獲取屬性
在xml設(shè)置了相應(yīng)的屬性后,就需要在FlowLayout里解析并獲取屬性值了:
public static final int DEFAULT_SPACING = 8;
public static final int DEFAULT_DIVIDER_COLOR = Color.parseColor("#ececec");
public static final int DEFAULT_DIVIDER_WIDTH = 3;
private int mGravity = (isIcs() ? Gravity.START : Gravity.LEFT) | Gravity.TOP;
private int mVerticalSpacing; //vertical spacing
private int mHorizontalSpacing; //horizontal spacing
private int mDividerColor;
private int mDividerWidth;
private void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.FlowLayout, defStyleAttr, defStyleRes);
try {
mHorizontalSpacing = (int) ta.getDimension(R.styleable.FlowLayout_horizonSpacing, DEFAULT_SPACING);
mVerticalSpacing = (int) ta.getDimension(R.styleable.FlowLayout_verticalSpacing, DEFAULT_SPACING);
mDividerWidth = (int) ta.getDimension(R.styleable.FlowLayout_dividerWidth, DEFAULT_DIVIDER_WIDTH);
mDividerColor = ta.getColor(R.styleable.FlowLayout_dividerColor, DEFAULT_DIVIDER_COLOR);
int index = ta.getInt(R.styleable.FlowLayout_android_gravity, -1);
if (index > 0) {
setGravity(index);
}
initPaint();
} finally {
ta.recycle();
}
setWillNotDraw(false);
}
一般來(lái)說(shuō)叁怪,我們的自定義屬性都得給個(gè)默認(rèn)值审葬,大家都這么懶,不能強(qiáng)人所難對(duì)不對(duì)奕谭。這默認(rèn)值可以通過(guò)常量直接寫(xiě)在自定義類(lèi)里涣觉,如上述代碼所示。也可以寫(xiě)在xml資源文件里血柳,提供給別人統(tǒng)一修改官册。
其次呢,英明神武的蘑菇君自然也得提供方法讓別人方便的通過(guò)代碼去動(dòng)態(tài)修改這些屬性啦(真不要臉~~( ﹁ ﹁ ) ~~~):
public void setHorizontalSpacing(int pixelSize) {
mHorizontalSpacing = pixelSize;
requestLayout();
}
public void setVerticalSpacing(int pixelSize) {
mVerticalSpacing = pixelSize;
requestLayout();
}
public void setDividerColor(@ColorInt int color) {
mDividerColor = color;
mDividerPaint.setColor(color);
invalidate();
}
...
關(guān)于自定義屬性的一些詳細(xì)知識(shí)可以參考文章: Android 深入理解Android中的自定義屬性
3.2 測(cè)量
在自定義ViewGroup時(shí)难捌,測(cè)量流程一般是所有流程中最為復(fù)雜的一環(huán)膝宁。因?yàn)槲覀儾粌H要測(cè)量ViewGroup自身的尺寸,還得測(cè)量所有孩子的尺寸栖榨。而ViewGroup和孩子們之間的尺寸又是相互影響的昆汹。
如下圖所示,在我們的FlowLayout里婴栽,當(dāng)寬的測(cè)量模式為AT_MOST
(比如FlowLayout的布局屬性android:layout_width
為wrap_content
時(shí))满粗,F(xiàn)lowLayout的測(cè)量寬度應(yīng)該是所有行里最長(zhǎng)的那一行的寬度,在下圖中就是第二行的寬度愚争。而當(dāng)高的測(cè)量模式為AT_MOST
映皆,F(xiàn)lowLayout的測(cè)量高度應(yīng)該是所有行的高度總和。
而對(duì)于child view來(lái)說(shuō)轰枝,也有個(gè)小小的限制:當(dāng)FlowLayout的layout_height
為wrap_content
捅彻,而child的layout_height
為match_parent
時(shí),我希望child的測(cè)量高為它所處那一行的高度鞍陨,而不是整個(gè)FlowLayout的高度或者是wrap_content
步淹。這也挺合情合理的吧,比如下圖中第一行的child 再見(jiàn)這群坑比
的layout_height
為match_parent
诚撵,所以它就和第一行的高度一樣高缭裆。
可能說(shuō)得大家都有點(diǎn)暈了X﹏X,還是來(lái)一起看看onMeasure
方法的源碼吧:
//保存所有child view
private final List<List<View>> mLines = new ArrayList<>();
//保存所有行高
private final List<Integer> mLineHeights = new ArrayList<>();
//保存所有行寬
private final List<Integer> mLineWidths = new ArrayList<>();
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
mLines.clear();
mLineHeights.clear();
mLineWidths.clear();
int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
int modeHeight = MeasureSpec.getMode(heightMeasureSpec);
int widthUsed = getPaddingLeft() + getPaddingRight() + mHorizontalSpacing;
int lineWidth = widthUsed;
int lineHeight = 0;
int childCount = getChildCount();
List<View> lineViews = new ArrayList<>();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child.getVisibility() == View.GONE) {
continue;
}
LayoutParams lp = (LayoutParams) child.getLayoutParams();
//測(cè)量每個(gè)child的寬高寿烟,每個(gè)child可用的最大寬高為sizeWidth-spacing-padding-margin
measureChildWithMargins(child, widthMeasureSpec, mHorizontalSpacing * 2, heightMeasureSpec, mVerticalSpacing * 2);
int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
//判斷這一行是否還能容下這個(gè)child
if (lineWidth + childWidth + mHorizontalSpacing > sizeWidth) {
//需要換行澈驼,則記錄這一行的寬度,高度筛武,下一行的初始寬度缝其,初始高度
mLineWidths.add(lineWidth);
lineWidth = widthUsed + childWidth + mHorizontalSpacing;
mLineHeights.add(lineHeight);
lineHeight = childHeight;
mLines.add(lineViews);
lineViews = new ArrayList<>();
} else {//容得下挎塌,則累加這一行的寬度,記錄這一行的高度
lineWidth += childWidth + mHorizontalSpacing;
lineHeight = Math.max(lineHeight, childHeight);
}
lineViews.add(child);
}
//最后一行的處理
mLineHeights.add(lineHeight);
mLineWidths.add(lineWidth);
mLines.add(lineViews);
int maxWidth = Collections.max(mLineWidths);
processChildHeights();//計(jì)算所有行的累積高度
int totalHeight = getChildHeights();
//TODO 處理getMinimumWidth/height的情況
//設(shè)置自身的測(cè)量寬高
setMeasuredDimension(
(modeWidth == MeasureSpec.EXACTLY) ? sizeWidth : Math.min(maxWidth, sizeWidth),
(modeHeight == MeasureSpec.EXACTLY) ? sizeHeight : Math.min(totalHeight, sizeHeight));
//重新測(cè)量child的lp.height為MATCH_PARENT時(shí)的child的尺寸
remeasureChild(widthMeasureSpec);
}
上面的代碼邏輯都有注釋?zhuān)嘈糯蠹叶寄芾砬宕蟾诺倪壿嬆诒摺簳r(shí)沒(méi)理解也沒(méi)關(guān)系榴都,稍后自己去看代碼再加上自己的思考肯定能看懂滴。(蘑菇君自我感覺(jué)腦子轉(zhuǎn)的算慢的假残,看Github上的FlowLayout源碼花了蠻久時(shí)間才弄懂大概邏輯缭贡,自己畫(huà)圖呀,運(yùn)行demo呀,弄懂了以后辉懒,才開(kāi)始自己動(dòng)手寫(xiě)自己的FlowLayout...(??????)??)
這里要特別注意的是對(duì)children的測(cè)量過(guò)程阳惹。在上面的代碼中,我使用了ViewGroup類(lèi)里提供的measureChildWithMargins
方法去測(cè)量每個(gè)child眶俩,對(duì)這個(gè)方法的具體剖析莹汤,可以去看自定義控件知識(shí)儲(chǔ)備-View的繪制流程,這篇文章講的很詳細(xì)颠印。但在上文中有提到過(guò)纲岭,我們對(duì)child有個(gè)限制:
當(dāng)child的
layout_height
為match_parent
時(shí),child的測(cè)量高為它所處那一行的高度线罕,而不是整個(gè)FlowLayout的高度或者是wrap_content
止潮。
但是這個(gè)child所處那一行的高度是那一行所有child的高度的最大值,所以只有在完成這一行所有child的測(cè)量后钞楼,才知道這一行的高度是多少喇闸。所以上面的要求無(wú)法滿(mǎn)足呀!我在測(cè)量該child的高度的時(shí)候询件,還不知道這一行的高度是多少叭颊А!
該怎么辦呢宛琅?其實(shí)也簡(jiǎn)單刻蟹,既然當(dāng)時(shí)測(cè)量某child的時(shí)候還不知道那一行的高度,那就在第一次所有child都測(cè)量完成后嘿辟,再對(duì)那些layout_height
為match_parent
的child測(cè)量一遍就好啦舆瘪。所以在上面onMeasure
方法里的最后調(diào)用了remeasureChild
這個(gè)方法去重新測(cè)量一遍child:
private void remeasureChild(int parentWidthSpec) {
int numLines = mLines.size();
for (int i = 0; i < numLines; i++) {//遍歷每一行
int lineHeight = mLineHeights.get(i);
List<View> lineViews = mLines.get(i);
int children = lineViews.size();
for (int j = 0; j < children; j++) {
View child = lineViews.get(j);
LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (lp.height == LayoutParams.MATCH_PARENT) {//對(duì)高為match_parent的child進(jìn)行處理
if (child.getVisibility() == View.GONE) {
continue;
}
int widthUsed = lp.leftMargin + lp.rightMargin +
getPaddingLeft() + getPaddingRight() + 2 * mHorizontalSpacing;
//再次調(diào)用child的measure方法進(jìn)行測(cè)量
child.measure(
getChildMeasureSpec(parentWidthSpec, widthUsed, lp.width),
MeasureSpec.makeMeasureSpec(lineHeight - lp.topMargin - lp.bottomMargin, MeasureSpec.EXACTLY)
);
}
}
}
}
從這里我們也看得出來(lái),一個(gè)View的onMeasure
方法是很有可能被調(diào)用多次來(lái)確定最終的測(cè)量寬高的红伦,所以下次遇到打印日志里或者斷點(diǎn)調(diào)試下發(fā)現(xiàn) onMeasure
方法多次運(yùn)行介陶,莫要方呀o( ̄??)。
3.3 布局
布局過(guò)程呢色建,就稍微簡(jiǎn)單一些,因?yàn)槲覀冊(cè)?code>onMeasure方法里已經(jīng)將所有child的寬高和位于哪一行等信息都計(jì)算好了舌缤,只要遍歷children調(diào)用它們的layout
方法放置好它們就行箕戳。不過(guò)這里有點(diǎn)麻煩的就是某残,我們需要支持FlowLayout自身的gravity屬性和children的 gravity屬性。那就得根據(jù)具體的gravity來(lái)計(jì)算相應(yīng)的偏移量了陵吸,代碼如下:
//根據(jù)gravity計(jì)算FlowLayout的垂直方向上的偏移量
private void processVerticalGravityMargin() {
int verticalGravityMargin;
int childHeights = getChildHeights();
switch ((mGravity & Gravity.VERTICAL_GRAVITY_MASK)) {
case Gravity.TOP://頂部
default:
verticalGravityMargin = 0;
break;
case Gravity.CENTER_VERTICAL://垂直居中
verticalGravityMargin = Math.max((getHeight() - childHeights) / 2, 0);
break;
case Gravity.BOTTOM://底部
verticalGravityMargin = Math.max(getHeight() - childHeights, 0);
break;
}
mVerticalGravityMargin = verticalGravityMargin;
}
//根據(jù)gravity計(jì)算FlowLayout的水平方向上的偏移量
private void processHorizontalGravityMargins() {
mLineMargins.clear();
float horizontalGravityFactor;
switch ((mGravity & Gravity.HORIZONTAL_GRAVITY_MASK)) {
case Gravity.LEFT://水平靠左
default:
horizontalGravityFactor = 0;
break;
case Gravity.CENTER_HORIZONTAL://水平居中
horizontalGravityFactor = .5f;
break;
case Gravity.RIGHT://水平靠右
horizontalGravityFactor = 1;
break;
}
int linesNum = mLineWidths.size();
for (int i = 0; i < linesNum; i++) {
int lineWidth = mLineWidths.get(i);
mLineMargins.add((int) ((getWidth() - lineWidth) * horizontalGravityFactor) + getPaddingLeft() + mHorizontalSpacing);
}
}
給FlowLayout設(shè)置gravity的效果如下:
內(nèi)容居中:
內(nèi)容在右下角:
計(jì)算好了每行的偏移量后玻墅,layout
方法的邏輯就很清晰了:
protected void onLayout(boolean changed, int l, int t, int r, int b) {
processHorizontalGravityMargins();
processVerticalGravityMargin();
int numLines = mLines.size();
int left;
int top = getPaddingTop() + mVerticalSpacing + mVerticalGravityMargin;
for (int i = 0; i < numLines; i++) {
int lineHeight = mLineHeights.get(i);
List<View> lineViews = mLines.get(i);
left = mLineMargins.get(i);
int children = lineViews.size();
for (int j = 0; j < children; j++) {
View child = lineViews.get(j);
if (child.getVisibility() == View.GONE) {
continue;
}
LayoutParams lp = (LayoutParams) child.getLayoutParams();
int childWidth = child.getMeasuredWidth();
int childHeight = child.getMeasuredHeight();
int gravityMargin = 0;
//根據(jù)child的gravity計(jì)算child的相應(yīng)偏移量
if (Gravity.isVertical(lp.gravity)) {
switch (lp.gravity) {
case Gravity.TOP:
default:
gravityMargin = 0;
break;
case Gravity.CENTER_VERTICAL:
case Gravity.CENTER:
gravityMargin = (lineHeight - childHeight - lp.topMargin - lp.bottomMargin) / 2;
break;
case Gravity.BOTTOM:
gravityMargin = lineHeight - childHeight - lp.topMargin - lp.bottomMargin;
break;
//TODO 水平方向上可以支持gravity么?
}
}
child.layout(left + lp.leftMargin,
top + lp.topMargin + gravityMargin,
left + lp.leftMargin + childWidth,
top + lp.topMargin + gravityMargin + childHeight);
Log.i(TAG, String.format("child (%d,%d) position: (%d,%d,%d,%d)",
i, j, child.getLeft(), child.getTop(), child.getRight(), child.getBottom()));
left += childWidth + lp.leftMargin + lp.rightMargin + mHorizontalSpacing;
}
top += lineHeight + mVerticalSpacing;
}
}
3.4 繪制
本FlowLayout支持繪制分割線(xiàn)壮虫,這也是很容易的繪制澳厢,只要找準(zhǔn)每條分割線(xiàn)的位置就行。不過(guò)萬(wàn)變不離其宗嘛囚似,我現(xiàn)在能畫(huà)一條線(xiàn)剩拢,下次就能畫(huà)一個(gè)圓,再下次就能畫(huà)個(gè)雞蛋饶唤,再再下次我就能飛上天徐伐,畫(huà)出太陽(yáng)肩并肩...∧伎瘢咳咳办素,扯遠(yuǎn)了,我們還是來(lái)看看onDraw
方法里的繪制邏輯:
@Override
protected void onDraw(Canvas canvas) {
int top = getPaddingTop() + mVerticalSpacing + mVerticalGravityMargin;
int numLines = mLines.size();
for (int i = 0; i < numLines; i++) {
int lineHeight = mLineHeights.get(i);
top += lineHeight + mVerticalSpacing;
canvas.drawLine(getPaddingLeft(), top - mVerticalSpacing / 2,
getWidth() - getPaddingRight(), top - mVerticalSpacing / 2, mDividerPaint);
}
}
確實(shí)很簡(jiǎn)單祸穷,遍歷每一行性穿,在兩行的中間根據(jù)配置的顏色和寬度畫(huà)出一條線(xiàn)段即可。
不過(guò)這里要注意View的一個(gè)特殊方法:setWillNotDraw
雷滚,來(lái)看一下這個(gè)方法的源碼:
/**
* If this view doesn't do any drawing on its own, set this flag to
* allow further optimizations. By default, this flag is not set on
* View, but could be set on some View subclasses such as ViewGroup.
*
* Typically, if you override {@link #onDraw(android.graphics.Canvas)}
* you should clear this flag.
*
* @param willNotDraw whether or not this View draw on its own
*/
public void setWillNotDraw(boolean willNotDraw) {
setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}
從這個(gè)方法的注釋中可以看出需曾,如果一個(gè)View不需要繪制任何內(nèi)容,那么設(shè)置這個(gè)標(biāo)記位為true后揭措,系統(tǒng)會(huì)進(jìn)行相應(yīng)的優(yōu)化胯舷。默認(rèn)情況下,View沒(méi)有啟用這個(gè)優(yōu)化標(biāo)記位绊含,而ViewGroup會(huì)默認(rèn)啟用這個(gè)標(biāo)記位桑嘶。
當(dāng)我們的自定義ViewGroup需要通過(guò)重寫(xiě)
onDraw
來(lái)繪制內(nèi)容時(shí),我們需要顯式地關(guān)閉WILL_NOT_DRAW這個(gè)標(biāo)記位躬充。
所以逃顶,在這個(gè)FlowLayout的構(gòu)造方法里,我們可以調(diào)用setWillNotDraw(false)
來(lái)進(jìn)行優(yōu)化充甚。
3.5 處理LayoutParams
幾乎每個(gè)自定義ViewGroup都得自定義自己的LayoutParams以政,來(lái)給children提供更好的服務(wù)。在本FlowLayout里伴找,能給children帶來(lái)的就是gravity屬性的支持盈蛮。來(lái)看看自定義的LayoutParams:
public static class LayoutParams extends MarginLayoutParams {
public int gravity = -1;
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.FlowLayout_Layout);
try {
gravity = a.getInt(R.styleable.FlowLayout_Layout_android_layout_gravity, -1);
} finally {
a.recycle();
}
}
public LayoutParams(int width, int height) {
super(width, height);
gravity = Gravity.TOP;
}
public LayoutParams(int width, int height, int gravity) {
super(width, height);
this.gravity = gravity;
}
public LayoutParams(ViewGroup.LayoutParams source) {
super(source);
}
}
同時(shí),F(xiàn)lowLayout還需要對(duì)以下幾個(gè)方法進(jìn)行重寫(xiě):
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return super.checkLayoutParams(p) && p instanceof LayoutParams;
}
@Override
protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
return new LayoutParams(p);
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
啥技矮?不知道為啥要按上述代碼那樣做抖誉?那是時(shí)候去看看自定義控件知識(shí)儲(chǔ)備-LayoutParams的那些事了殊轴。看完了你就大徹大悟袒炉,遁入......咳咳旁理。
3. 展示
哎呀呀,這篇文章已經(jīng)夠長(zhǎng)了我磁,我就不貼資源文件孽文,截圖等東西啦,大家有需要的話(huà)夺艰,可以去Github上下載源碼進(jìn)行學(xué)習(xí)芋哭。
Github地址: https://github.com/yisizhu520/FlowLayout
PS:蘑菇君寫(xiě)的這個(gè)FlowLayout肯定還存在bug,而且我自己也知道幾個(gè)不影響使用的小bug劲适,但是我沒(méi)有去改楷掉,等待有緣人去發(fā)現(xiàn)哈(≧?≦)?。
也歡迎大家去提交issue和pull request霞势,一起交流烹植,一起進(jìn)步龙优。
4. 總結(jié)
終于寫(xiě)完這篇博客了溯香,真是寫(xiě)死我了?(T?T)。希望這篇文章除了能加深自己對(duì)自定義ViewGroup的理解外赃梧,還能幫助到大家固以。以前一直以為自己了解了自定義ViewGroup的一些知識(shí)墩虹,想要寫(xiě)一個(gè)容器控件出來(lái)應(yīng)該不難的。然而憨琳,紙上得來(lái)終覺(jué)淺诫钓,當(dāng)自己真的開(kāi)始寫(xiě)的時(shí)候,發(fā)現(xiàn)滿(mǎn)滿(mǎn)的都是細(xì)節(jié)篙螟,滿(mǎn)滿(mǎn)的都是套路菌湃。比如在FlowLayout里的測(cè)量、布局遍略、繪制都得考慮到間距的問(wèn)題惧所,什么margin啊,padding啊绪杏,spacing啊下愈,都需要小心對(duì)待。不過(guò)蕾久,最終還是在不斷的調(diào)試和修改中寫(xiě)出來(lái)了這個(gè)FlowLayout势似,想想還有點(diǎn)小激動(dòng)呢!以后要做的應(yīng)該就是不斷的練習(xí)和總結(jié),畢竟編程這件事履因,沒(méi)啥好說(shuō)的辖佣,just code it!
我是蘑菇君搓逾,我為自己帶鹽