實現流式佈局FlowLayout
我在拉勾網App上搜索公司或者職位的下方發現一個效果
拉勾網這些顯示的具體數據怎麼來的我們不討論,我們試着來實現一下它的這個佈局效果。
處於上方的Tag“猜你喜歡”、“熱門公司”可以用一個TextView顯示,我們忽略它。關鍵是下方的標籤流式佈局。我們就來分析它。
- 首先流式佈局中的標籤應該是個TextView,關於它下方的橢圓形邊界,我們可以爲其制定background
layout/tag_view.xml
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:background="@drawable/tag_bg"
android:text="Helloworld"
android:textSize="15sp"
android:textColor="@drawable/text_color">
</TextView>
drawable/tag_bg.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:drawable="@drawable/checked_bg"
android:state_checked="true"
>
</item>
<item
android:drawable="@drawable/normal_bg"></item>
</selector>
drawable/checked_bg.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#88888888"/>
<corners android:radius="30dp"/>
<padding
android:bottom="2dp"
android:left="10dp"
android:right="10dp"
android:top="2dp"/>
</shape>
drawable/normal_bg.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" >
<solid android:color="#ffffff" />
<corners android:radius="30dp" />
<stroke android:color="#88888888" android:width="1dp"/>
<padding
android:bottom="2dp"
android:left="10dp"
android:right="10dp"
android:top="2dp" />
</shape>
drawable/text_color.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="#888888"/>
</selector>
上方佈局可得到如下預覽
至此我們的準備工作已經完畢。
- 自定義ViewGroup(重點)
上面我們已經得到了一個佈局文件達到了我們流式佈局中的子View的顯示效果。那我們下面就來自定義ViewGroup來實現上述的流式佈局。
① 首先繼承自ViewGroup,繼承自ViewGroup重寫其構造函數以及onLayout方法,我們使用AndroidStudio提示就行了
public class MyTagFlowLayout extends ViewGroup {
public MyTagFlowLayout(Context context) {
this(context, null);
}
public MyTagFlowLayout(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public MyTagFlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs,defStyleAttr,0);
}
@SuppressLint("NewApi")
public MyTagFlowLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
}
}
② 初始化一些信息
//由上圖可知,我們可將上面的流式佈局分爲三部分
//每一行的View 組成的List
private List<View> lineViews = new ArrayList<>();
//每一行的高度 組成的List
private List<Integer> mLineHeight = new ArrayList<Integer>();
//所有的View
private List<List<View>> mAllViews = new ArrayList<List<View>>();
//適配器
private MyTagAdapter mTagAdapter;
我們先搞定適配器,我們提供一個數組信息
//需要顯示的數據
private String[] mGuseeYourLoveVals = new String[]
{"Android", "Android移動", "Java", "UI設計師", "android實習",
"android 移動","android安卓","安卓"};
適配器的實現十分簡單,我們可以仿照Android系統自有的適配器
/**
抽象類
*/
public abstract class MyTagAdapter<T> {
//數據
private List<T> mTagDatas;
//構造函數
public MyTagAdapter(T[] datas) {
mTagDatas = new ArrayList<T>(Arrays.asList(datas));
}
//獲取總數
public int getCount() {
return mTagDatas == null ? 0 : mTagDatas.size();
}
//抽象方法 獲取View 由子類具體實現如何獲得View
public abstract View getView(MyTagFlowLayout parent, int position, T t);
//獲取數據中的某個Item
public T getItem(int position) {
return mTagDatas.get(position);
}
}
我們在MainActivity中調用如下語句
//MyTagFlowLayout使我們自定義的ViewGroup,目前該類還是默認實現
mGuseeYourLoveFlowLayout = (MyTagFlowLayout) findViewById(R.id.id_guess_your_love);
//指定適配器,我們這裏使用了匿名內部類的方式指定
mGuseeYourLoveFlowLayout.setAdapter(new MyTagAdapter<String>(mGuseeYourLoveVals)
{
//獲取LayoutInflater
final LayoutInflater mInflater = LayoutInflater.from(MainActivity.this);
//重點來了,我們在該匿名內部類中實現了MyTagAdapter的getView方法
@Override
public View getView(MyTagFlowLayout parent, int position, String s)
{
//在該方法中我們去加載了我們上面提到的layout/tag_view.xml,並返回TextView
TextView tv = (TextView) mInflater.inflate(R.layout.tag_view,
mGuseeYourLoveFlowLayout, false);
tv.setText(s);
return tv;
}
});
其中MyTagFlowLayout的setAdapter方法如下,,我們一點點分析MyTagFlowLayout定義過程
public void setAdapter(MyTagAdapter adapter) {
removeAllViews();//先清空MyTagFlowLayout下的所有View
for (int i = 0; i < adapter.getCount(); i++) {
//這裏的tagView 就是剛纔的TextView
View tagView = adapter.getView(this, i, adapter.getItem(i));
//添加View
addView(tagView);
}
}
此時我們的MyTagFlowLayout數據已經加載完畢,接下來就是顯示,,顯示纔是重中之重
我們先來複習一下View的顯示過程measure->layout->draw。那麼顯然我們這個要先measure,那就重寫onMeasure方法把
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//這裏我們先獲取父View給定的測量參數,注意這個父View代表的是MyTagFlowLayout的父View
int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);//獲取父View傳給MyTagFlowLayout的寬度
int modeWidth = MeasureSpec.getMode(widthMeasureSpec);//獲取父View傳給MyTagFlowLayout的寬度測量模式
int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);//獲取父View傳給MyTagFlowLayout的高度
int modeHeight = MeasureSpec.getMode(heightMeasureSpec);//獲取父View傳給MyTagFlowLayout的高度測量模式
int width = 0;
int height = 0;
int lineWidth = 0;
int lineHeight = 0;
//得到所有的子View,在上一步的過程中我們已經添加的子View,按照上一步的數據,這裏的cCount 應該是8
int cCount = getChildCount();
for (int i = 0; i < cCount; i++) {
//循環得到每一個子View,這個的child指向的實際是我們上面添加TextView
View child = getChildAt(i);
//測量每一個子View,
measureChild(child, widthMeasureSpec, heightMeasureSpec);
//得到每一個子View的測量寬度和高度
int childWidth = child.getMeasuredWidth();
int childHeight = child.getMeasuredHeight();
//如果當前行的寬度+將要添加的child的寬度 > MyTagFlowLayout的寬度-pading,說明當前行已經“滿”了,這個“滿”了意思是,當前行已經容納不了下一個子View
if (lineWidth + childWidth > sizeWidth - getPaddingLeft() - getPaddingRight()) {//"滿"了需要換行
width = Math.max(width, lineWidth);//MyTagFlowLayout的寬度取上一次寬度和當前lineWidth的最大值
lineWidth = childWidth;//重置當前行的lineWidth
height += lineHeight;//MyTagFlowLayout的高度增加
lineHeight = childHeight;//重置當前行的lineHeight 爲子View的高度
} else {//沒“滿”,當前行可以容納下一個子View
lineWidth += childWidth;//當前行的寬度增加
lineHeight = Math.max(lineHeight, childHeight);//當前行的高度取上一次高度和子View的高度的最大值
}
if (i == cCount - 1) {//如果當前View是最後的View
width = Math.max(lineWidth, width);//MyTagFlowLayout的寬度取上一次寬度和當前lineWidth的最大值
height += lineHeight;//MyTagFlowLayout的高度增加
}
}
//設置MyTagFlowLayout的高度和寬度
//如果是在XMl指定了MyTagFlowLayout的寬度,如 android:layout_width="40dp"那就使用指定的寬度,否則使用測量的寬度-padding,高度的設置與寬度雷同
setMeasuredDimension(
modeWidth == MeasureSpec.EXACTLY ? sizeWidth : width + getPaddingLeft() + getPaddingRight(),
modeHeight == MeasureSpec.EXACTLY ? sizeHeight : height + getPaddingTop() + getPaddingBottom()
);
}
上面我們已經分析了onMeasure方法,measure是測量,後面的layout是佈局,我們來看一下佈局
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
//先清除所有的List
mAllViews.clear();
mLineHeight.clear();
lineViews.clear();
//得到MyTagFlowLayout的寬度,這個我們已經在onMeasure方法中得到了
int width = getWidth();
//行寬和行高初始化爲0
int lineWidth = 0;
int lineHeight = 0;
//一樣的得到所有子View的數量
int cCount = getChildCount();
for (int i = 0; i < cCount; i++) {
//循環得到每一個子View,這個的child指向的實際是我們上面添加TextView
View child = getChildAt(i);
//View 可見性如果是View.GONE,則忽略它
if (child.getVisibility() == View.GONE) continue;
//得到子View的測量寬度和高度
int childWidth = child.getMeasuredWidth();
int childHeight = child.getMeasuredHeight();
//如果當前行寬lineWidth + 當前子View的寬度 > MyTagFlowLayout的寬度-padding,那麼我們該換行顯示了
if (childWidth + lineWidth > width - getPaddingLeft() - getPaddingRight()) {
mLineHeight.add(lineHeight);//把當前行高lineHeight添加進表示當前所有行 行高表示的mLineHeight list中
mAllViews.add(lineViews);//同樣的加入mAllViews
lineWidth = 0;//重置行寬
lineHeight = childHeight;//重置行高
lineViews = new ArrayList<View>();//重置lineViews
}
lineWidth += childWidth;//當前行寬lineWidth 增加
lineHeight = Math.max(lineHeight, childHeight );;//當前行高lineHeight 取前一次行高和子View的最大值
lineViews.add(child);//把子View添加進表示當前所有子View的lineViews的list中
}
mLineHeight.add(lineHeight);//把當前行高lineHeight添加進表示當前所有行 行
mAllViews.add(lineViews);//同樣的加入mAllViews
//獲取PaddingTop
int top = getPaddingTop();
//獲取所有行的數量
int lineNum = mAllViews.size();
for (int i = 0; i < lineNum; i++) {
//循環取出每一行
lineViews = mAllViews.get(i);
//循環去除每一行的行高
lineHeight = mLineHeight.get(i);
//獲取PaddingLeft
int left = getPaddingLeft();
for (int j = 0; j < lineViews.size(); j++) {
//從每一行中循環取出子View
View child = lineViews.get(j);
if (child.getVisibility() == View.GONE) {
continue;
}
//調用child的layout,這裏實際上是調用TextView.layout
child.layout(left, top, lc + child.getMeasuredWidth(), tc + child.getMeasuredHeight());
left += child.getMeasuredWidth() ;//left遞增
}
top += lineHeight;//top遞增
}
}
好了,我們來運行一下效果並不像我們在文章開頭給出的那樣,,但是起碼出來一個類似的了。下面要考慮的就是如何爲這些子View添加合適的間距了。。我相信聰明的讀者一定可以自行解決這個問題的。這裏稍微提示一下間距->margin??