上一篇文章已经对View绘制的流程进行了讲解,并用其解决了ListView的嵌套问题。今天我们再次通过热门标签这种常用的自定义控件来演示View绘制的综合运用。
本篇文章讲了热门标签这一简单自定控件的三种实现。其中,前两种基于onMeasure、onLayout方法,最后一种基于LinearLayout布局。
这样做的目的是在加强对View绘制理解的同时,注意实现过程的多样性,增强对知识的灵活运用能力。自定义控件的实现方式可能存在多种,不能拘泥于某一种实现,要能够扩散思维,从而找到最佳或者最适合自己的实现方式。比如,我们之前讲过的圆角图形,就可以通过BitmapShader和Xfermode两种方式来实现。
言归正传,先上效果图,图中为第一种(上不)和第三种(下部)方式实现效果。
从图中看上下两部分效果一样,这也是我们的目的,但实现过程却有一定差异。
第一种实现方式
这种实现方式需要继承自ViewGroup,核心点为:
- 在onMeasure方法测量出控件本身的宽高
- 在onLayout方法里计算控件的位置并摆放
- 设置字符串数据,添加View到ViewGroup
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
mTotalWidth = MeasureSpec.getSize(widthMeasureSpec);//获得总宽度
mLeftPadding = getPaddingLeft();//左侧padding
mTopPadding = getPaddingTop();//右侧padding
mRightPadding = getPaddingRight();//上部padding
mBottomPadding = getPaddingRight();//上部padding
int x = mLeftPadding;
int lineNum = 0;//行数
int height = 0;
for (int i = 0; i < getChildCount(); i++) {
View view = getChildAt(i);
view.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
int width = view.getMeasuredWidth();
height = view.getMeasuredHeight();
if (0 == i || (x + width > mTotalWidth - mRightPadding)) {
lineNum++;//新一行
x = mLeftPadding;
}
x = x + width + ROW_SPACE;
}
setMeasuredDimension(mTotalWidth,
mTopPadding + (lineNum - 1) * (height + LINE_SPACE) + height + mBottomPadding);
}
onMeasure方法
首先通过MeasureSpec获得自身宽度以及各方向的padding,x记录子View的起始位置,默认为mLeftPadding。
然后,for循环子View,获得每个子View的宽高,如果是第一个子View或者剩余宽度不够存当前子View(x+width为假如子View放到该行时右边的x坐标,如果这个坐标大于mTotalWidth - mRightPadding即控件右边可用控件的x坐标)lineNum+1,然后把x恢复到mLeftPadding。
接着,x坐标变为下一个子View的起始位置,即x+width+ROW_SPACE;
最后,调用setMeasuredDimension设置获得的大小。宽度不管mode是啥,设置为原有的宽度。高度为 mTopPadding + (lineNum - 1) * (height + LINE_SPACE) + height + mBottomPadding,即上下padding,加上行数乘以行高,加上(行数-1)乘以行间距。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int x = mLeftPadding;
int lineNum = 0;//行数
for (int i = 0; i < getChildCount(); i++) {
View view = getChildAt(i);
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
if (0 == i || (x + width > mTotalWidth - mRightPadding)) {
lineNum++;//新一行
x = mLeftPadding;
}
view.layout(x, mTopPadding + (lineNum - 1) * (height + LINE_SPACE),
x + width, mTopPadding + (lineNum - 1) * (height + LINE_SPACE) + height);
x = x + width + ROW_SPACE;
}
}
onLayout方法跟onMeasure方法有点类似:
首先,x记录子View的起始位置,默认为mLeftPadding。
然后,for循环子View,获得每个子View的宽高,如果是第一个子View或者剩余宽度不够存当前子View(x+width为假如子View放到该行时右边的x坐标,如果这个坐标大于mTotalWidth - mRightPadding即控件右边可用控件的x坐标)lineNum+1,然后把x恢复到mLeftPadding。
接着,调用 view.layout设置摆放子View本身。left为x,top为mTopPadding + (lineNum - 1) * (height + LINE_SPACE),right为x + width,bottom为mTopPadding + (lineNum - 1) * (height + LINE_SPACE) + height。
最后,x坐标变为下一个子View的起始位置,即x+width+ROW_SPACE;
/***
* 设置标签数据
* @param tagStrList
*/
public void setViewData(List<String> tagStrList) {
removeAllViews();//清除所有view
if (null == tagStrList || tagStrList.size() < 1) {
return;
}
LayoutInflater m_Inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
for (int i = 0; i < tagStrList.size(); i++) {
TextView aTextView = (TextView) m_Inflater.inflate(
R.layout.item_tag, null);
if (aTextView != null) {
aTextView.setText(tagStrList.get(i));
addView(aTextView);
}
}
}
setViewData方法为设置数据,然后利用m_Inflater来获得一个TextView,给这个TextView设置要显示的内容,添加到布局中。这个TextView其实也可以用代码方式创建,但是一个情况下这个TextView都需要设定一些特定的样式及背景,用XML的方式配置比较直观。
第二种实现方式
这种方式同样需要继承自ViewGroup,可以看做在第一种方式上的改进。因为通过第一种的实现过程可以看出,在onMeasure方法计算控件本身宽高的同时,实际上已经能够确定子View的位置。通过对子View位置的保存,减少了在onLayout中重复计算工作,直接对子View进行摆放即可。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
mTotalWidth = MeasureSpec.getSize(widthMeasureSpec);//获得总宽度
mLeftPadding = getPaddingLeft();//左侧padding
mTopPadding = getPaddingTop();//右侧padding
mRightPadding = getPaddingRight();//上部padding
mBottomPadding = getPaddingRight();//上部padding
int x = mLeftPadding;
int lineNum = 0;//行数
int height = 0;
for (int i = 0; i < getChildCount(); i++) {
View view = getChildAt(i);
view.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
int width = view.getMeasuredWidth();
height = view.getMeasuredHeight();
if (0 == i || (x + width > mTotalWidth - mRightPadding)) {
lineNum++;//新一行
x = mLeftPadding;
}
view.setLeft(x);
view.setTop(mTopPadding + (lineNum - 1) * (height + LINE_SPACE));
view.setRight(x + width);
view.setBottom(mTopPadding + (lineNum - 1) * (height + LINE_SPACE) + height);
x = x + width + ROW_SPACE;
}
setMeasuredDimension(mTotalWidth,
mTopPadding + (lineNum - 1) * (height + LINE_SPACE) + height + mBottomPadding);
}
与第一种方式的onMeasure方法相比,增加了一下几行代码:
view.setLeft(x);
view.setTop(mTopPadding + (lineNum - 1) * (height + LINE_SPACE));
view.setRight(x + width);
view.setBottom(mTopPadding + (lineNum - 1) * (height + LINE_SPACE) + height);
主要是保存当前子View的位置,left为x,top为mTopPadding + (lineNum - 1) * (height + LINE_SPACE),right为x + width,bottom为mTopPadding + (lineNum - 1) * (height + LINE_SPACE) + height。
特别注意的是: x = x + width + ROW_SPACE;代表着下一个子View的起始位置,所以应将当前子View一切计算完成后,再调用该行代码。
第三种实现方式
这种方式需要继承自LinearLayout,跟View的绘制知识关联相对较少,核心点为:
- 设置布局的Orientation为VERTICAL,传入字符串数据
- 每一行用一个LinearLayout包裹并将这个LinearLayout添加到布局中
- 通过子View的宽度判断是否需要换行,同时调整相应的布局参数
public void setViewData(List<String> tagStrList) {
removeAllViews();//清除所有view
if (null == tagStrList || tagStrList.size() < 1) {
return;
}
setOrientation(VERTICAL);
LayoutInflater m_Inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
int surplus;
int i = 0;
while (i < tagStrList.size()) {
LinearLayout wrapLayout = new LinearLayout(getContext());
wrapLayout.setOrientation(HORIZONTAL);
LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
if (i > 0) {
params.topMargin = LINE_SPACE;
}
surplus = mContentWidth;
wrapLayout.setLayoutParams(params);
addView(wrapLayout);
for (int j = i; j < tagStrList.size(); j++) {
TextView aTextView = (TextView) m_Inflater.inflate(R.layout.item_tag, null);
aTextView.setText(tagStrList.get(i));
aTextView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
int width = aTextView.getMeasuredWidth();
if (surplus - width >=0) {
if (surplus!=mContentWidth) {
LinearLayout.LayoutParams textParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
textParams.leftMargin = ROW_SPACE;
aTextView.setLayoutParams(textParams);
}
surplus = surplus - width - ROW_SPACE;
wrapLayout.addView(aTextView);
i++;
} else {
break;
}
}
}
}
首先,清除所有子View,设置Orientation为VERTICAL。
然后,while循环添加LinearLayout,这个LinearLayout的rientation为HORIZONTAL。如果不是第一个需要给布局参数params设置topMargin。
接着,for循环给单个LinearLayout添加子View。这些子View需要判断是否为第一个,如果不是第一个的话需要设置leftMargin。surplus代表该行LinearLayout还可存放子View的空间,如果不能够存放下一个子View,则跳出for循环,进入到While循环中,切换到新的一行。
这种方式的实现,主要是利用了LinearLayout布局的特性来完成的,比较容易理解,但可能存在布局多层嵌套的问题,有一定的性能的影响。
另外需要注意的是,mContentWidth是在onSizeChanged中获得的,因此在调用setViewData需要保证View得到绘制。
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mContentWidth = w - getPaddingLeft() - getPaddingRight();
}
方法调用
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
String testStr = "测试测试测试测试测试";
final List<String> stringList = new ArrayList<>();
for (int i = 0; i < 12; i++) {
stringList.add(testStr.substring(0, 1 + (new java.util.Random().nextInt(testStr.length() - 1))));
}
TagViewByViewGroup tagViewByViewGroup = findViewById(R.id.tagview_by_viewgroup);
tagViewByViewGroup.setViewData(stringList);
final TagViewByLinearLayout tagViewByLinearLayout = findViewById(R.id.tagview_by_linearlayout);
tagViewByLinearLayout.post(new Runnable() {
@Override
public void run() {
tagViewByLinearLayout.setViewData(stringList);
}
});
}
从字符串中随机截取一段,然后添加到list中。找到对应控件,调用setViewData设置数据。TagViewByLinearLayout利用post方法保证onSizeChanged调用后再设置数据,否则会因为mContentWidth为0,设置不上数据。
以上就完成了热门标签的开发,但仅具备基础功能。如果需要点击事件以及背景切换的只需对子View设置即可,相对简单,本文就不再介绍了。
老规矩,项目地址