前言
电商应用开发时用户评论功能是一个很重要的模块,通过用户的反馈知道服务存在的不足,进而为改进工作流程提供依据。用户的评论会被企业使用大数据处理抽取出一些基本的评论标签,这些标签就能够展示整体评论上方供用户了解其他客户对商家或货物的整体看法。现在就来简单的使用流式布局实现展示用户评价标签。
效果
实现过程
流式布局的特点是能够根据用户提供的数据按照从左到右,从上到下的方式展示控件,如果在一行展示不下就会空下余下的位置并切到下一行展示。很显然Android自带的五大布局无法完成这样复杂的实现逻辑,需要自定义实现这种流式布局方法。
首先需要定义每个标签之前的横向和竖向的间距,有了间距之后就不会感到所有的标签都聚集在一起的拥挤感了。在attrs.xml文件中定义如下的自定义属性:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="FlowLayout">
<attr name="vertical_margin" format="dimension" />
<attr name="horizontal_margin" format="dimension" />
</declare-styleable>
</resources>
在FlowLayout自定义布局的构造方法里解析这两个数值:
public FlowLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
if (attrs != null) {
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.FlowLayout);
mVerticalMargin = array.getDimensionPixelOffset(R.styleable.FlowLayout_vertical_margin, CommonUtils.dp2px(5));
mHorizontalMargin = array.getDimensionPixelOffset(R.styleable.FlowLayout_horizontal_margin, CommonUtils.dp2px(5));
array.recycle();
}
init();
}
接下来当用户设置标签数据的时候为这些标签数据添加对应的展示控件并初始化。
public void setData(List<String> data) {
// 清空之前的数据
mData.clear();
mData.addAll(data);
// 清空之前的控件
removeAllViews();
// 清空之前的行控件记录和高度记录
mLineViews.clear();
mHeights.clear();
int count = data.size();
for (int i = 0; i < count; i++) {
String str = data.get(i);
View view = mInflater.inflate(R.layout.item_comment, this, false);
TextView textView = (TextView) view.findViewById(R.id.text);
textView.setText(str);
addView(view);
}
}
在流式布局里定义一下几个简单的变量来记录控件的拜访位置:
// 展示的标签数据
private List<String> mData = new ArrayList<>();
// 每行展示的标签View,mLineViews.size()代表展示多少行,
// 里面的每个List<View>表示当前行展示的标签
private List<List<View>> mLineViews = new ArrayList<>();
// 每一行展示的高度
private List<Integer> mHeights = new ArrayList<>();
private LayoutInflater mInflater;
// 标签之间的横向和竖向边距
private int mVerticalMargin = CommonUtils.dp2px(5);
private int mHorizontalMargin = CommonUtils.dp2px(5);
前面添加了所有标签控件接下来开始在onMeasure测量期间判断每一行展示的标签控件和每行的展示高度,通过这些计算最终能够获取整个流式布局的高度和宽度。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 获取父控件传递进来的测量数据
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int width = 0, height = 0, lineWidth = 0, lineHeight = 0;
// 如果父控件要求必须是指定的widthSize,就按照widthSize来设置流式布局的宽度
if (widthMode == MeasureSpec.EXACTLY) {
width = widthSize;
} else {
// 否则就认为是wrap_content,使用屏幕宽度和指定最大值里最小的
width = Math.min(widthSize, CommonUtils.getScreenWidth());
}
// 清空每行展示标签和每行高度
mLineViews.clear();
mHeights.clear();
int count = getChildCount();
int maxLineWidth = width - getPaddingLeft() - getPaddingRight();
List<View> lineViews = new ArrayList<>();
mLineViews.add(lineViews);
// 遍历所有标签控件
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
// 测量子控件的高度
measureChild(child, widthMeasureSpec, heightMeasureSpec);
// 如果当前行的宽度加上标签子控件的宽度大于一行的最大宽度,换行
if (lineWidth + child.getMeasuredWidth() + mHorizontalMargin > maxLineWidth) {
// 保存当前行的高度
mHeights.add(lineHeight);
// 为下一行新增保存下一行标签的容器
lineViews = new ArrayList<>();
mLineViews.add(lineViews);
lineViews.add(child);
// 下一行目前测量到的标签所占据的宽度
lineWidth = child.getMeasuredWidth() + mHorizontalMargin;
// 下一行测量到的高度
lineHeight = child.getMeasuredHeight() + mVerticalMargin;
} else {
// 如果当前行测量到的标签还没有到换行的情况,将当前标签控件方法当前行
lineViews.add(child);
// 如果当前的标签高度超过目前测量的行高,行高改成较大的值
if (lineHeight < child.getMeasuredHeight() + mVerticalMargin) {
lineHeight = child.getMeasuredHeight() + mVerticalMargin;
}
// 增加目前测量到的标签占据宽度值
lineWidth += child.getMeasuredWidth() + mHorizontalMargin;
// 如果是最后一个控件,由于不会在做任何测量,需要把最后一行的高度保存下来
if (i == count - 1) {
mHeights.add(lineHeight);
}
}
}
// 根据前面测量每行的高度,相加起来获取展示标签需要的高度
int maxHeight = getPaddingBottom() + getPaddingTop();
for (int i = 0; i < mHeights.size(); i++) {
maxHeight += mHeights.get(i);
}
if (heightMode == MeasureSpec.EXACTLY) {
height = heightSize;
} else {
height = Math.min(maxHeight, heightSize);
}
// 设置流式布局的高度
setMeasuredDimension(width, height);
}
测量完成之后需要使用每行的标签控件mLineViews布局,一行一行从前向后,从上到下开始做布局。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (changed) {
// x开始位置从paddingLeft开始,y从paddingTop开始
int x = getPaddingLeft(), y = getPaddingTop(), count = mLineViews.size(), lineHeight;
// 遍历所有行
for (int i = 0; i < count; i++) {
List<View> viewList = mLineViews.get(i);
lineHeight = mHeights.get(i);
// 遍历每行里的每一个标签
for (int j = 0; j < viewList.size(); j++) {
// 放置标签控件
View child = viewList.get(j);
child.layout(x, y, x + child.getMeasuredWidth(), y + child.getMeasuredHeight());
x += child.getMeasuredWidth() + mHorizontalMargin;
}
// 下一行x从paddingLeft开始,y需要加上当前行的行高
x = getPaddingLeft();
y += lineHeight;
}
}
}
通过这些简单的测量和布局操作就能够实现简单的流式布局,至于标签的选中点击事件等其实就是简单的添加背景和设置Click事件,这里就不再赘述。