本文共 10957 字,大约阅读时间需要 36 分钟。
1.
2.
通过前面的1和2已经实现了简单的自定义viewgroup,流式布局和上一个换行的viewgroup相比:
需要判断一行上可以放置多少个子view;某一行view的实际高度是这一行最高的呢个view的高度;还需要考虑子view的margin属性。
支持margin:重写generateLayoutParams方法,返回new MarginLayoutParams对象。
1.根据传入的datas将子view add到viewgroup里面
正常使用肯定是在xml里面写<Flowlayout/>,代码写flowlayout.setData(),类似于listview那样;
那么addview就不能在构造器里面而是在onMeasure里面,而且onMeasure会执行多次,注意removeAllview;
2.onMeasure 计算多少行,每行都存放哪些子view以及viewgroup的宽高
计算子view的宽高,遍历子view,计算在哪换行等。使用List<List<View>> 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方法
源代码地址:
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(Listdatas) { 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);
转载地址:http://pcrci.baihongyu.com/