之前为了图方便,用过很多其他人写的流式布局,也基本满足了当时的要求。但是这次的要求有点多:可展开和收缩,限制行数、最初展示的最多为两行。接到需求第一步,网上找一个满足功能的FlowLayout,确实找到了几个但是都存在不一样的问题。以下就说一下遇到的问题,不知道你遇到过没有:
1.因为每个Item的TextView为wrap_content,所以当文本本身特别长的时候就会出现问题,尤其是当TextView设置了圆角背景的时候,你会发现TextView的右侧圆弧边没有了,因为子控件的实际宽度大于他的父控件。就算你设置ellipsize都没用。
2.数据显示会出现问题,比如展开的时候本应该显示4行,最终却只显示三行,当限制最多显示5行的时候,ImageView会出现单独占据第6行的情况。
废话不多说,直接上代码:
以下分别为两个布局文件
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="@dimen/dp_35"
android:layout_margin="@dimen/dp_6"
android:maxLines="1"
android:ellipsize="end"
android:background="@drawable/radio18_f8f8f8_bg"
android:gravity="center_vertical"
android:paddingLeft="@dimen/dp_12"
android:paddingRight="@dimen/dp_12"
android:textColor="@color/black"
android:textSize="@dimen/sp_14"
android:textStyle="bold">
</TextView>
<?xml version="1.0" encoding="utf-8"?>
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="@dimen/dp_35"
android:layout_margin="@dimen/dp_4"
android:background="@drawable/radio18_f8f8f8_bg"
android:paddingLeft="@dimen/dp_12"
android:paddingRight="@dimen/dp_12"
android:src="@mipmap/search_open">
</ImageView>
/**
* 1.设置最初可展示的行数,如果实际的View超过个行数的时候
* 显示 “可展开View”, 如果不超过这个行数,则不显示 “可展开View”
* 2.设置最大可展示的行数,超过的不再展示更多View,不超过的但是查过最
* 初可展示的行数时,需要显示 “可回收View”
* 3.两次测量,第一次测量,所有的View,并且知道所有的数据信息,包括一共的行数,每行的个数,这时候展示所有的
* View,但是这一次不展示给用户看。第二次测量,根据第一次测量的信息,展示View
*/
public class FlowRecordLayout extends ViewGroup {
private LayoutInflater mInflater;
//最多可展示的View
private List<List<View>> mRowViews = new ArrayList<>();
//当前行的View
private List<View> currentViewList = new ArrayList<>();
//记录每行的行高
private List<Integer> lineHeights = new ArrayList<>();
/**
* 图片
*/
private ImageView imgExpand;
/**
* 是否有图片,只有一种情况没有图片
* 分析几种状态
* 1.初次测量,添加 {@link ImageView},测量宽度高度,方便计算展开时的位置和收缩时的位置。
* 2.记录展开时
* 3.记录收缩时
* 4.记录不足的数量没有填充慢基础行 (只有这种情况下,才会没有图片)
*/
private boolean mHasImg;
/**
* 是否为初次测量
*/
private boolean mInitMeasure = true;
//未展开的时候,ImageView显示的坐标---初步测量的时候,记录的行数超过基础行
private int closeExpandIndex;
//展开的时候,ImageView展示的坐标---初步测量的时候,记录超过最大行,当数据超过基础行,但是不超过最大行
// 的时候这个数一直为 0.所以这个数值并不是指所有的展开时候的位置
private int openExpandIndex;
/**
* 基础可展示的行数,也可以理解为回收状态下最大可展示的行数
*/
private int mBaseLineCount;
/**
* 基础行数固定,也可以通过属性设置 看 mBaseLineCount 属性
*/
private static final int BASE_LINE = 2;
/**
* 标签列表
*/
private List<SearchRecord> recordList;
/**
* 是否超过基础行数
*/
private boolean isOverBaseLine;
/**
* 最多可展示的数,设置为尽量打,则相当于不限制
*/
private final int mMaxLineCount = 5;
/**
* 是否展开
*/
private boolean expandable;
public FlowRecordLayout(Context context) {
this(context, null);
}
public FlowRecordLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public FlowRecordLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void setRecordList(List<SearchRecord> recordList) {
mRowViews.clear();
mInitMeasure = true;
closeExpandIndex = 0;
openExpandIndex = 0;
expandable = false;
this.recordList = recordList;
mInflater = LayoutInflater.from(getContext());
removeAllViews();
for (int i = 0; i < recordList.size(); i++) {
SearchRecord record = recordList.get(i);
TextView textChild = (TextView) mInflater.inflate(R.layout.text_search_tag_layout, this, false);
textChild.setText(CommonUtils.setLimitLength(16, record.getName()));
addView(textChild);
int finalI = i;
textChild.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (mOnTagClickListener != null) {
mOnTagClickListener.onTagClick(textChild, finalI);
}
}
});
}
imgExpand = (ImageView) mInflater.inflate(R.layout.img_search_record_tag, this, false);
addView(imgExpand);
imgExpand.setImageResource(R.mipmap.search_open);
imgExpand.setOnClickListener(v -> mOnTagClickListener.onExpandClick(expandable));
requestLayout();
}
public void setRecordList(List<SearchRecord> recordList, boolean hasImg) {
mInitMeasure = false;
removeAllViews();
this.mHasImg = hasImg;
mInflater = LayoutInflater.from(getContext());
this.recordList = recordList;
int index = !hasImg ? recordList.size() : !expandable ? closeExpandIndex : (openExpandIndex > 0) ? openExpandIndex : recordList.size();
for (int i = 0; i < index; i++) {
SearchRecord record = recordList.get(i);
TextView textChild = (TextView) mInflater.inflate(R.layout.text_search_tag_layout, this, false);
textChild.setText(CommonUtils.setLimitLength(16, record.getName()));
addView(textChild);
int finalI = i;
textChild.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (mOnTagClickListener != null) {
mOnTagClickListener.onTagClick(textChild, finalI);
}
}
});
}
if (hasImg) {
imgExpand = (ImageView) mInflater.inflate(R.layout.img_search_record_tag, this, false);
addView(imgExpand);
int resId = expandable ? R.mipmap.search_close : R.mipmap.search_open;
imgExpand.setImageResource(resId);
imgExpand.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
mOnTagClickListener.onExpandClick(expandable);
}
});
}
requestLayout();
}
private OnTagClickListener mOnTagClickListener;
public void setOnTagClickListener(OnTagClickListener onTagClickListener) {
mOnTagClickListener = onTagClickListener;
}
public interface OnTagClickListener {
void onTagClick(View view, int position);
void onExpandClick(boolean expandable);
}
/**
* 是否展开
*
* @param expandable 是否展开
*/
public void setExpandable(boolean expandable) {
removeAllViews();
mInflater = LayoutInflater.from(getContext());
mInitMeasure = false;
mHasImg = true;
this.expandable = expandable;
int index = !expandable ? closeExpandIndex : (openExpandIndex > 0) ? openExpandIndex : recordList.size();
for (int i = 0; i < index; i++) {
SearchRecord record = recordList.get(i);
TextView textChild = (TextView) mInflater.inflate(R.layout.text_search_tag_layout, this, false);
textChild.setText(CommonUtils.setLimitLength(16, record.getName()));
addView(textChild);
int finalI = i;
textChild.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (mOnTagClickListener != null) {
mOnTagClickListener.onTagClick(textChild, finalI);
}
}
});
}
imgExpand = (ImageView) mInflater.inflate(R.layout.img_search_record_tag, this, false);
addView(imgExpand);
int resId = expandable ? R.mipmap.search_close : R.mipmap.search_open;
imgExpand.setImageResource(resId);
imgExpand.setOnClickListener(v -> mOnTagClickListener.onExpandClick(expandable));
}
public boolean getExpandable() {
return expandable;
}
public boolean isOverBaseLine() {
return isOverBaseLine;
}
/**
* 重新方法用来获取子view的margin值
*/
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int top = 0;
int left = 0;
int cl, ct, cr, cb;
for (int i = 0; i < mRowViews.size(); i++) {
List<View> viewList = mRowViews.get(i);
for (int j = 0; j < viewList.size(); j++) {
View child = viewList.get(j);
MarginLayoutParams params = (MarginLayoutParams) child.getLayoutParams();
cl = left + params.leftMargin;
ct = top + params.topMargin;
cr = cl + child.getMeasuredWidth();
cb = ct + child.getMeasuredHeight();
child.layout(cl, ct, cr, cb);
left = cr + params.rightMargin;
}
top += lineHeights.get(i);
left = 0;
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
mRowViews.clear();
lineHeights.clear();
currentViewList.clear();
int width = MeasureSpec.getSize(widthMeasureSpec);
int modeW = MeasureSpec.getMode(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
int modeH = MeasureSpec.getMode(heightMeasureSpec);
int curLineWidth = 0;
int curLineHeight = 0;
int curLineCount = 1;
int totalHeight = 0;
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
measureChild(child, widthMeasureSpec, heightMeasureSpec);
int childWidth = child.getMeasuredWidth();
int childHeight = child.getMeasuredHeight();
MarginLayoutParams params = (MarginLayoutParams) child.getLayoutParams();
int leftMargin = params.leftMargin;
int topMargin = params.topMargin;
int rightMargin = params.rightMargin;
int bottomMargin = params.bottomMargin;
int childViewWidth = childWidth + leftMargin + rightMargin;
int childViewHeight = childHeight + topMargin + bottomMargin;
// TODO: 2021-08-26 判断子控件的宽是否大于父控件的宽,当大于父控件的宽度时,子控件、
// 显示不完整 ,所以需要重新设置宽度,同时子控件的内容应该添加 android:ellipsize="end" 属性
if (childViewWidth > width) {
LayoutParams param = child.getLayoutParams();
params.width = width - params.leftMargin - params.rightMargin;
child.setLayoutParams(param);
childViewWidth = params.width + params.leftMargin + params.rightMargin;
}
// TODO: 2021-08-27 测量基础行队尾添加 ImageView 的情况和限制行队尾添加 ImageView 的情况。
if (curLineWidth + childViewWidth > width) {
// TODO: 2021-08-27 1.标注特殊行 基础行、限制行
if (curLineCount == BASE_LINE && mInitMeasure) { //基础行并且处于第一次测量阶段
ImageView imgChild = (ImageView) getChildAt(childCount - 1);
measureChild(imgChild, widthMeasureSpec, heightMeasureSpec);
MarginLayoutParams imgParams = (MarginLayoutParams) child.getLayoutParams();
int imgSpaceWidth = imgChild.getMeasuredWidth() + imgParams.leftMargin + imgParams.rightMargin;
closeExpandIndex = imgSpaceWidth + curLineWidth > width ? i - 1 : i;
} else if (curLineCount == mMaxLineCount && mInitMeasure) { //限制行并且处于第一次测量阶段
ImageView imgChild = (ImageView) getChildAt(childCount - 1);
measureChild(imgChild, widthMeasureSpec, heightMeasureSpec);
MarginLayoutParams imgParams = (MarginLayoutParams) child.getLayoutParams();
int imgSpaceWidth = imgChild.getMeasuredWidth() + imgParams.leftMargin + imgParams.rightMargin;
openExpandIndex = imgSpaceWidth + curLineWidth > width ? i - 1 : i;
}
// TODO: 2021-08-27 换行
totalHeight += curLineHeight;
mRowViews.add(currentViewList);
lineHeights.add(curLineHeight);
currentViewList = new ArrayList<>();
currentViewList.add(child);
curLineHeight = childViewHeight;
curLineWidth = childViewWidth;
curLineCount++;
} else {
curLineHeight = Math.max(curLineHeight, childViewHeight);
curLineWidth += childViewWidth;
currentViewList.add(child);
}
if (i == childCount - 1) {
totalHeight += curLineHeight;
lineHeights.add(curLineHeight);
mRowViews.add(currentViewList);
}
}
mHasImg = mInitMeasure && mRowViews.size() > BASE_LINE && !(mRowViews.get(BASE_LINE).get(0) instanceof ImageView);
isOverBaseLine = mHasImg;
// 宽度可以不用考虑 主要考虑高度
if (modeH == MeasureSpec.EXACTLY) {
setMeasuredDimension(width, height);
} else {
setMeasuredDimension(width, totalHeight);
}
}
}
@BindView(R.id.flow_layout_search_record)
FlowRecordLayout mZFlowLayout;
private void initZFlowLayout() {
mZFlowLayout.setRecordList(keyList);
mZFlowLayout.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
mZFlowLayout.getViewTreeObserver().removeOnGlobalLayoutListener(this);
mZFlowLayout.setRecordList(keyList, mZFlowLayout.isOverBaseLine());
}
});
mZFlowLayout.setOnTagClickListener(new FlowRecordLayout.OnTagClickListener() {
@Override
public void onTagClick(View view, int position) {
EventBus.getDefault().postSticky(new SearchKeyChangeEvent(keyList.get(position).getName()));
}
@Override
public void onExpandClick(boolean expandable) {
mZFlowLayout.setExpandable(!expandable);
}
});
}