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,這樣我們就全部實現了,需要源碼的同學可以在這里去下載