亲测实现PopupWindow显示FlowLayout流式布局带固定文本/按钮(位置可改)

实现:动态绘制并带固定文本/按钮,固定文本/按钮固定在最后一行的右边且垂直居中,若最后一行放不下,则新开一行放到新行的右边且垂直居中(新行的行高跟前面的一样),可单选、多选、重置。

注:若不要求放到最后一行,只放在FlowLayout的下面,则可以在xml布局放个文本/按钮实现。
思路:FlowLayout流式布局,把固定文本/按钮的数据添加到要绘制的内容最后面,在绘制时,对最后一个进行特别处理样式,事件等。设置不显示固定文本/按钮时,则不需要添加固定文本/按钮的数据到要绘制的内容最后面。
效果图如下(粉色区域就是动态绘制的):

图1:不显示固定文本/按钮,最后一行放得下

图2:显示固定文本/按钮,最后一行放得下,就放在最后一行右边且跟最后一行垂直居中

图3:不显示固定文本/按钮,最后一行放不下,换行

图4:显示固定文本/按钮,最后一行放不下,新开一行放到新行的右边且垂直居中(新行的行高跟前面的一样)

       

 前提准备:

  1. onMeasure方法:测量出viewGroup的宽高如上图粉色的宽高,是指整个的不是指周1、周2这些child的宽高
     /**
         * xml布局时match_parent或者wrap_content,如果只super()不对宽高测量:控件的大小是由父控件决定的,一般就是会填充父容器。
         * 对自定义viewGroup的宽高进行测量
         * 行宽:新增view,加了后宽度没有超过GroupView最大宽度,则添加;否则换行,新一行行宽就是该view的测量宽度,有行间距时,需要加上行间距
         * 行高:本行子最高view高度,没有换行就一直对比新增的view高度,谁大取谁。换行时,行高=换行后第一个view高度。有行间距时需要加上。
         * 期望的总高度:height设置wrap_content 时,ViewGroup的上下内间距 + 行高…+行高
         * 注:addView、setVisbility、setTextView等方法会重新调用 requestLayout,会重新测量、重新摆放、重新绘制view,影响性能,因此尽量少用
         *
         * @param widthMeasureSpec
         * @param heightMeasureSpec
         */
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            //对动态绘制的viewGroup宽高进行测量
            …………………………………………………………………………………………………………………………………………
        }
  2. onLayout方法:每个child在什么位置上,在这个方法里面可以改变固定文本/按钮的位置。
        /**
         * 确定控件现在在哪个位置,view.layout(左,上,右,下)
         * 布局,计算view起始位置,顶部偏移量,左侧偏移量(每个child的位置(超出onMeasure宽高部分不显示))
         * 行高:当前行最高高度,换行时,顶部偏移量需要加上该行行高+行间距,更新新行高的为新view的高度
         * child顶部偏移量:view的top位置,同行的顶部偏移量一样;换行时,新行的顶部偏移 = 当前偏移量+当前行高
         * child左侧偏移量:view的left位置,新view的左侧偏移距离 = 当前左侧偏移 + 当前子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) {
               ………………………………………………………………………………………………………………………………
               for (int i = 0; i < getChildCount(); i++) {//遍历所以的view,把它们的位置放好
                    //计算child的左,上,右,下的值
                    ……………………………………………………………………………………………………………………
                    child.layout(左,上,右,下); //child的位置,child就会在这个位置显示
                }    
    }
  3. 单选、多选、取消选中等,重点是保存好每个child选中的情况,及几个数据的索引一致

        private List<String> dataList = new ArrayList<>();  //数据源
     
        //动态绘制的flowlayout布局内容textView,可以设置选中,不选中
        private List<TextView> tvList = new ArrayList<>();
    
        /**
         * textView 是否选中boolean,方便后面重置、全选,全不选、指定被选等需求.
         * 这里的key跟dataList、tvList的索引是一致的,就可以通过dataMap是否选中得到key进而得到dataList具体的内容
         * eg:Boolean为true就是选中的,可以得到key是1,那么dataList.get(1)就可以知道具体被选中的string,也可以将tvList.get(1)设置为不选中setSelected(false)
         */
        private Map<Integer, Boolean> dataMap = new HashMap<>();
    
        private int maxNum;// 0不限制数量,1单选,大于1的限制选择数量
    
        //是否有固定在最后一行右边的按钮/文字。放不下就换行(true即表示有,且默认最后一个数据就是固定的文本/按钮)
        private boolean isRightBt = false;

 代码:

  1. FLWCWindow悬浮框
    public class FLWCWindow implements View.OnClickListener {
        private FrameLayout frameLayout;
        private PopupWindow popupWindow;
        private Context context;
        private View view;
        private FlowLayoutWCView flowLayoutWCView;
        private Button btnSubmit, btnCancel, bt_selectAll, bt_cancleAll, bt_selectNum;
        private TextView fl_Title;//标题
    
        private OnSubmitListener submitListener;//确定按钮事件回调
    
        public FLWCWindow(Context context) {
            this.context = context;
            initView();//要处理的数据在此之前就要设置好,比如isRightBt,为了用
        }
    
        private void initView() {
            view = LayoutInflater.from(context).inflate(R.layout.flwc_window, null);
            btnCancel = view.findViewById(R.id.btnCancel);
            fl_Title = view.findViewById(R.id.tvTitle);
            btnSubmit = view.findViewById(R.id.btnSubmit);
    
            flowLayoutWCView = view.findViewById(R.id.flwc_view);
    
            bt_selectAll = view.findViewById(R.id.bt_selectAll);
            bt_cancleAll = view.findViewById(R.id.bt_cancleAll);
            bt_selectNum = view.findViewById(R.id.bt_selectNum);
    
            btnCancel.setOnClickListener(this);
            btnSubmit.setOnClickListener(this);
            bt_selectAll.setOnClickListener(this);
            bt_cancleAll.setOnClickListener(this);
    
            //时刻监听flowLayoutWCView通过点击选中的情况
            flowLayoutWCView.setOnUpdateListener(new FlowLayoutWCView.OnUpdateListener() {
                @Override
                public void updateSelect(List<String> list) {
                    if (list != null && list.size() > 0) {
                        bt_selectNum.setText("选中了" + list.size() + "个");
                    } else {
                        bt_selectNum.setText("");
                    }
                }
            });
    
            popupWindow = new PopupWindow(view, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, true);
            popupWindow.setOutsideTouchable(false);//设置外部区域可以点击取消popupWindow
            popupWindow.setTouchable(true);//是否可以触摸,false话,整个窗口点击不了
            //设置颜色跟背景色一样,某些情况下,会出现间隙,其实这个也是PopupWindow的一部分
            popupWindow.setBackgroundDrawable(new ColorDrawable(Color.parseColor("#d8d8d8")));
    
            //设置触摸监听
            popupWindow.setTouchInterceptor(new View.OnTouchListener() {
                @Override
                public boolean onTouch(View v, MotionEvent event) {
                    //popupWindow背景色灰色,整个窗口黑色0.5透明(蒙版),点击灰色任意地方调用两次此方法(一次蒙版一次popupWindow),点击非灰色调用一次。
                    return false;
                }
            });
    
            //设置取消事件监听
            popupWindow.setOnDismissListener(new PopupWindow.OnDismissListener() {
                @Override
                public void onDismiss() {
                    removeFrame();//弹窗消失时,移除蒙版
                }
            });
    
        }
    
        //设置标题,同样的增加“取消”、“确定”文字,及是否显示等方法
        public void setFl_Title(String str) {
            fl_Title.setText(str);
        }
    
        //是否显示固定文本/按钮,若true则特殊处理最后一个TextView的样式、位置
        public void isShowRightBt(boolean isRightBt) {
            flowLayoutWCView.isShowRightBt(isRightBt);
        }
    
        //设置数据
        public void setDataList(List<String> dataList){
            flowLayoutWCView.setDataList(dataList);//设置数据源
        }
    
        //显示window
        public void show() {
            addFrame();
            popupWindow.showAtLocation(view, Gravity.BOTTOM, 0, 0);//显示在底部BOTTOM,可以改成CENTER、TOP等
        }
    
        //添加蒙版
        private void addFrame() {
            FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
            frameLayout = new FrameLayout(context);
            frameLayout.setBackgroundColor(context.getResources().getColor(R.color.black));//效果是整个窗口的背景色,配合setAlpha使用
            frameLayout.setAlpha(0.5f);//透明度
            frameLayout.setLayoutParams(layoutParams);
            ((Activity) context).getWindow().addContentView(frameLayout, layoutParams);//添加蒙版,必须是activity才有getWindow获取窗口方法
        }
    
    
        //移除蒙版
        private void removeFrame() {
            ((ViewGroup) frameLayout.getParent()).removeView(frameLayout);//添加蒙版时的view父布局ViewGroup移除view
        }
    
        /**
         * 关闭弹框
         */
        public void dismiss() {
            if (null != popupWindow && popupWindow.isShowing()) {
                popupWindow.dismiss();
            }
        }
    
        @Override
        public void onClick(View v) {
            int i = v.getId();
            if (i == R.id.btnCancel) {//取消
                dismiss();
    
            } else if (i == R.id.btnSubmit) {//确定
                //选中的返回
                if (submitListener != null && flowLayoutWCView != null) {
                    submitListener.onSubmit(flowLayoutWCView.selectList());
                }
                dismiss();
    
            } else if (i == R.id.bt_selectAll) {//全选
                flowLayoutWCView.selectAllTv();
                bt_selectNum.setText("选中了" + flowLayoutWCView.selectNum() + "个");
    
            } else if (i == R.id.bt_cancleAll) {//重置
                flowLayoutWCView.cancleAllSelect();
    
            }
        }
    
        //确定按钮事件回调
        public interface OnSubmitListener {
            void onSubmit(List<String> str);
        }
    
        public void setOnSubmitListener(OnSubmitListener submitListener) {
            this.submitListener = submitListener;
        }
    }
    
  2. flwc_window悬浮框对应的布局(放重点的,其它省略)
        <com.example.myapplication.ui.FlowLayoutWCView
            android:id="@+id/flwc_view"
            android:layout_width="match_parent"
            android:background="@color/gray1"
            android:layout_height="wrap_content"
            app:layout_constraintTop_toBottomOf="@id/btnCancel"
            app:maxNum_wc="0"
            app:text_margins_horizontal="30"
            app:text_margins_vertical="30"
            app:text_padding_horizontal="20"
            app:text_padding_vertical="20" />
  3. FlowLayoutWCView(重点FlowLayout自定义控件)
    /**
     * FlowLayout流式布局+固定文本/按钮(在最右边固定位置,若最后一行放得下,那就放,放不下就重新开一行放在新行的最右边)
     * 动态绘制的布局大小是LayoutParams.WRAP_CONTENT
     */
    public class FlowLayoutWCView extends ViewGroup {
        private String TAG = FlowLayoutWCView.class.getSimpleName();
        private Context context;
    
        private List<String> dataList = new ArrayList<>();  //数据源
        //动态绘制的flowlayout布局内容textView,可以设置选中,不选中
        private List<TextView> tvList = new ArrayList<>();
        /**
         * textView 是否选中boolean,方便后面重置、全选,全不选、指定被选等需求.
         * 这里的key跟dataList、tvList的索引是一致的,就可以通过dataMap是否选中得到key进而得到dataList具体的内容
         * eg:Boolean为true就是选中的,可以得到key是1,那么dataList.get(1)就可以知道具体被选中的string,也可以将tvList.get(1)设置为不选中setSelected(false)
         */
        private Map<Integer, Boolean> dataMap = new HashMap<>();
    
        //单选时,被选中的位置(跟dataList、tvList、dataMap存的顺序是一样的),用于判断是否需要清除选中的这个,-1当前没有选中
        private int nowSelectIndex = -1;
    
        private OnUpdateListener onUpdateListener;//选中情况的更新事件回调,每一次点击的情况,返回当前被选中的list
        private List<String> selectList;//选中的list内容
    
        private int maxNum;// 0不限制数量,1单选,大于1的限制选择数量
        private int textStyle;//text基本样式,字体大小,颜色
        private int text_padding_vertical;//上下内边距
        private int text_padding_horizontal;//上下内边距
        private int text_margins_vertical;//上下外边距
        private int text_margins_horizontal;//左右外边距
        private int textBg;//文本的背景,需要圆角,选中未选中状态等用drawable文件里面实现,只是背景颜色颜色可以color
    
        //是否有固定在最后一行右边的按钮/文字。放不下就换行(true即表示有,且默认最后一个数据就是固定的文本/按钮)
        private boolean isRightBt = false;
    
    //    //实例化时调用
    //    public FlowLayoutWCView(Context context) {
    //        super(context);
    //        this.context=context;
    //    }
    
        //xml中定义会调用
        public FlowLayoutWCView(Context context, AttributeSet attrs) {
            super(context, attrs);
            this.context = context;
    
            TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.FlowLayoutWCView);
            maxNum = ta.getInteger(R.styleable.FlowLayoutWCView_maxNum_wc, 0);//单选,有限制选中,没有限制选择
            textStyle = ta.getResourceId(R.styleable.FlowLayoutWCView_text_wc, R.style.default_text_style_wc);//文本的样式,字体大小、颜色这些
            textBg = ta.getInteger(R.styleable.FlowLayoutWCView_textBg_wc, R.drawable.flow_layout_wc_style);//需要圆角,选中未选中状态等用drawable,单颜色可以color
            text_padding_vertical = ta.getInteger(R.styleable.FlowLayoutWCView_text_padding_vertical, 10);//上下内边距
            text_padding_horizontal = ta.getInteger(R.styleable.FlowLayoutWCView_text_padding_horizontal, 10);//上下内边距
            text_margins_vertical = ta.getInteger(R.styleable.FlowLayoutWCView_text_margins_vertical, 20);//左右外边距
            text_margins_horizontal = ta.getInteger(R.styleable.FlowLayoutWCView_text_margins_horizontal, 20);//左右外边距
            ta.recycle();
        }
    
    //    //在构造函数中主动调用的  defStyleAttr是默认的Style是指它在当前Application或Activity所用的Theme中的默认Style
    //    public FlowLayoutWCView(Context context, AttributeSet attrs, int defStyleAttr) {
    //        super(context, attrs, defStyleAttr);
    //        this.context = context;
    //    }
    
        /**
         * xml布局时match_parent或者wrap_content,如果只super()不对宽高测量:控件的大小是由父控件决定的,一般就是会填充父容器。
         * 对自定义viewGroup的宽高进行测量
         * 行宽:新增view,加了后宽度没有超过GroupView最大宽度,则添加;否则换行,新一行行宽就是该view的测量宽度,有行间距时,需要加上行间距
         * 行高:本行子最高view高度,没有换行就一直对比新增的view高度,谁大取谁。换行时,行高=换行后第一个view高度。有行间距时需要加上。
         * 期望的总高度:height设置wrap_content 时,ViewGroup的上下内间距 + 行高…+行高
         * 注:addView、setVisbility、setTextView等方法会重新调用 requestLayout,会重新测量、重新摆放、重新绘制view,影响性能,因此尽量少用
         *
         * @param widthMeasureSpec
         * @param heightMeasureSpec
         */
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            int widthSize = MeasureSpec.getSize(widthMeasureSpec);//宽度测量大小
    
            int height = 0;//动态布局的总高度
            int lineWidth = 0;//行宽
            int lineHeight = 0;//行高
    
            int childWidth;//子view的宽度
            int childHeight;//子view的高度
    
            //循环遍历子View进行测量
            for (int i = 0; i < getChildCount(); i++) {//getChildCount()获取child的数量
                View child = getChildAt(i);//获取child view
                //测量子view的宽高
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
                // 得到child的lp
                MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
                if (lp == null) {
                    continue;
                }
                childWidth = child.getMeasuredWidth();//包含child的padding值
                childHeight = child.getMeasuredHeight();
    
                //新加子view后,宽度还没有大于widthSize,就是当前行放得下,不用换行
                if (lineWidth + childWidth + lp.leftMargin + lp.rightMargin <= widthSize) {
                    lineWidth += childWidth + lp.leftMargin + lp.rightMargin;//更新行宽,记得加上child的左右外边距
                    lineHeight = Math.max(lineHeight, childHeight + lp.topMargin + lp.bottomMargin);//行高,一直跟新view+上下外边距对比,哪个大要哪个
                } else {//换行
                    height += lineHeight;//动态布局的总高度=换行前那行的行高,新行的行高还需要跟后面的新child对比大小,所以一直缺少最后一行的行高,需要额外处理
                    lineWidth = childWidth + lp.leftMargin + lp.rightMargin;//新行宽度=child的测量宽度+左右外边距
                    lineHeight = childHeight + lp.topMargin + lp.bottomMargin;//新行的行高=child的测量宽度+上下外边距
                }
                //添加最后一行的高度,因为前面是只有换行时才加行高,最后一行的高度没有加上
                if (i == getChildCount() - 1) {
                    height += lineHeight;//总高度加上最后一行行高,不需要加上下外边距,因为行高赋值时已经加过了
                }
            }
            LogUtil.e(TAG, "onMeasure()方法里面的,onLayout方法布局时的位置超过了此宽高的部分不显示,widthSize=" + widthSize + ",height=" + height);
            /**
             * 模式
             * MeasureSpec.UNSPECIFIED:父不约束子view大小; 如ListView; 注 自定义view一般用不到
             * MeasureSpec.EXACTLY:父为子指定确切尺寸,子大小必须在改尺寸内; 如match_parent,具体数字50dp
             * MeasureSpec.AT_MOST:父为子指定最大尺寸,所以子必须适应在改尺寸内; 如wrap_content
             *
             * MeasureSpec.getSize(widthMeasureSpec);//宽度测量大小:占测量规格(MeasureSpec)的低30位
             * MeasureSpec.getMode(widthMeasureSpec);//宽度测量模式 :占测量规格(MeasureSpec)的高2位
             */
            /**因为在xml布局中宽是match_parent,高是wrap_content,所以直接用宽度测量大小,自己测量出来的高度;
             *如果不知道模式是什么,那么需要对比了,宽/高测量模式 == MeasureSpec.EXACTLY ? 宽/高测量大小 : 宽/高自己测量出来大小
             */
            setMeasuredDimension(widthSize, height);//存储计算得到的宽高
        }
    
    
        /**
         * 确定控件现在在哪个位置,view.layout(左,上,右,下)
         * 布局,计算view起始位置,顶部偏移量,左侧偏移量(每个child的位置(超出onMeasure宽高部分不显示))
         * 行高:当前行最高高度,换行时,顶部偏移量需要加上该行行高+行间距,更新新行高的为新view的高度
         * child顶部偏移量:view的top位置,同行的顶部偏移量一样;换行时,新行的顶部偏移 = 当前偏移量+当前行高
         * child左侧偏移量:view的left位置,新view的左侧偏移距离 = 当前左侧偏移 + 当前子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 width = r - l;//可布局的宽 getWidth,值都是一样的
    
            //child 左偏移量
            int childLeftOffset = 0;
            //child 顶部偏移量
            int childTopOffset = 0;
    
            int lineWidth = 0;//行宽
            int lineHeight = 0;//行高
    
            int childWidth = 0;//子view 的宽
            int childHeight = 0;//子view 的高
    
            //遍历view
            for (int i = 0; i < getChildCount(); i++) {
                View child = getChildAt(i);
                //过滤gone
                if (child.getVisibility() == View.GONE) {
                    continue;
                }
                //强制转换,可能导致空指针和类型转换异常
                MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();//可以获取边距
                if (lp == null) {
                    continue;
                }
                //获取child的宽高
                childWidth = child.getMeasuredWidth();//包含child的padding值
                childHeight = child.getMeasuredHeight();
                //左右margin外边距,不换行
                if (lineWidth + childWidth + lp.leftMargin + lp.rightMargin <= width) {
                    lineWidth += childWidth + lp.leftMargin + lp.rightMargin;
                    lineHeight = Math.max(lineHeight, childHeight + lp.topMargin + lp.bottomMargin);//行高取本行最高的height
                } else {//换行了,此时child就是新行的第一个,那么child的宽高就是新行的数据了
                    childLeftOffset = 0;//初始化
                    lineWidth = childWidth + lp.leftMargin + lp.rightMargin;//需要加上左右边距
                    lineHeight = childHeight;
                    childTopOffset += lineHeight + lp.topMargin + lp.bottomMargin;//换行了,顶部偏移量是childHeight+上下边距(这是没换行的上下边距)
                }
    
                //计算childView的left,top,right,bottom
                int lc = childLeftOffset + lp.leftMargin;//自身带的左边距+左偏移量=自身左边实际的位置
                int tc = childTopOffset + lp.topMargin;//自身带的上边距+顶部偏移量=自身顶部实际的位置
                int rc = lc + childWidth;
                int bc = tc + childHeight;
    
                //显示固定文本/按钮,且是最后一个,特殊处理,最右边固定位置,若最后一行放得下,那就放,放不下就重新开一行放在新行的最右边(改固定文本/按钮位置,按需改左上右下四个值即可)
                if (isRightBt && i == getChildCount() - 1) {
                    int left = width - childWidth - lp.leftMargin;
                    int right = left + childWidth;
                    child.layout(left, tc, right, bc); //child的位置
                    Log.e(TAG, "child.layout布局" + " , l = " + left + " , t = " + tc + " , r =" + right + " , b = " + bc);
    
                } else {//否则,按照正常的来放位置
                    child.layout(lc, tc, rc, bc); //child的位置
                    Log.e(TAG, "child.layout布局" + " , l = " + lc + " , t = " + tc + " , r =" + rc + " , b = " + bc);
                }
    
                childLeftOffset += childWidth + lp.leftMargin + lp.rightMargin;//下一个child 的左侧偏移量=childWidth+自己的左右外边距
            }
        }
    
    
        /**
         * 设置数据
         *
         * @param list 需要展示的数据
         */
        public void setDataList(List<String> list) {
            dataList.clear();
            tvList.clear();
            dataList.addAll(list);
            for (int i = 0; i < dataList.size(); i++) {
                //初始化所有的点都为未选中
                String data = dataList.get(i);
                TextView tv = null;
                //显示固定文本/按钮,且是最后一个数据,特殊处理最后一个数据,因为最后一个数据固定在最后一行(若放不下就另起一行)右边(具体位置,可以控制)
                if (isRightBt && i == dataList.size() - 1) {
                    tv = lastText(data);
                } else {//否则正常的来
                    dataMap.put(i, false);//tv的选中与否状态,最后一个tv不需要加进来
                    tv = newTv(data, i);//代码绘制TextView
                    tvList.add(tv);//后续选中、不选中,最后一个tv不需要加进来
                }
                addView(tv, i);
            }
            invalidate();
        }
    
        /**
         * TextView初始化方法(非固定文本/按钮)
         *
         * @param dataStr  数据
         * @param position 位置
         * @return TextView
         */
        private TextView newTv(final String dataStr, final int position) {
            final TextView tv = new TextView(context);
            LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
            lp.setMargins(text_margins_horizontal, text_margins_vertical, text_margins_horizontal, text_margins_vertical);//外边距
            tv.setPadding(text_padding_horizontal, text_padding_vertical, text_padding_horizontal, text_padding_vertical);//内边距
            tv.setLayoutParams(lp);
            tv.setText(dataStr);
            tv.setGravity(Gravity.CENTER);//center
            tv.setTextAppearance(context, textStyle);
            tv.setBackgroundResource(textBg);
    
            tv.setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (maxNum == 0) { //无限选择
                        if (dataMap.get(position)) {//前面就选中了,那么现在取消选中
                            tv.setSelected(false);
                            dataMap.put(position, false);
    
                        } else {//没有选中
                            tv.setSelected(true);
                            dataMap.put(position, true);
                        }
    
                    } else if (maxNum == 1) {//单选
                        if (dataMap.get(position)) {//前面就选中了,那么现在取消选中
                            nowSelectIndex = -1;
                            tv.setSelected(false);
                            dataMap.put(position, false);
    
                        } else {//没有选中,那么取消已选中的,让这个选中
                            //有选中的,那么取消已选中的
                            if (nowSelectIndex != -1) {
                                cancleSelect(nowSelectIndex);
                            }
                            nowSelectIndex = position;//单选时,被选中的是第几个
                            tv.setSelected(true);
                            dataMap.put(position, true);
                        }
    
                    } else {//有限选择
                        if (dataMap.get(position)) {//前面就选中了,那么现在取消选中
                            tv.setSelected(false);
                            dataMap.put(position, false);
                        } else {
                            if (selectNum() < maxNum) {//还没有达到上限
                                tv.setSelected(true);
                                dataMap.put(position, true);
                            } else {//已达到上限,不能再选了
                                Toast.makeText(context, "已达上限,不能再选“" + dataStr + "”", Toast.LENGTH_SHORT).show();
                            }
                        }
                    }
                    onUpdateListener.updateSelect(selectList());//监听选中的情况
                }
            });
    
            return tv;
        }
    
        /**
         * 为最后一个数据设置另外的样式(宽高\setMargins\setPadding这些跟tv大小相关的设置和newTv一致时方便计算child.layout()),
         * 若不一样则最后一个child.layout()四个参数需要根据实际情况重新算)
         *
         * @param dataStr
         * @return
         */
        private TextView lastText(String dataStr) {
            TextView tv = new TextView(context);
            LinearLayout.LayoutParams lp1 = new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);//外边距
            lp1.setMargins(text_margins_horizontal, text_margins_vertical, text_margins_horizontal, text_margins_vertical);//外边距
            tv.setPadding(text_padding_horizontal, text_padding_vertical, text_padding_horizontal, text_padding_vertical);//内边距
            tv.setLayoutParams(lp1);
            tv.setText(dataStr);
            tv.setGravity(Gravity.CENTER);
            tv.setTextAppearance(context, textStyle);
            tv.setTextColor(Color.BLUE);
            tv.setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View v) {
                    cancleAllSelect();//固定文本/按钮事件,这里是重置,所以是cancleAllSelect()方法,具体什么方法看要求
                }
            });
            return tv;
        }
    
        //是否显示固定文本/按钮,若true则特殊处理最后一个TextView的样式、位置
        public void isShowRightBt(boolean isRightBt) {
            this.isRightBt = isRightBt;
        }
    
    
        /**
         * 取消指定选中
         *
         * @param i
         */
        public void cancleSelect(int i) {
            if (tvList != null && tvList.size() > i) {//指定的这个数字,必须在tvList里面才行
                dataMap.put(i, false);
                tvList.get(i).setSelected(false);
            } else {
                Toast.makeText(context, "指定取消项不存在,请重新选择!", Toast.LENGTH_SHORT).show();
            }
        }
    
        /**
         * 指定取消选中哪些(List<Integer>中Integer存的值要跟tvList的顺序一致,eg:指定第2,4个不选中,那么list{2,4},就是要tvList的第2,4个更改为未选中状态)
         *
         * @param list
         */
        public void cancleSelect(List<Integer> list) {
            if (tvList != null && list != null) {
                for (int i = 0; i < list.size(); i++) {
                    if (tvList.size() >= list.get(i)) {//指定的这个,要存在
                        dataMap.put(list.get(i) - 1, false);// tvList.size(),是从0开始的,第几个第几个是从1开始的,所以减1
                        tvList.get(list.get(i) - 1).setSelected(false);
                    } else {
    
                    }
                }
            }
        }
    
        /**
         * 取消全部选中的(置空)
         */
        public void cancleAllSelect() {
            if (tvList != null && tvList.size() > 0) {
                for (int i = 0; i < tvList.size(); i++) {
                    dataMap.put(i, false);
                    tvList.get(i).setSelected(false);
                }
            }
            onUpdateListener.updateSelect(selectList());//监听选中的情况
        }
    
        /**
         * 指定选中哪些(List<Integer>中Integer存的值要跟tvList的顺序以至,eg:指定第2,4个为选中,那么list{2,4},就是要tvList的第2,4个更改为选中状态)
         *
         * @param list
         */
        public void selectAppointTv(List<Integer> list) {
            if (tvList != null && list != null) {
                for (int i = 0; i < list.size(); i++) {
                    if (tvList.size() >= list.get(i)) {//指定的这个,要存在
                        dataMap.put(list.get(i) - 1, true);// tvList.size(),是从0开始的,第几个第几个是从1开始的,所以减1
                        tvList.get(list.get(i) - 1).setSelected(true);
                    } else {
    
                    }
                }
            }
        }
    
        /**
         * 指定选中tvList的前几个
         *
         * @param num
         */
        public void selectAppointTv(int num) {
            if (tvList != null && tvList.size() >= num) {
                for (int i = 0; i < num; i++) {
                    dataMap.put(i, true);
                    tvList.get(i).setSelected(true);
                }
            }
        }
    
        /**
         * 指定选中tvList的第几个(从1开始)
         *
         * @param num
         */
        public void selectTv(int num) {
            if (tvList != null && tvList.size() >= num) {//指定的这个数字,必须在tvList里面才行
                dataMap.put(num - 1, true);
                tvList.get(num - 1).setSelected(true);
            } else {
                Toast.makeText(context, "指定选中项不存在,请重新选择!", Toast.LENGTH_SHORT).show();
            }
        }
    
        /**
         * 全部选中(分单选、限制选择、不限制选择的情况)
         */
        public void selectAllTv() {
            if (tvList != null && tvList.size() > 0) {
                if (maxNum == 0) {//没有限制选择,就全部选中
                    selectAppointTv(tvList.size());
    
                } else if (maxNum == 1) {//单选,就选中第一个(也可以随机选择一个,从tvList.size()-1中随机选一个数字,)
                    selectAppointTv(1);
    
                } else {//有限制选择
                    if (tvList.size() >= maxNum) {//能选的数据数量,大于等于限制选的数量,eg:有7个选项,限制选择5个或7个(也可以随机选择maxNum个)
                        selectAppointTv(maxNum);
    
                    } else {//能选的数据数量,小限制选的数量,eg:有7个选项,限制选择8个,此时就全选了
                        selectAppointTv(tvList.size());
                    }
                }
            }
        }
    
        /**
         * 选中的数量
         *
         * @return
         */
        public int selectNum() {
            int num = 0;
            for (Map.Entry<Integer, Boolean> entry : dataMap.entrySet()) {//类型 : 循环对象   超级循环
                if (entry.getValue()) {//获取的是dataMap的value值boolean,key值是Integer
                    num++;
                }
            }
            return num;
        }
    
        /**
         * 选中的list
         *
         * @return
         */
        public List<String> selectList() {
            selectList = new ArrayList<>();
            for (Map.Entry<Integer, Boolean> entry : dataMap.entrySet()) {//类型 : 循环对象   超级循环
                if (entry.getValue()) {//获取的是dataMap的value值boolean,key值是Integer
                    selectList.add(dataList.get(entry.getKey()));//dataList存的顺序跟dataMap的key是一致的
                }
            }
            return selectList;
        }
    
    
        /**
         * 需要支持margin,所以使用系统的MarginLayoutParams
         */
        public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attributeSet) {
            return new MarginLayoutParams(getContext(), attributeSet);
        }
    
        /**
         * 设置默认的MarginLayoutParams
         */
        @Override
        protected LayoutParams generateDefaultLayoutParams() {
            return new MarginLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
        }
    
        public interface OnUpdateListener {
            void updateSelect(List<String> list);
        }
    
        public void setOnUpdateListener(OnUpdateListener listener) {
            this.onUpdateListener = listener;
        }
    
    }
    
  4. attrs.xml里的FlowLayoutWCView
        <declare-styleable name="FlowLayoutWCView">
            <attr name="maxNum_wc" format="integer" />
            <attr name="text_wc" format="integer" />
            <attr name="text_padding_vertical" format="integer" />
            <attr name="text_padding_horizontal" format="integer" />
            <attr name="text_margins_vertical" format="integer" />
            <attr name="text_margins_horizontal" format="integer" />
            <attr name="textBg_wc" format="integer" />
        </declare-styleable>
  5. 使用
            tfMonthList = new ArrayList<>();
            for (int i = 1; i <= 10; i++) {
                tfMonthList.add("周" + i);
            }
            tfMonthList.add("重置");//固定文本/按钮,特殊处理的这个,必须放在数据源的最后
    
            flwcWindow = new FLWCWindow(this);
            flwcWindow.setFl_Title("周几");
            flwcWindow.isShowRightBt(true);//是否显示固定文本/按钮默认是false,也可以通过attrs实现
            flwcWindow.setDataList(tfMonthList);//isShowRightBt必须在前面,不然是否显示无效,因为设置数据时就需要判断是否显示固定文本/按钮了
            flwcWindow.setOnSubmitListener(new FLWCWindow.OnSubmitListener() {
                @Override
                public void onSubmit(List<String> str) {
                    Toast.makeText(FlowLayoutActivity.this,"您一共选中了"+str.size()+"个分别是:"+str.toString(),Toast.LENGTH_SHORT).show();
                }
            });
            flwcWindow.show();

扩展 

  1. 在数据长度都差不多,为了美观,可以固定TextView的宽高(newTv、lastText方法里面),新增方法,在外面设置宽高。eg:周几这些长度一样,就可以固定宽高

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值