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

news2024/11/28 6:43:09

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

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

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

图2:实时知道选择的情况,单选、多选、取消选中等 (自定义监听回调)

图3:最后一行放不下,新开一行放到新行的右边且垂直居中(新行的行高跟前面的一样)

 前提准备:

  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的限制选择数量

 代码

  1. FLWCWindow悬浮框
    public class FLWCWindow implements View.OnClickListener {
        private FrameLayout frameLayout;
        private PopupWindow popupWindow;
        private Context context;
        private List<String> list;
        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, List<String> list) {
            this.context = context;
            this.list = list;
            initView();
        }
    
        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.setDataList(list);//设置数据源
            //时刻监听flowLayoutWCView通过点击选中的状态(非全选/重置,也可以实现在selectAllTv、cancleAllSelect新增事件回调即可,)
            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);
        }
    
    
        //显示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();
                bt_selectNum.setText("选中了0个");
    
            }
        }
    
        //确定按钮事件回调
        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流式布局+固定文本/按钮(在最右边固定位置,若最后一行放得下,那就放,放不下就重新开一行放在新行的最右边)
     * 慎用 addView、setVisbility、setTextView等方法,因为这几个方法会重新调用 requestLayout,会重新测量、重新摆放、重新绘制view,影响性能
     * 动态绘制的布局大小是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
        private boolean isRightBt = true;//是否有固定在最后一行右边的按钮/文字(样式后续自己赋值),默认不显示(放不下就换行)
    
    //    //实例化时调用
    //    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,值都是一样的1080
    
            //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;
    
                Log.e(TAG, "child信息"  + " , childLeftOffset =" + childLeftOffset + " , childTopOffset = " + childTopOffset + " , childWidth = " + childWidth + " , childHeight = " + childHeight);
                if (i != getChildCount() - 1) {//不是最后一个的位置的处理
                    child.layout(lc, tc, rc, bc); //child的位置
                    Log.e(TAG, "child.layout布局" + " , l = " + lc + " , t = " + tc + " , r =" + rc + " , b = " + bc);
    
                } else {//是最后一个,特殊处理,最右边固定位置,若最后一行放得下,那就放,放不下就重新开一行放在新行的最右边(改固定文本/按钮位置,按需改左上右下四个值即可)
                    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);
                }
    
                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 (i != dataList.size() - 1) {
                    dataMap.put(i, false);//tv的选中与否状态,最后一个tv不需要加进来
                    tv = newTv(data, i);//代码绘制TextView
                    tvList.add(tv);//后续选中、不选中,最后一个tv不需要加进来
    
                } else {//特殊处理最后一个数据,因为最后一个数据固定在最后一行(若放不下就另起一行)右边(具体位置,可以控制)
                    tv = lastText(data);
                }
                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();
                    onUpdateListener.updateSelect(selectList());//监听选中的情况
                }
            });
            return tv;
        }
    
    
        /**
         * 取消指定选中
         *
         * @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);
                }
            }
        }
    
        /**
         * 指定选中哪些(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, tfMonthList);
            flwcWindow.setFl_Title("周几");
            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:周几这些长度一样,就可以固定宽高

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/364311.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SQL零基础入门学习(六)

SQL零基础入门学习&#xff08;六&#xff09; SQL零基础入门学习&#xff08;五&#xff09; SQL 通配符 通配符可用于替代字符串中的任何其他字符。 SQL 通配符用于搜索表中的数据。 在 SQL 中&#xff0c;可使用以下通配符&#xff1a; 演示数据库 在本教程中&#xff…

robotframework自动化测试环境搭建

环境说明 win10 python版本&#xff1a;3.8.3rc1 安装清单 安装配置 selenium安装 首先检查pip命令是否安装&#xff1a; C:\Users\name>pipUsage:pip <command> [options]Commands:install Install packages.download Do…

掌握这10个测试方法,软件测试已登堂入室

当然还有很多测试方法&#xff0c;这些要根据实际不同应用场景而变化&#xff0c;这里就以键盘为例子进行测试方法的讲解。 1.需求测试 需求这种大家都知道这种主要是就是甲方或者项目经理写的&#xff0c;或者某些人需要什么我们就给什么&#xff0c;一般来讲一个东西给到…

API 接口应该如何设计?如何保证安全?如何签名?如何防重?

说明&#xff1a;在实际的业务中&#xff0c;难免会跟第三方系统进行数据的交互与传递&#xff0c;那么如何保证数据在传输过程中的安全呢&#xff08;防窃取&#xff09;&#xff1f;除了https的协议之外&#xff0c;能不能加上通用的一套算法以及规范来保证传输的安全性呢&am…

训练一个ChatGPT需要多少数据?

“风很大”的ChatGPT正在席卷全球。作为OpenAI在去年底才刚刚推出的机器人对话模型&#xff0c;ChatGPT在内容创作、客服机器人、游戏、社交等领域的落地应用正在被广泛看好。这也为与之相关的算力、数据标注、自然语言处理等技术开发带来了新的动力。自OpenAI发布ChatGPT以来&…

好用的SQL工具盘点:从学习到工作总有一款适合你

标题一.入坑阶段&#xff08;学习入门&#xff09;&#xff1a; 这个阶段一般就是小白&#xff0c;想学习SQL语言&#xff0c;然后到处找软件&#xff0c;找免费破解版找半天&#xff0c;找到了半天安装不下来&#xff0c;还可能把自己电脑搞中毒。 其实对于小白来说&#xf…

Shiro常用的Filter过滤器

Shiro常用的Filter过滤器 核心过滤器&#xff1a;DefaultFilter&#xff0c;配置了的相应路径的相应的拦截器进行处理 常用过滤器 authc&#xff1a;org.apache.shiro.web.filter.authc.FormAuthenticationFilter 需要认证登录才能访问 user&#xff1a;org.apache.shiro.w…

动作识别、检测、分割、解析相关数据集介绍

文章目录动作识别UCF101(UCF101 Human Actions dataset)Kinetics (Kinetics Human Action Video Dataset)动作检测 / 时序动作定位CharadesActivityNetMulti-THUMOSUCF101-24IKEA ASM动作分割Breakfast (The Breakfast Actions Dataset)GTEA (Georgia Tech Egocentric Activity…

谷粒学院项目对应知识点

1、数据库分库分表 1.1、业务分库 按照业务模块将数据分散到不同的数据库服务器&#xff0c;比如用户数据、商品数据、订单数据存放在三个不同的数据库服务器中&#xff0c;分散存储和访问压力。但也会带来一些问题&#xff1a; join 操作问题&#xff1a;业务分库后&#x…

[qiankun]-多页签缓存

[qiankun]-多页签缓存环境功能需求多页签缓存方案方案1.主服务进行html替换方案2.微服务vnode 替换方案3.每个微服务都不卸载微服务加载方式的选择微服务的路由路径选择微服务的缓存工具微服务的容器使用tab作为微服务的挂载容器使用微服务路由作为微服务的挂载容器场景描述微服…

day18_集合_List

今日内容 上课同步视频:CuteN饕餮的个人空间_哔哩哔哩_bilibili 同步笔记沐沐霸的博客_CSDN博客-Java2301 零、 复习昨日 一、集合框架体系 二、Collection 三、泛型 四、迭代 五、List 六、ArrayList 七、LinkedList 零、 复习昨日 throw和throws什么区别 throwthrows位置方法…

Word中批量调整图片大小

当一个文档中图片较多&#xff0c;又需要调整图片大小时&#xff0c;这时可以通过“宏”执行代码来批量调整。打开一个Word文档。“AltF8"键打开宏。设置“宏名”&#xff0c;并单击“创建”。创建完宏后&#xff0c;将进入Visual Basic 编辑器界面。在代码编辑区全选&…

WebDAV之π-Disk派盘+文件管理器

文件管理器 支持WebDAV方式连接π-Disk派盘。 推荐一款iOS上的免费文件管理器新秀。 文件管理器这是一款功能强大的文件管理工具,支持zip,rar,7z等压缩包的解压和压缩,支持小说,漫画,视频下载及播,极大提升日常办公,娱乐,文件管理的工作效率,使得文档的归档和管理随心…

Android system实战 — Android R(11) 进程保活白名单

Android system实战 — Android R 进程保活白名单0. 前言1. 具体实现1.1 准备工作1.2 源码实现1.2.1 源码1.2.2 diff文件0. 前言 最近在Android R上实现一些需求&#xff0c;进行记录一下&#xff0c;关于进程保活的基础知识可以参考Android system — 进程生命周期与ADJ&#…

纯手动搭建hadoop3.x集群记录002_安装hadoop3.x_创建xsync分发工具_配置ssh免密登录---大数据之Hadoop3.x工作笔记0164

1.首先我有 172.19.126.117 母机 对应 172.19.126.122 虚拟机 172.19.126.116 母机 对应 172.19.126.121 虚拟机 172.19.126.115 母机 对应 172.19.126.120 虚拟机 对吧,然后我们从115上 开始安装hadoop 2. 然后走到这个opt/software文件夹中,然后解压 hadoop-3.1.3.tar.…

nn.Dropout随机丢神经元的用法

前言&#xff1a; pytorch与tensorflow中均有nn.dropout,两者之间的使用方法&#xff0c;下面将介绍。 一、torch.nn.dropout 说明文档&#xff1a; r"""During training, randomly zeroes some of the elements of the input tensor with probability :att…

人员行为识别系统 TensorFlow

人员行为识别系统人员行为识别系统通过TensorFlow深度学习技术&#xff0c;人员行为识别算法对画面中区域人员不按要求穿戴、违规抽烟打电话、睡岗离岗以及作业流程不规范实时分析预警&#xff0c;发现违规行为立即抓拍告警。深度学习应用到实际问题中&#xff0c;一个非常棘手…

2.22 位运算

位运算 预备知识 首先注意二进制位数从0开始记录&#xff0c;即 15&#xff08;1111&#xff09;&#xff0c;位数分别为3210 左移和右移 左移 << 将某个数的二进制向左移动几位&#xff0c;低位补0 如5>>2 即20&#xff0c;左移k位即相当于乘上2的k次方 右移 >…

C++学习笔记-数组

所谓数组&#xff0c;就是相同数据类型的元素按一定顺序排列的集合&#xff0c;就是把有限个类型相同的变量用一个名字命名&#xff0c;然后用编号区分他们的变量的集合&#xff0c;这个名字称为数组名&#xff0c;编号称为下标。组成数组的各个变量称为数组的分量&#xff0c;…

[面向小白]一篇博客带你认识什么是栈以及如何手撕一个栈

目录 0.前言 1.什么是栈 2.实现栈所选择的基本结构 3.认识栈的小练习 4. 用代码实现一个栈 4.1 用什么可以描述出一个栈 4.2栈接口的设计原则 4.3栈的初始化 4.4栈的插入 4.5 栈的删除 4.6 栈的判空 4.7栈的有效元素的数量 4.8取出栈顶元素 4.9栈的销毁 5. 对实…