zoukankan      html  css  js  c++  java
  • Android -- 自定义ViewGroup实现FlowLayout效果

    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,这样我们就全部实现了,需要源码的同学可以在这里去下载

  • 相关阅读:
    iOS上传AppStore被拒原因及处理方案
    记录一个打包upload时报错处理
    Git撤销merge
    设置UIButton上面是image,下面是title
    Sourcetree中切换到其他分支,临时分支的这次提交丢失
    修改字符串指定范围的颜色
    定时消失的Alert弹窗
    ALAsset 将资源转换为 NSData
    textField和textView的输入字数最大限制
    手机号码正则判断
  • 原文地址:https://www.cnblogs.com/wjtaigwh/p/6626517.html
Copyright © 2011-2022 走看看