在这一章节,我们继续学习Android自定义控件。这里要自定义的是Android里面的一个常用控件-Android流式Tag布局,这里我们命名为:FlowTagLayout,我们要实现的流式布局,有如下特色:
- 填充数据和ListView、GridView用法一样使用Adapter,更新数据直接通过adapter.notifyDataChanged来更新
- 支持点击、单选、多选三种模式:FLOW_TAG_CHECKED_NONE、FLOW_TAG_CHECKED_SINGLE、FLOW_TAG_CHECKED_MULTI
正式讲解之前先看下我们实现后的效果图:
目前网上有很多的教程来写流式布局实现,我看到的版本大体上有两种,一种是继承ViewGroup,然后重写其onMeasure和onLayout方法,另一种则是继承自RelativeLayout,例如这个TagView
而我们这里采用的是第一种方法,因为我感觉第一种方法简单、清晰、明了!!
因为我们直接继承的ViewGroup,所以要指定它的LayoutParams,这里因为只需要margin,所以我们直接返回MarginLayoutParams就可以了,代码如下:
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
onMeasure测量
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//获取Padding
// 获得它的父容器为它设置的测量模式和大小
int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
int modeHeight = MeasureSpec.getMode(heightMeasureSpec);
//FlowLayout最终的宽度和高度值
int resultWidth = 0;
int resultHeight = 0;
//测量时每一行的宽度
int lineWidth = 0;
//测量时每一行的高度,加起来就是FlowLayout的高度
int lineHeight = 0;
//遍历每个子元素
for (int i = 0, childCount = getChildCount(); i < childCount; i++) {
View childView = getChildAt(i);
//测量每一个子view的宽和高
measureChild(childView, widthMeasureSpec, heightMeasureSpec);
//获取到测量的宽和高
int childWidth = childView.getMeasuredWidth();
int childHeight = childView.getMeasuredHeight();
//因为子View可能设置margin,这里要加上margin的距离
MarginLayoutParams mlp = (MarginLayoutParams) childView.getLayoutParams();
int realChildWidth = childWidth + mlp.leftMargin + mlp.rightMargin;
int realChildHeight = childHeight + mlp.topMargin + mlp.bottomMargin;
//如果当前一行的宽度加上要加入的子view的宽度大于父容器给的宽度,就换行
if ((lineWidth + realChildWidth) > sizeWidth) {
//换行
resultWidth = Math.max(lineWidth, realChildWidth);
resultHeight += realChildHeight;
//换行了,lineWidth和lineHeight重新算
lineWidth = realChildWidth;
lineHeight = realChildHeight;
} else {
//不换行,直接相加
lineWidth += realChildWidth;
//每一行的高度取二者最大值
lineHeight = Math.max(lineHeight, realChildHeight);
}
//遍历到最后一个的时候,肯定走的是不换行
if (i == childCount - 1) {
resultWidth = Math.max(lineWidth, resultWidth);
resultHeight += lineHeight;
}
setMeasuredDimension(modeWidth == MeasureSpec.EXACTLY ? sizeWidth : resultWidth,
modeHeight == MeasureSpec.EXACTLY ? sizeHeight : resultHeight);
}
}
代码注释的很详细,首先得到其父容器传入的测量模式和宽高的计算值,然后遍历所有的childView,使用measureChild方法对所有的childView进行测量。然后根据所有childView的测量得出的宽和高得到该ViewGroup如果设置为wrap_content时的宽和高
onLayout
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int flowWidth = getWidth();
int childLeft = 0;
int childTop = 0;
//遍历子控件,记录每个子view的位置
for (int i = 0, childCount = getChildCount(); i < childCount; i++) {
View childView = getChildAt(i);
//跳过View.GONE的子View
if (childView.getVisibility() == View.GONE) {
continue;
}
//获取到测量的宽和高
int childWidth = childView.getMeasuredWidth();
int childHeight = childView.getMeasuredHeight();
//因为子View可能设置margin,这里要加上margin的距离
MarginLayoutParams mlp = (MarginLayoutParams) childView.getLayoutParams();