又到了美好周末時間蒜鸡,由于更新博客的時間總是斷斷續(xù)續(xù),突然有個想法牢裳,想對博客進(jìn)行每十天一更逢防,不知道能不能合理的安排出時間來,嘗試著做看看吧蒲讯。
由于公司產(chǎn)品的迭代更新忘朝,這次UI設(shè)計師做出了個類似簡書熱門標(biāo)簽頁的效果,雖然谷歌官方已經(jīng)提供了一個很友好的彈性布局FlexboxLayout傳送門可以幫助我們實現(xiàn)這一效果判帮,但是強(qiáng)迫癥的我還是想要自己實現(xiàn)一波局嘁。
首先先來看下簡書上的實現(xiàn)效果:
下面是我自己仿寫的效果:
實現(xiàn)思路:
首先我們可以把這一標(biāo)簽頁的整體看成一個容器,然后容器內(nèi)有許多小控件(TextView晦墙,Button导狡,ImageView等),再來這些小控件成水平排列并且會根據(jù)自身的布局大小來決定是否換行偎痛。
我們很容易可以聯(lián)想到:
容器:自定義ViewGroup
小控件:原生自帶的控件
水平排列并根據(jù)自身大小換行:需要測量比對旱捧,布置位置
關(guān)于ViewGroup:
關(guān)于測量:
由于在容器ViewGroup里裝載的是原生控件(TextView,Button,ImageView等)枚赡,所以對于原生控件的屬性(內(nèi)部對齊方式氓癌,縮放方式,內(nèi)邊距等)我們不需要去做另外的處理贫橙,但這里涉及到了一個控件與控件之間的距離(外邊距margin)贪婉,所以我們需要讓ViewGroup去認(rèn)識這個標(biāo)簽,在原生的ViewGroup里是不支持margin的卢肃,ViewGroup里有兩個內(nèi)部類分別是ViewGroup.LayoutParams和ViewGroup. MarginLayoutParams疲迂,ViewGroup. MarginLayoutParams繼承于ViewGroup.LayoutParams也擴(kuò)展了支持的屬性,也就是magin屬性莫湘,所以我們需要重寫generateLayoutParams方法讓其返回MarginLayoutParams對象
/**
* 指定ViewGroup的LayoutParams
*
* @param attrs
* @return
*/
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
再來就是關(guān)于測量了尤蒿,在ViewGroup里測量是在onMeasure方法里實現(xiàn)的,由于LinearLayout和RelativeLayout等這些布局是繼承于ViewGroup的幅垮,這些布局?jǐn)[放控件的方式是不一樣的腰池,比如LinearLayout是呈線性擺放的,RelativeLayout是呈疊加擺放的忙芒,所以測量的方式也是不一樣的示弓,所以ViewGroup并沒有給我們做好子View的測量工作,而是讓我們?nèi)ブ貙憃nMeasure方法進(jìn)行測量呵萨,一個ViewGroup除非指定它的寬高精確值或者讓其充滿match_parent奏属,否則它是不知道自身大小應(yīng)該是多少的,所以我們需要對包含在ViewGroup里的子View進(jìn)行逐個測量潮峦,然后累加寬高來確定ViewGroup的寬高值拍皮。
/**
* Measure specification mode: The parent has not imposed any constraint
* on the child. It can be whatever size it wants.
*/
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
/**
* Measure specification mode: The parent has determined an exact size
* for the child. The child is going to be given those bounds regardless
* of how big it wants to be.
*/
public static final int EXACTLY = 1 << MODE_SHIFT;
/**
* Measure specification mode: The child can be as large as it wants up
* to the specified size.
*/
public static final int AT_MOST = 2 << MODE_SHIFT;
谷歌官方給我們提供了三種測量模式:
EXACTLY:精確大小,可以讓ViewGroup寬高為我們的指定值或者是充滿match_parent
AT_MOST:給出限定大小跑杭,子View可以在ViewGroup的允許范圍內(nèi)伸展大小
UNSPECIFIED:不指定限制,子View想要多大就給多大(幾乎很少使用)
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
我們可以在onMeasure方法里通過MeasureSpec.getMode和MeasureSpec.getSize得到對應(yīng)的測量模式和測量尺寸咆耿,當(dāng)模式為EXACTLY的時候德谅,我們就可以直接應(yīng)用所測量的尺寸,但如果是其他模式萨螺,那么我們就只能自己去測量子View的尺寸了窄做。
關(guān)于布局:
@Override
protected abstract void onLayout(boolean changed,
int l, int t, int r, int b);
在自定義ViewGroup中提供了一個抽象方法onLayout需要我們對其實現(xiàn),在這里我們可以對子View的位置進(jìn)行確定排列慰技,ViewGroup通過onLayout 方法來確定View在容器中的位置椭盏,View通過layout方法來確認(rèn)自己在父容器中的位置,l吻商,t掏颊,r,b參數(shù)分別代表left,top乌叶,right盆偿,bottom,也就是左上和右下准浴。
好了事扭,由于篇幅的限制,有些東西不能講的太細(xì)乐横,有不清楚的朋友自行查閱相關(guān)資料補(bǔ)充吧求橄。
具體實現(xiàn):
我們開始來分析下今天要實現(xiàn)的效果:
1、首先我們需要去自定義一個ViewGroup葡公,然后再其內(nèi)包含各種子控件(這里是多個TextView)罐农,就好比LinearLayout包含子控件一樣。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.lcw.view.flowlayout.FlowLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:background="@drawable/shape_tv_blue"
android:padding="5dp"
android:text="歐美影視"
android:textColor="@color/colorAccent" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:background="@drawable/shape_tv_blue"
android:padding="5dp"
android:text="婚姻育兒"
android:textColor="@color/colorAccent" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:background="@drawable/shape_tv_blue"
android:padding="5dp"
android:text="散文"
android:textColor="@color/colorAccent" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:background="@drawable/shape_tv_blue"
android:padding="5dp"
android:text="程序員"
android:textColor="@color/colorAccent" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:background="@drawable/shape_tv_blue"
android:padding="5dp"
android:text="大學(xué)生生活"
android:textColor="@color/colorAccent" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:background="@drawable/shape_tv_blue"
android:padding="5dp"
android:text="運(yùn)營互助幫"
android:textColor="@color/colorAccent" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:background="@drawable/shape_tv_blue"
android:padding="5dp"
android:text="設(shè)計"
android:textColor="@color/colorAccent" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:background="@drawable/shape_tv_blue"
android:padding="5dp"
android:text="讀書"
android:textColor="@color/colorAccent" />
</com.lcw.view.flowlayout.FlowLayout>
</LinearLayout>
2匾南、然后我們在onMeasure里通過getChildCount拿到子View的個數(shù)啃匿,并利用measureChild對子View進(jìn)行遍歷測量,通過測量寬高的累加后得到再通過setMeasuredDimension設(shè)置ViewGroup的寬高蛆楞。
/**
* 測量所有子View大小,確定ViewGroup的寬高
*
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//由于onMeasure會執(zhí)行多次,避免重復(fù)的計算控件個數(shù)和高度,這里需要進(jìn)行清空操作
mLineViews.clear();
mLineHeight.clear();
//獲取測量的模式和尺寸大小
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
//記錄ViewGroup真實的測量寬高
int viewGroupWidth = 0;
int viewGroupHeight = 0;
if (widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY) {
viewGroupWidth = widthSize;
viewGroupHeight = heightSize;
} else {
//當(dāng)前所占的寬高
int currentLineWidth = 0;
int currentLineHeight = 0;
//用來存儲每一行上的子View
List<View> lineView = new ArrayList<View>();
int childViewsCount = getChildCount();
for (int i = 0; i < childViewsCount; i++) {
View childView = getChildAt(i);
//對子View進(jìn)行測量
measureChild(childView, widthMeasureSpec, heightMeasureSpec);
MarginLayoutParams marginLayoutParams = (MarginLayoutParams) childView.getLayoutParams();
int childViewWidth = childView.getMeasuredWidth() + marginLayoutParams.leftMargin + marginLayoutParams.rightMargin;
int childViewHeight = childView.getMeasuredHeight() + marginLayoutParams.topMargin + marginLayoutParams.bottomMargin;
if (currentLineWidth + childViewWidth > widthSize) {
//當(dāng)前行寬+子View+左右外邊距>ViewGroup的寬度,換行
viewGroupWidth = Math.max(currentLineWidth, widthSize);
viewGroupHeight += currentLineHeight;
//添加行高
mLineHeight.add(currentLineHeight);
//添加行對象
mLineViews.add(lineView);
//new新的一行
lineView = new ArrayList<View>();
//添加行對象里的子View
lineView.add(childView);
currentLineWidth = childViewWidth;
} else {
//當(dāng)前行寬+子View+左右外邊距<=ViewGroup的寬度,不換行
currentLineWidth += childViewWidth;
currentLineHeight = Math.max(currentLineHeight, childViewHeight);
//添加行對象里的子View
lineView.add(childView);
}
if (i == childViewsCount - 1) {
//最后一個子View的時候
//添加行對象
mLineViews.add(lineView);
viewGroupWidth = Math.max(childViewWidth, viewGroupWidth);
viewGroupHeight += childViewHeight;
//添加行高
mLineHeight.add(currentLineHeight);
}
}
}
setMeasuredDimension(viewGroupWidth, viewGroupHeight);
}
3溯乒、然后我們在onLayout里去設(shè)置子View所在的位置。
/**
* 設(shè)置ViewGroup里子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 left = 0;
int top = 0;
//一共有幾行
int lines = mLineViews.size();
for (int i = 0; i < lines; i++) {
//每行行高
int lineHeight = mLineHeight.get(i);
//行內(nèi)有幾個子View
List<View> viewList = mLineViews.get(i);
int views = viewList.size();
for (int j = 0; j < views; j++) {
View view = viewList.get(j);
MarginLayoutParams marginLayoutParams = (MarginLayoutParams) view.getLayoutParams();
int vl = left + marginLayoutParams.leftMargin;
int vt = top + marginLayoutParams.topMargin;
int vr = vl + view.getMeasuredWidth();
int vb = vt + view.getMeasuredHeight();
view.layout(vl, vt, vr, vb);
left += view.getMeasuredWidth() + marginLayoutParams.leftMargin + marginLayoutParams.rightMargin;
}
left = 0;
top += lineHeight;
}
}
說明:
1豹爹、List<List<View>>對象裆悄,View表示子View,List<View>表示每行有多少子View臂聋,List<List<View>>表示有幾行光稼。
2、List<Integer>對象孩等,用來記錄每行的高度艾君。
3、在onMeasure測量方法中肄方,我們在測量每個子View的寬度時把寬度進(jìn)行累加(包含子View的左右外邊距)冰垄,當(dāng)累加的寬度比最大寬度寬時,需要進(jìn)行換行权她,同時在測量每個子View的時候需要記錄它的高度虹茶,行高取最大高度的那個子View。
4隅要、這里還可以做一些擴(kuò)展的補(bǔ)充蝴罪,比如設(shè)置標(biāo)簽的點擊監(jiān)聽事件,解決ViewGroup內(nèi)邊距等步清,隨后會在Github庫上更新要门。
源碼下載:
這里附上源碼地址(歡迎Star,歡迎Fork):源碼下載