1.介紹
流式布局的應(yīng)用還是很廣泛的辰晕,比如搜索熱詞顽分、關(guān)鍵詞標(biāo)簽等,GitHub上已經(jīng)有很多這樣的布局了丝里,但是還是想著自己實(shí)現(xiàn)一下曲初,最近一直在學(xué)自定義控件,也鞏固一下所學(xué)的知識(shí)杯聚。
本文實(shí)現(xiàn)的效果如下圖所示:
2.思路
- 繼承自RelativeLayout臼婆,可以直接使用RelativeLayout中的相關(guān)屬性,本文也可以修改為繼承ViewGroup幌绍,并不會(huì)有什么影響颁褂。
- 在onMeasure方法中計(jì)算出所有childView的寬和高,然后根據(jù)childView的寬和高計(jì)算出布局自身的寬和高傀广。
- 在onLayout方法中計(jì)算出所有childView的位置并進(jìn)行布局颁独。
- 封裝Line對(duì)象,管理每行上的View對(duì)象
3.實(shí)現(xiàn)
初始化一些屬性
public class FlowLayout extends RelativeLayout {
// 水平間距伪冰,單位為dp
private int horizontalSpacing = dp2px(10);
// 豎直間距誓酒,單位為dp
private int verticalSpacing = dp2px(10);
// 行的集合
private List<Line> lines = new ArrayList<Line>();
// 當(dāng)前的行
private Line line;
// 當(dāng)前行使用的空間
private int lineSize = 0;
// 關(guān)鍵字大小,單位為sp
private int textSize = sp2px(15);
// 關(guān)鍵字顏色
private int textColor = Color.BLACK;
// 關(guān)鍵字背景框
private int backgroundResource = R.drawable.bg_frame;
// 關(guān)鍵字水平padding贮聂,單位為dp
private int textPaddingH = dp2px(7);
// 關(guān)鍵字豎直padding靠柑,單位為dp
private int textPaddingV = dp2px(4);
public FlowLayout(Context context) {
this(context, null);
}
public FlowLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray typedArray = context.getTheme().obtainStyledAttributes(
attrs, R.styleable.FlowLayoutAttrs, defStyleAttr, 0);
int count = typedArray.getIndexCount();
for (int i = 0; i < count; i++) {
int attr = typedArray.getIndex(i);
switch (attr) {
case R.styleable.FlowLayoutAttrs_horizontalSpacing:
horizontalSpacing = typedArray.getDimensionPixelSize(attr, dp2px(10));
break;
case R.styleable.FlowLayoutAttrs_verticalSpacing:
verticalSpacing = typedArray.getDimensionPixelSize(attr, dp2px(10));
break;
case R.styleable.FlowLayoutAttrs_textSize:
textSize = typedArray.getDimensionPixelSize(attr, sp2px(15));
break;
case R.styleable.FlowLayoutAttrs_textColor:
textColor = typedArray.getColor(attr, Color.BLACK);
break;
case R.styleable.FlowLayoutAttrs_backgroundResource:
backgroundResource = typedArray.getResourceId(attr, R.drawable.bg_frame);
break;
case R.styleable.FlowLayoutAttrs_textPaddingH:
textPaddingV = typedArray.getDimensionPixelSize(attr, dp2px(7));
break;
case R.styleable.FlowLayoutAttrs_textPaddingV:
verticalSpacing = typedArray.getDimensionPixelSize(attr, dp2px(4));
break;
}
}
typedArray.recycle();
}
...
}
onMeasure
首先獲取父容器傳入的寬高值與測(cè)量模式,計(jì)算出實(shí)際使用的寬和高吓懈,遍歷所有的childView歼冰,對(duì)childView進(jìn)行測(cè)量,根據(jù)當(dāng)前行已用的寬度判斷是否需要換行耻警,然后累計(jì)所有高度隔嫡,設(shè)置布局自身尺寸甸怕。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 實(shí)際可以用的寬和高
int width = MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft() - getPaddingRight();
int height = MeasureSpec.getSize(heightMeasureSpec) - getPaddingBottom() - getPaddingTop();
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
// Line初始化
restoreLine();
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
// 測(cè)量所有的childView
int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width,
widthMode == MeasureSpec.EXACTLY ? MeasureSpec.AT_MOST : widthMode);
int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height,
heightMode == MeasureSpec.EXACTLY ? MeasureSpec.AT_MOST : heightMode);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
if (line == null) {
// 創(chuàng)建新一行
line = new Line();
}
// 計(jì)算當(dāng)前行已使用的寬度
int measuredWidth = child.getMeasuredWidth();
lineSize += measuredWidth;
// 如果使用的寬度小于可用的寬度,這時(shí)候childView能夠添加到當(dāng)前的行上
if (lineSize <= width) {
line.addChild(child);
lineSize += horizontalSpacing;
} else {
// 換行
newLine();
line.addChild(child);
lineSize += child.getMeasuredWidth();
lineSize += horizontalSpacing;
}
}
// 把最后一行記錄到集合中
if (line != null && !lines.contains(line)) {
lines.add(line);
}
int totalHeight = 0;
// 把所有行的高度加上
for (int i = 0; i < lines.size(); i++) {
totalHeight += lines.get(i).getHeight();
}
// 加上行的豎直間距
totalHeight += verticalSpacing * (lines.size() - 1);
// 加上上下padding
totalHeight += getPaddingBottom();
totalHeight += getPaddingTop();
// 設(shè)置自身尺寸
// 設(shè)置布局的寬高畔勤,寬度直接采用父view傳遞過來的最大寬度蕾各,而不用考慮子view是否填滿寬度
// 因?yàn)樵摬季值奶匦跃褪翘顫M一行后,再換行
// 高度根據(jù)設(shè)置的模式來決定采用所有子View的高度之和還是采用父view傳遞過來的高度
setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec),
resolveSize(totalHeight, heightMeasureSpec));
}
附上restoreLine與newLine方法
private void restoreLine() {
lines.clear();
line = new Line();
lineSize = 0;
}
private void newLine() {
// 把之前的行記錄下來
if (line != null) {
lines.add(line);
}
// 創(chuàng)建新的一行
line = new Line();
lineSize = 0;
}
Line
封裝了每行上的View對(duì)象庆揪,提供添加與繪制childView的方法式曲。
/**
* 管理每行上的View對(duì)象
*/
class Line {
// 子控件集合
private List<View> children = new ArrayList<View>();
// 行高
int height;
/**
* 添加childView
*
* @param childView 子控件
*/
public void addChild(View childView) {
children.add(childView);
// 讓當(dāng)前的行高是最高的一個(gè)childView的高度
if (height < childView.getMeasuredHeight()) {
height = childView.getMeasuredHeight();
}
}
/**
* 設(shè)置childView的繪制區(qū)域
*
* @param left 左上角x軸坐標(biāo)
* @param top 左上角y軸坐標(biāo)
*/
public void layout(int left, int top) {
int totalWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
// 當(dāng)前childView的左上角x軸坐標(biāo)
int currentLeft = left;
for (int i = 0; i < children.size(); i++) {
View view = children.get(i);
// 設(shè)置childView的繪制區(qū)域
view.layout(currentLeft, top, currentLeft + view.getMeasuredWidth(),
top + view.getMeasuredHeight());
// 計(jì)算下一個(gè)childView的位置
currentLeft = currentLeft + view.getMeasuredWidth() + horizontalSpacing;
}
}
public int getHeight() {
return height;
}
public int getChildCount() {
return children.size();
}
}
onLayout
指定所有childView的位置,調(diào)用Line對(duì)象中的layout方法缸榛。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
int left = getPaddingLeft();
int top = getPaddingTop();
for (int i = 0; i < lines.size(); i++) {
Line line = lines.get(i);
line.layout(left, top);
// 計(jì)算下一行的起點(diǎn)y軸坐標(biāo)
top = top + line.getHeight() + verticalSpacing;
}
}
setFlowLayout
用于設(shè)置FlowLayout中的內(nèi)容吝羞,并提供點(diǎn)擊事件處理。
public void setFlowLayout(List<String> list, final OnItemClickListener onItemClickListener) {
for (int i = 0; i < list.size(); i++) {
final TextView tv = new TextView(getContext());
// 設(shè)置TextView屬性
tv.setText(list.get(i));
tv.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
tv.setTextColor(textColor);
tv.setGravity(Gravity.CENTER);
tv.setPadding(textPaddingH, textPaddingV, textPaddingH, textPaddingV);
tv.setClickable(true);
tv.setBackgroundResource(backgroundResource);
this.addView(tv, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT));
tv.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
onItemClickListener.onItemClick(tv.getText().toString());
}
});
}
}
public interface OnItemClickListener {
void onItemClick(String content);
}
使用方法
- 在項(xiàng)目根目錄的build.gradle文件中加入如下代碼
maven { url "https://jitpack.io" }
- 在app根目錄的buil.gradle文件中加入依賴
compile 'com.github.alidili:FlowLayout:v1.0'
- 在Activity中使用内颗,設(shè)置點(diǎn)擊事件
相關(guān)屬性(字體大小钧排、顏色、間距等)可以在布局文件中設(shè)置均澳,也可以通過set方法設(shè)置恨溜。
public class MainActivity extends AppCompatActivity {
private FlowLayout flKeyword;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
flKeyword = (FlowLayout) findViewById(R.id.fl_keyword);
List<String> list = new ArrayList<>();
list.add("關(guān)鍵詞一");
list.add("關(guān)鍵詞二");
list.add("關(guān)鍵詞三");
list.add("關(guān)鍵詞四");
list.add("關(guān)鍵詞五");
flKeyword.setFlowLayout(list, new FlowLayout.OnItemClickListener() {
@Override
public void onItemClick(String content) {
Toast.makeText(MainActivity.this, content, Toast.LENGTH_SHORT).show();
}
});
}
}
- 布局文件
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.yang.flowlayoutlibrary.FlowLayout
android:id="@+id/fl_keyword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
android:layout_marginTop="10dp"
app:itemColor="@color/colorAccent"
app:itemSize="15sp" />
</RelativeLayout>
4.寫在最后
源碼已托管到GitHub上,歡迎Fork找前,覺得還不錯(cuò)就Start一下吧糟袁!
GitHub地址:https://github.com/alidili/FlowLayout
歡迎同學(xué)們吐槽評(píng)論,如果你覺得本篇博客對(duì)你有用躺盛,那么就留個(gè)言或者點(diǎn)下喜歡吧(^-^)