由于公司產(chǎn)品的迭代更新二汛,這次UI設計師做出了個類似簡書熱門標簽頁的效果,雖然谷歌官方已經(jīng)提供了一個很友好的彈性布局FlexboxLayout傳送門可以幫助我們實現(xiàn)這一效果奇钞,但是強迫癥的我還是想要自己實現(xiàn)一波。
首先先來看下簡書上的實現(xiàn)效果:
下面是我自己仿寫的效果:
實現(xiàn)思路:
首先我們可以把這一標簽頁的整體看成一個容器汁蝶,然后容器內(nèi)有許多小控件(TextView晒杈,Button,ImageView等)方椎,再來這些小控件成水平排列并且會根據(jù)自身的布局大小來決定是否換行。
我們很容易可以聯(lián)想到:
容器:自定義ViewGroup
小控件:原生自帶的控件
水平排列并根據(jù)自身大小換行:需要測量比對钧嘶,布置位置
關于ViewGroup:
關于測量:
由于在容器ViewGroup里裝載的是原生控件(TextView棠众,Button,ImageView等)有决,所以對于原生控件的屬性(內(nèi)部對齊方式闸拿,縮放方式,內(nèi)邊距等)我們不需要去做另外的處理书幕,但這里涉及到了一個控件與控件之間的距離(外邊距margin)新荤,所以我們需要讓ViewGroup去認識這個標簽,在原生的ViewGroup里是不支持margin的台汇,ViewGroup里有兩個內(nèi)部類分別是ViewGroup.LayoutParams和ViewGroup. MarginLayoutParams苛骨,ViewGroup. MarginLayoutParams繼承于ViewGroup.LayoutParams也擴展了支持的屬性篱瞎,也就是magin屬性,所以我們需要重寫generateLayoutParams方法讓其返回MarginLayoutParams對象
/**
* 指定ViewGroup的LayoutParams
*
* @param attrs
* @return
*/
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
再來就是關于測量了痒芝,在ViewGroup里測量是在onMeasure方法里實現(xiàn)的俐筋,由于LinearLayout和RelativeLayout等這些布局是繼承于ViewGroup的,這些布局擺放控件的方式是不一樣的严衬,比如LinearLayout是呈線性擺放的澄者,RelativeLayout是呈疊加擺放的,所以測量的方式也是不一樣的请琳,所以ViewGroup并沒有給我們做好子View的測量工作粱挡,而是讓我們?nèi)ブ貙憃nMeasure方法進行測量,一個ViewGroup除非指定它的寬高精確值或者讓其充滿match_parent俄精,否則它是不知道自身大小應該是多少的询筏,所以我們需要對包含在ViewGroup里的子View進行逐個測量,然后累加寬高來確定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得到對應的測量模式和測量尺寸灌危,當模式為EXACTLY的時候,我們就可以直接應用所測量的尺寸碳胳,但如果是其他模式勇蝙,那么我們就只能自己去測量子View的尺寸了。
關于布局:
@Override
protected abstract void onLayout(boolean changed,
int l, int t, int r, int b);
在自定義ViewGroup中提供了一個抽象方法onLayout需要我們對其實現(xiàn)挨约,在這里我們可以對子View的位置進行確定排列味混,ViewGroup通過onLayout 方法來確定View在容器中的位置,View通過layout方法來確認自己在父容器中的位置诫惭,l翁锡,t,r夕土,b參數(shù)分別代表left馆衔,top,right怨绣,bottom角溃,也就是左上和右下。
好了篮撑,由于篇幅的限制减细,有些東西不能講的太細,有不清楚的朋友自行查閱相關資料補充吧赢笨。
具體實現(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="大學生生活"
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" />
</com.lcw.view.flowlayout.FlowLayout>
</LinearLayout>
2糠馆、然后我們在onMeasure里通過getChildCount拿到子View的個數(shù),并利用measureChild對子View進行遍歷測量怎憋,通過測量寬高的累加后得到再通過setMeasuredDimension設置ViewGroup的寬高又碌。
/**
* 測量所有子View大小,確定ViewGroup的寬高
*
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//由于onMeasure會執(zhí)行多次,避免重復的計算控件個數(shù)和高度,這里需要進行清空操作
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 {
//當前所占的寬高
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進行測量
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) {
//當前行寬+子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 {
//當前行寬+子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里去設置子View所在的位置绊袋。
/**
* 設置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的寬度時把寬度進行累加(包含子View的左右外邊距),當累加的寬度比最大寬度寬時擂达,需要進行換行土铺,同時在測量每個子View的時候需要記錄它的高度,行高取最大高度的那個子View板鬓。
4悲敷、這里還可以做一些擴展的補充,比如設置標簽的點擊監(jiān)聽事件俭令,解決ViewGroup內(nèi)邊距等后德,隨后會在Github庫上更新。
源碼下載:
這里附上源碼地址(歡迎Star抄腔,歡迎Fork):源碼下載