1.https://mp.csdn.net/postedit/87189294
2.https://mp.csdn.net/postedit/87189763
通过前面的1和2已经实现了简单的自定义viewgroup,流式布局和上一个换行的viewgroup相比:
需要判断一行上可以放置多少个子view;某一行view的实际高度是这一行最高的呢个view的高度;还需要考虑子view的margin属性。
支持margin:重写generateLayoutParams方法,返回new MarginLayoutParams对象。
1.根据传入的datas将子view add到viewgroup里面
正常使用肯定是在xml里面写
那么addview就不能在构造器里面而是在onMeasure里面,而且onMeasure会执行多次,注意removeAllview;
2.onMeasure 计算多少行,每行都存放哪些子view以及viewgroup的宽高
计算子view的宽高,遍历子view,计算在哪换行等。使用List> list 来存储所有view:外层list表示当前有多少行,内层表示每一行都是哪些子view。
3.onLayout显示view
遍历上面的list分别显示每一行,注意考虑margin。
4.子view点击事件并刷新显示
可以使用直接通过子view来控制也可以用数据源来控制。
(1.使用数据源来控制:点击事件的回调中对是否选中的bool值进行更改并调用requestLayout方法刷新显示,但是通过数据源来控制会频繁触发onMeasure和onLayout。这样做可以把控制单选和多选交给使用者通过数据源来处理
(2.使用子view控制:点击事件直接对数据源并更改只对子view更新显示,这样做控制单选和单选就必须在这个Viewgroup内部实现。
5.单选与多选
数据源控制的方式就不说了:点击事件修改数据源,然后requestLayout.
当子view控制时,多选在for循环addview的时候直接datas.get(i).setIsChoosed(!get(i).isChoosed)即可;
单选的时候就必须保证点击完整个view,其他的view全部变成未选中,变成未选中容易但是同时修改view对应的数据就有点麻烦。
两种方法处理:
(1.用add(view,index),这里的index就是getChildAt的index,通过判断getChildAt==点击事件的view,得到触发点击事件view在datas以及childs中对应的下标,for循环datas或者把childs不是这个下标的全部置false和设置不选中。
(2.这个点击事件本身就在datas的for循环内部,再for循环datas把除了这个下标外的所有数据全部置false,然后for循环childs判断当前点击view相等,不相等的全部设置不选中。
6.刷新数据
调用requestLayout方法重新走onmearsure方法
源代码地址:
https://github.com/15539158137/FlowLayoutDemo/tree/master
ViewGroup代码如下:
public class Flowlayout extends ViewGroup {
OnFlowlayoutItemClickListener onFlowlayoutItemClickListener;
public void setOnFlowlayoutItemClickListener(OnFlowlayoutItemClickListener onFlowlayoutItemClickListener) {
this.onFlowlayoutItemClickListener = onFlowlayoutItemClickListener;
}
public Flowlayout(Context context) {
super(context);
}
public Flowlayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public Flowlayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public Flowlayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
//外部传入数据
public void setDatas(List datas) {
this.dataBeanList = datas;
//invalidate();这个方法只会触发onDraw
requestLayout();//这个只会触发onDraw和OnMeasure
}
//设置是否可以多选
private boolean canChooseMulite;
public void setChooseMulite(boolean canChooseMulite) {
this.canChooseMulite = canChooseMulite;
}
//通过数据源来控制子view的点击事件的方法
public void notifyDataChanged() {
//这个会触发onMeasure方法,但是必须保证dataBeanList还是同一个对象才行
requestLayout();
}
private List dataBeanList;
private void initView() {
//先清空所有
this.removeAllViews();
for (int i = 0; i < dataBeanList.size(); i++) {
final DataBean dataBean = dataBeanList.get(i);
final TextView textView = new TextView(getContext());
textView.setText(dataBean.getFlowItemName());
//设置开始是否被选中
if (dataBean.isFlowItemIsChoosed()) {
textView.setBackgroundResource(R.drawable.shape_tv_red);
} else {
textView.setBackgroundResource(R.drawable.shape_tv_blue);
}
textView.setBackgroundResource(R.drawable.shape_item_bg);
textView.setPadding(SizeUtil.dip2px(getContext(), 5), SizeUtil.dip2px(getContext(), 3), SizeUtil.dip2px(getContext(), 5), SizeUtil.dip2px(getContext(), 3));
MarginLayoutParams marginLayoutParams = new MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
marginLayoutParams.rightMargin = SizeUtil.dip2px(getContext(), 10);
marginLayoutParams.leftMargin = SizeUtil.dip2px(getContext(), 10);
marginLayoutParams.topMargin = SizeUtil.dip2px(getContext(), 5);
marginLayoutParams.bottomMargin = SizeUtil.dip2px(getContext(), 5);
textView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
//可以多选
dataBean.setFlowItemIsChoosed(!dataBean.isFlowItemIsChoosed());
if (dataBean.isFlowItemIsChoosed()) {
textView.setBackgroundResource(R.drawable.shape_tv_red);
} else {
textView.setBackgroundResource(R.drawable.shape_tv_blue);
}
if (onFlowlayoutItemClickListener != null) {
onFlowlayoutItemClickListener.onItemClick(textView, dataBean);
}
if (!canChooseMulite) {
boolean nowState = !dataBean.isFlowItemIsChoosed();
//不管当前这个点击之后的状态是不是选中,其他的都必须是未选中。
for (int j = 0; j < dataBeanList.size(); j++) {
if (v == getChildAt(j)) {
//这个就是当前点击的这个子view
} else {
dataBeanList.get(j).setFlowItemIsChoosed(false);
getChildAt(j).setBackgroundResource(R.drawable.shape_tv_blue);
}
}
}
}
});
textView.setLayoutParams(marginLayoutParams);
this.addView(textView, i);//后面的i表示index就是getChildAt index
}
}
//所有的view
List> allViews;
//每一行上的view
List oneLineViews;
//每一行的高度
List allHeights;
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Log.e("onMeasure", "===");
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
initView();
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthMeasure = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightMeasure = MeasureSpec.getSize(heightMeasureSpec);
//实际操作中height一定是wrap,如果height写成match或者具体尺寸,也不判断是否能容纳下
//width不管写成warp还是match还是具体尺寸,都不影响,因为wrap默认就是match,所以实际宽度就是这个计算出来的数值
int width = widthMeasure;
int height = heightMeasure;
//先计算viewgroup横向上实际能够使用的宽度:width-padding
//可以使用的宽度是
int canUseWidth = width - getPaddingLeft() - getPaddingRight();
//for循环view,计算每个view的宽高,当宽度超出就换行
//list> 内层是这行的view,外层是行数
int childCount = getChildCount();
Log.e("当前页面有多少个child", childCount + "");
//当前一行上累计的宽度
int nowWidth = 0;
//这一行的高度
int nowHeight = 0;
//
int totalHeight = 0;
//所有的view
allViews = new ArrayList<>();
//每一行上的view
oneLineViews = new ArrayList<>();
//每一行的高度
allHeights = new ArrayList<>();
Log.e("横向可容纳的宽度是", canUseWidth + "");
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
// measureChildWithMargins();把viewgroup的margin都加进去计算进去
measureChild(child, widthMeasureSpec, heightMeasureSpec);
MarginLayoutParams marginLayoutParams = (MarginLayoutParams) child.getLayoutParams();
int childWidth = child.getMeasuredWidth();
int childHeight = child.getMeasuredHeight();
nowWidth = nowWidth + childWidth + marginLayoutParams.rightMargin + marginLayoutParams.leftMargin;
Log.e("当前nowWidth是", nowWidth + "");
//viewgroup的高度也开始计算了,得到这一行最高的高度然后相加
int childTotalHeight = childHeight + marginLayoutParams.topMargin + marginLayoutParams.bottomMargin;
Log.e("当前的下标", i + "");
if (nowWidth > canUseWidth) {
//这一行放不下了,该换行了
totalHeight = totalHeight + nowHeight;//把上一行的最大值加上
allHeights.add(nowHeight);//设置上一行的行高
Log.e("大于", totalHeight + "");
//把上一行加进去,新开一行并到当前view加到新的一行去
allViews.add(oneLineViews);
oneLineViews = new ArrayList<>();
oneLineViews.add(child);
//换行了,下一行的nowwidth和nowheight是这个view的宽高
nowWidth = childWidth + marginLayoutParams.rightMargin + marginLayoutParams.leftMargin;
nowHeight = childTotalHeight;
} else if (nowWidth == canUseWidth) {
//正好放满
if (childTotalHeight > nowHeight) {
nowHeight = childTotalHeight;
}
totalHeight = totalHeight + nowHeight;
Log.e("等于", totalHeight + "");
allHeights.add(nowHeight);//设置这一行的行高
nowHeight = 0;
//刚好能放下--加到当前行并开新行
oneLineViews.add(child);
allViews.add(oneLineViews);
oneLineViews = new ArrayList<>();
nowWidth = 0;
} else {
//小于,加到当前行里面
oneLineViews.add(child);
if (childTotalHeight > nowHeight) {
nowHeight = childTotalHeight;
}
if (i == childCount - 1) {
//如果他单独一行,上面已经new oneline了 而且已经add过了,所以不管是这行之前还有其他view还是他单独一行,只用add一下就可以,
allViews.add(oneLineViews);
allHeights.add(nowHeight);
totalHeight = totalHeight + nowHeight;
nowHeight = 0;
nowWidth = 0;
}
Log.e("小于", totalHeight + "");
}
}
if (widthMode == MeasureSpec.EXACTLY) {
//返回计算出来的
} else {
//还是计算出来的
}
if (heightMode == MeasureSpec.EXACTLY) {
//返回计算出来的
} else {
height = totalHeight + getPaddingTop() + getPaddingBottom();
}
Log.e("计算出来的宽高是", width + "==" + height);
setMeasuredDimension(width, height);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int startY = 0;
Log.e("一共有多少行", allViews.size() + "");
for (int i = 0; i < allViews.size(); i++) {
Log.e("每一行的行高是", allHeights.get(i) + "");
List line = allViews.get(i);
int startX = 0;
for (int j = 0; j < line.size(); j++) {
View child = line.get(j);
MarginLayoutParams marginLayoutParams = (MarginLayoutParams) child.getLayoutParams();
int childWidth = child.getMeasuredWidth();
int childHeight = child.getMeasuredHeight();
child.layout(startX + marginLayoutParams.leftMargin, startY + marginLayoutParams.topMargin, startX + marginLayoutParams.leftMargin + childWidth, startY + marginLayoutParams.topMargin + childHeight);
startX = startX + childWidth + marginLayoutParams.leftMargin + marginLayoutParams.rightMargin;
}
//当前累计的行高加当前行的
startY = startY + allHeights.get(i);
}
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
//return super.generateLayoutParams(attrs);
//子控件带margin,使用margin
return new MarginLayoutParams(getContext(), attrs);
}
}
使用方法:
//实例化
final Flowlayout flowlayout = findViewById(R.id.flowlayout);
all = new ArrayList<>();
for (int i = 0; i < 20; i++) {
MyBean dataBean = new MyBean();
dataBean.setFlowItemName("数据" + i * i * 10000);
all.add(dataBean);
}
//设置数据源
flowlayout.setDatas(all);
flowlayout.setOnFlowlayoutItemClickListener(new OnFlowlayoutItemClickListener() {
@Override
public void onItemClick(View view, DataBean dataBean) {
//子view点击的回调
//点击了子view,返回的是子view改变后的状态
Log.e("点击信息", dataBean.getFlowItemName() + "点击变为" + dataBean.isFlowItemIsChoosed());
for (DataBean dataBean1 : all) {
Log.e("是否被选中", dataBean1.isFlowItemIsChoosed() + dataBean1.getFlowItemName());
}
}
});
all.clear();
for (int i = 0; i < 10; i++) {
MyBean dataBean = new MyBean();
dataBean.setFlowItemName("数据3333" + i * i * 10000);
all.add(dataBean);
}
//更新数据的方法
flowlayout.notifyDataChanged();
//设置单选和多选的方法
flowlayout.setChooseMulite(isChooseMulite);