在项目中经常遇到一些标签云的效果,比如城市的选择,景点类型选择,酒店房型选择 这些常见使用标签云效果就比较好了。
今天一步步的写一个标签云view,借鉴于github上TagCloudView
1.准备工作
attr.xml
<resources>
<declare-styleable name="TagTextViewStyle">
<attr name="t_textSize" format="dimension"/>
<attr name="t_textColor" format="color"/>
<attr name="t_itemBorder" format="dimension"/>
<attr name="t_viewBorder" format="dimension"/>
<attr name="t_tagBackground" format="reference"/>
<attr name="t_singleLine" format="boolean"/>
<attr name="t_imageWidth" format="dimension"/>
<attr name="t_imageHeight" format="dimension"/>
<attr name="t_rightArrow" format="integer"/>
<attr name="t_showArrow" format="boolean"/>
<attr name="t_showMore" format="boolean"/>
<attr name="t_moreTextStr" format="string"/>
<attr name="t_moreTextWidth" format="dimension"/>
</declare-styleable>
</resources>
然后定义一个TagTextView,继承与ViewGroup,接下来就从attr中获取这些属性
TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.TagTextViewStyle, defStyleAttr, defStyleAttr);
mTextSize = typedArray.getDimensionPixelSize(R.styleable.TagTextViewStyle_t_textSize, 12);
//更多属性自行实现...
2.重写onMeasure
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//1,计算子view
//2,计算tag view 实际需要高度
//3,根据高度设置
}
计算子view
MeasureSpec封装了父布局传递给子布局的布局要求,每个MeasureSpec代表了一组宽度和高度的要求。一个MeasureSpec由size和mode组成,它有三种模式:UNSPECIFIED(未指定),父元素部队自元素施加任何束缚,子元素可以得到任意想要的大小;EXACTLY(完全),父元素决定自元素的确切大小,子元素将被限定在给定的边界里而忽略它本身大小;AT_MOST(至多),子元素至多达到指定大小的值
这里不指定具体的mode和size作特殊处理,只是使用到size和mode
//计算 ViewGroup 上级容器为其推荐的宽高
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
mWidthSize = MeasureSpec.getSize(widthMeasureSpec);
mHeightSize = MeasureSpec.getSize(heightMeasureSpec);
// 计算出所有的childView的宽和高
measureChildren(widthMeasureSpec, heightMeasureSpec);
measureChildren一行代码就计算了所以的childview的宽,高,这里不具体分析,可以自行查看源码分析measureChildren原理。
计算TagTextView的高度并确定item的位置
如果所有标签没有超过1行,那么TagTextView的高度就是item的高度;如果超过一行,那么高度h=item高度*行数+所有间距
这里实现2中模式:多行模式和单行模式
先介绍怎么实现多行模式
private int getMultiTotalHeight(int totalWidth, int totalHeight) {
int childWidth;
int childHeight;
//遍历所有的子view
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
childWidth = child.getMeasuredWidth();
childHeight = child.getMeasuredHeight();
//总的宽度 = 所有子view宽+item间距
totalWidth += childWidth + mItemBorder;
//设置第一行高度 因为layout t = totalHeight - childHeight
//要让totalHeight - childHeight不为负数 所以先设置一行高度
if (i == 0) {
totalHeight = childHeight + mItemBorder;
}
//2种情况所有item宽度大于viewgroup宽度,这时候需要换行;不大于viewgroup宽度,继续
//所有的totalWidth=item宽度+之间的边距,加上左边第一个item与viewGroup边距mViewBorder,加上右边mItemBorder边距,大于viewGroup的宽,需要换行
if (totalWidth + mItemBorder + mViewBorder > mWidthSize) {
totalWidth = mItemBorder;//换行设置间距
//高度 = 原高度 + item高度 + item间距
totalHeight += childHeight + mItemBorder;
//layout确定子view在view group中的位置
child.layout(
totalWidth + mViewBorder,
totalHeight - childHeight,
totalWidth + childWidth + mViewBorder,
totalHeight);
totalWidth += childWidth;
} else {
//横排:起始 间隔viewboder距离开始,到总的width(items+item间距)+view间距
//竖排:起始 离顶部viewboder间距开始,到总的height
child.layout(totalWidth - childWidth + mViewBorder,
totalHeight - childHeight,
totalWidth + mViewBorder,
totalHeight);
}
}
return totalHeight + mItemBorder;
}
设置高度
//子view计算的宽,高度去决定测量的宽高值
setMeasuredDimension(mWidthSize, heightMode == MeasureSpec.EXACTLY ? mHeightSize : totalHeight);
做完上面的步骤,view的测量,布局都已经完成,接下来只需要把item view放进来。
添加item到ViewGroup
public void setTags(List<String> tagDatas) {
if (tagDatas == null) {
return;
}
mTags = tagDatas;
//先清空所有的view
removeAllViews();
String tag;
for (int i = 0; i < mTags.size(); i++) {
tag = tagDatas.get(i);
//加载布局文件
TextView tagView = (TextView) mInflater.inflate(R.layout.layout_item, null);
tagView.setText(tag);
tagView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize);
tagView.setBackgroundResource(mTagBackground);
tagView.setTextColor(mTextColor);
LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
tagView.setLayoutParams(params);
//添加view
addView(tagView);
}
//刷新
postInvalidate();
}
多行显示tag就在这里写完了,运行,看下标签云的效果
写完多行模式,接下来写单行模式,我们会问,多行模式下不超过viewgroup宽度不就是单行吗,为什么还要定义单行模式?
这里的单行模式是指不管有多少item都值显示单行,超出的item不显示,用”…”代替。
单行模式与多行模式基本类似,主要区别在于获取totalHeight和addView部分。接下来对这两个部分进行实现。
单行模式实现
初始化单行模式view
private void initSingleLineView(int widthMeasureSpec, int heightMeasureSpec) {
//判断是否是单行模式
if (!mSingleLine) {
return;
}
//是否显示向右箭头
if (mShowArrow) {
mArrowIv = new ImageView(getContext());
mArrowIv.setImageResource(mArrowResId);
mArrowIv.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
mArrowIv.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
measureChild(mArrowIv, widthMeasureSpec, heightMeasureSpec);
mArrowIconWidth = mArrowIv.getMeasuredWidth();
mImageHeight = mArrowIv.getMeasuredHeight();
addView(mArrowIv);
}
//是否显示更多item
if (mShowMore) {
mMoreTextTv = (TextView) mInflater.inflate(R.layout.layout_item, null);
mMoreTextTv.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize);
mMoreTextTv.setTextColor(mTextColor);
mMoreTextTv.setBackgroundResource(mTagBackground);
@SuppressLint("DrawAllocation")
LayoutParams layoutParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
mMoreTextTv.setLayoutParams(layoutParams);
mMoreTextTv.setText(mMoreTextStr == null || mMoreTextStr.equals("") ? "..." : mMoreTextStr);
measureChild(mMoreTextTv, widthMeasureSpec, heightMeasureSpec);
mMoreTextWidth = mMoreTextTv.getMeasuredWidth();
addView(mMoreTextTv);
}
}
计算单行模式高度及确定位置2
单行模式看起来代码比较多,其实并不复杂,只是比多行模式多了一个arrow icon和查看更多item
private int getSingleTotalHeight(int totalWidth, int totalHeight) {
int childWidth;
int childHeight = 0;
totalWidth += mViewBorder;//设置左边距
//上图红色边框的总宽度
int textTotalWidth = getTextTotalWidth();
//items 总宽度小于 viewgroup - 箭头图片width 不用显示更多item
if (textTotalWidth < mWidthSize - mArrowIconWidth) {
mMoreTextStr = null;
mMoreTextWidth = 0;
}
//计算item中宽度并确定布局
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
childWidth = child.getMeasuredWidth();
childHeight = child.getMeasuredHeight();
if (i == 0) {
totalWidth += childWidth;
totalHeight = childHeight + mViewBorder;
} else {
totalWidth += childWidth + mItemBorder;
}
//左边距+items宽+item边距+更多item宽+arrow icon宽+右边距 < viewgroup宽
if (mViewBorder + totalWidth + mItemBorder + mMoreTextWidth + mArrowIconWidth+ mViewBorder < mWidthSize) {
//横排:起始 间隔mItemBorder距离开始,到总的width(items+item间距)+mItemBorder间距
//竖排:起始 离顶部viewboder间距开始,到总的height
child.layout(
totalWidth - childWidth + mItemBorder,
totalHeight - childHeight,
totalWidth + mItemBorder,
totalHeight);
} else {
//超出部分不显示 ,前面已经加上了,所以这里需要减去
totalWidth -= childWidth + mViewBorder;
break;
}
}
//更多item
if (mMoreTextTv != null) {
//起始位置 从最后一个item+item边距位置x开始
//结束位置 x+moretextWidth
mMoreTextTv.layout(
mViewBorder+ totalWidth + mItemBorder,
totalHeight - childHeight,
totalWidth + mViewBorder + mItemBorder + mMoreTextWidth,
totalHeight);
}
totalHeight += mViewBorder;
//箭头 与上面类似
if (mArrowIv != null) {
mArrowIv.layout(
mWidthSize - mArrowIconWidth - mViewBorder,
(totalHeight - mImageHeight) / 2,
mWidthSize - mViewBorder,
(totalHeight - mImageHeight) / 2 + mImageHeight);
}
return totalHeight;
}
这样单行模式也就完成了,剩下的就是加入监听事件,优化等,功能扩展,这里不多介绍
完整demo下载地址:TagTextView