1,在開發的時候,常在我們的需求中會有這種效果,添加一個商品的一些熱門標簽,效果圖如下:
2,從上面效果可以看得出來,這是一個自定義的ViewGroup,然后實現換行效果,讓我們一起來實現一下
- 自定義屬性
從上面的效果來看,我們需要動態的設置每個lable的寬度和高度,所以我們編寫如下的自定義屬性
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="FlowLayout">
<!-- 標簽之間的間距-->
<attr name="lineSpace" format="dimension"/>
<!-- 每一行之間的間距-->
<attr name="rowSpace" format="dimension"/>
</declare-styleable>
</resources>
在布局文件中使用
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:flowlayout="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.qianmo.flowlayout.FlowLayout
android:id="@+id/flowLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="20dip"
flowlayout:lineSpace="20dip"
flowlayout:rowSpace="10dip"/>
</LinearLayout>
在類中獲取自定義屬性
public class FlowLayout extends ViewGroup {
private static String TAG = "FlowLayout";
//自定義屬性
private int LINE_SPACE;
private int ROW_SPACE;
//放置標簽的集合
private List<String> lables;
private List<String> lableSelects;
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 a = context.obtainStyledAttributes(attrs, R.styleable.FlowLayout);
LINE_SPACE = a.getDimensionPixelSize(R.styleable.FlowLayout_lineSpace, 10);
ROW_SPACE = a.getDimensionPixelSize(R.styleable.FlowLayout_rowSpace, 10);
a.recycle();
}
}
- 初始化數據數據源
向FlowLayout類中添加數據
/**
* 添加標簽
*
* @param lables 標簽集合
* @param isAdd 是否添加
*/
public void setLables(List<String> lables, boolean isAdd) {
if (this.lables == null) {
this.lables = new ArrayList<>();
}
if (this.lableSelects == null) {
this.lableSelects = new ArrayList<>();
}
if (isAdd) {
this.lables.addAll(lables);
} else {
this.lables.clear();
this.lables = lables;
}
if (lables != null && lables.size() > 0) {
for (final String lable : lables) {
final TextView tv = new TextView(getContext());
tv.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT,
LayoutParams.WRAP_CONTENT));
tv.setText(lable);
tv.setTextSize(20);
tv.setBackgroundResource(R.drawable.shape_item_lable_bg);
tv.setTextColor(Color.BLACK);
tv.setGravity(Gravity.CENTER);
tv.setPadding(12, 5, 12, 5);
//判斷是否選中
if (lableSelects.contains(lable)) {
tv.setSelected(true);
tv.setTextColor(getResources().getColor(R.color.tv_blue));
} else {
tv.setSelected(false);
tv.setTextColor(getResources().getColor(R.color.tv_gray));
}
//點擊之后選中標簽
tv.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
tv.setSelected(tv.isSelected() ? false : true);
if (tv.isSelected()) {
tv.setTextColor(getResources().getColor(R.color.tv_blue));
lableSelects.add(lable);
} else {
tv.setTextColor(getResources().getColor(R.color.tv_gray));
lableSelects.remove(lable);
}
}
});
//添加到容器中
addView(tv);
}
}
}
下面的代碼是textview的背景選擇器
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android" >
<!--選中效果-->
<item android:state_selected="true">
<shape >
<solid android:color="#ffffff" />
<stroke android:color="@color/tv_blue"
android:width="2px"/>
<corners android:radius="10000dip"/>
</shape>
</item>
<!--默認效果-->
<item>
<shape >
<solid android:color="#ffffff" />
<stroke android:color="@color/divider_gray"
android:width="2px"/>
<corners android:radius="10000dip"/>
</shape>
</item>
</selector>
- 重寫onMeasure方法
本布局在寬度上是使用的建議的寬度(填充父窗體或者具體的size),如果需要wrap_content的效果,還需要重新計算,當然這種需求是非常少見的,所以直接用建議寬度即可;布局的高度就得看其中的標簽需要占據多少行(row ),那么高度就為row * 單個標簽的高度+(row -1) * 行距,代碼如下:
/**
* 通過測量子控件高度,來設置自身控件的高度
* 主要是計算
*
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//測量所有子view的寬高
measureChildren(widthMeasureSpec, heightMeasureSpec);
//獲取view的寬高測量模式
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
//這里的寬度建議使用match_parent或者具體值,當然當使用wrap_content的時候沒有重寫的話也是match_parent所以這里的寬度就直接使用測量的寬度
int width = widthSize;
int height;
//判斷寬度
if (heightMode == MeasureSpec.EXACTLY) {
height = heightSize;
} else {
int row = 1;
int widthSpace = width; //寬度剩余空間
for (int i = 0; i < getChildCount(); i++) {
View view = getChildAt(i);
//獲取標簽寬度
int childW = view.getMeasuredWidth();
//判斷剩余寬度是否大於此標簽寬度
if (widthSpace >= childW) {
widthSpace -= childW;
} else {
row++;
widthSpace = width - childW;
}
//減去兩邊間距
widthSpace -= LINE_SPACE;
}
//獲取子控件的高度
int childH = getChildAt(0).getMeasuredHeight();
//測算最終所需要的高度
height = (childH * row) + (row - 1) * ROW_SPACE;
}
//保存測量高度
setMeasuredDimension(width, height);
}
- 重寫OnLayout方法
onLayout(boolean changed, int l, int t, int r, int b)方法是一個抽象方法,自定義ViewGroup時必須實現它,用於給布局中的子控件分配位置,其中的參數l,t,r,b分別代表本ViewGroup的可用空間(除去margin和padding后的剩余空間)的左、上、右、下的坐標(相對於自身),相當於一個約束,如果子控件擺放的位置超過這個范圍,超出的部分將不可見。
/**
* 擺放子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 row = 0;
int right = 0;
int bottom = 0;
for (int i = 0; i < getChildCount(); i++) {
View chileView = getChildAt(i);
int childW = chileView.getMeasuredWidth();
int childH = chileView.getMeasuredHeight();
right += childW;
bottom = (childH + ROW_SPACE) * row + childH;
if (right > (r - LINE_SPACE)) {
row++;
right = childW;
bottom = (childH + ROW_SPACE) * row + childH;
}
chileView.layout(right - childW, bottom - childH, right, bottom);
right += LINE_SPACE;
}
}
看一下實現的效果圖
ok,這樣我們就全部實現了,需要源碼的同學可以在這里去下載
