实现:动态绘制并带固定文本/按钮,固定文本/按钮固定在最后一行的右边且垂直居中,若最后一行放不下,则新开一行放到新行的右边且垂直居中(新行的行高跟前面的一样),可单选、多选、重置。
注:若不要求放到最后一行,只放在FlowLayout的下面,则可以在xml布局放个文本/按钮实现。
思路:FlowLayout流式布局,把固定文本/按钮的数据添加到要绘制的内容最后面,在绘制时,对最后一个进行特别处理样式,事件等。
效果图如下(粉色区域就是动态绘制的):
图1:最后一行放得下固定文本/按钮,就放在最后一行右边且跟最后一行垂直居中
图2:实时知道选择的情况,单选、多选、取消选中等 (自定义监听回调)
图3:最后一行放不下,新开一行放到新行的右边且垂直居中(新行的行高跟前面的一样)
前提准备:
- 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宽高进行测量 ………………………………………………………………………………………………………………………………………… }
- 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就会在这个位置显示 } }
-
单选、多选、取消选中等,重点是保存好每个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的限制选择数量
代码
- 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; } }
- 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" />
- 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; } }
- 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>
- 使用
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();
扩展
- 在数据长度都差不多,为了美观,可以固定TextView的宽高(newTv、lastText方法里面),新增方法,在外面设置宽高。eg:周几这些长度一样,就可以固定宽高