流式布局一般会用在搜索页面展示热门搜索,看着错乱但是视觉效果还是不错的。Android 原生控件关于这个没有很好的实现,所以如果开发中有需求的话需要自己动手实现。
实现的效果如下:
效果图
自定义LayoutParams
ViewGroup的LayoutParams用于存储了子View在加入ViewGroup中时的一些参数信息。如果需要可以新建一个自定义的LayoutParams类,就像SDK中我们熟悉的LinearLayout.LayoutParams,RelativeLayout.LayoutParams类等一样。
为了方便记录子控件在父容器的位置,我们在这里定义了一个XYLayoutParams,其中的X和Y就是子控件相对父控件的位置信息。
public static class XYLayoutParams extends ViewGroup.MarginLayoutParams {
private int x;
private int y;
public XYLayoutParams(Context context, AttributeSet attributeSet) {
super(context, attributeSet);
}
public XYLayoutParams(int width, int height) {
super(width, height);
}
public XYLayoutParams(ViewGroup.LayoutParams layoutParams) {
super(layoutParams);
}
public void setPosition(int x, int y) {
this.x = x;
this.y = y;
}
}
向父容器中addView的时候会创建LayoutParams对象,想使用自定义的LayoutParams需要从写下面的四个方法:
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof XYLayoutParams;
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new XYLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attributeSet) {
return new XYLayoutParams(getContext(), attributeSet);
}
@Override
protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
return new XYLayoutParams(p);
}
原因是可以看ViewGroup中的addView源码:
generateDefaultLayoutParams(...)用于创建默认LayoutParams
public void addView(View child, int index) {
...
LayoutParams params = child.getLayoutParams();
if (params == null) {
params = generateDefaultLayoutParams();
...
}
addView(child, index, params);
}
addView(...)会调用到addViewInner(...),其中会检测LayoutParams的类型,并调用generateLayoutParams(ViewGroup.LayoutParams p)从新创建
LayoutParams 对象
private void addViewInner(View child, int index, LayoutParams params,
boolean preventRequestLayout) {
...
if (!checkLayoutParams(params)) {
params = generateLayoutParams(params);
}
...
测量每个子View的大小
关于View换行
默认使用从左往右的横向排版,遍历测量子View的大小,连续记录子view的宽度,当新的View宽度加上之前记录的宽度大于父布局宽度的时候,那么换行,记录宽度值,行宽总和清零。
关于父View的宽高
1.当上述所需换行的时候记录换行前的宽度,对比每次换行的宽度,记录宽度最大值
2.换行后累加上上一行所需高度的最大值,记录为所需最小高度。
3.根据测绘模式决定FlowLayout最后的宽高
onMeasure()方法如下,主要流程是:首先获取父控件的大小和父控件的测绘模式,
遍历所有的View 根据上述所描述的换行规则依次排序,记录每个View的LayoutParams,记录所需最小的宽高。最后根据父控件的测绘模式决定父控件的最终大小。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//获取控件的宽高和模式
int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
int modeHeight = MeasureSpec.getMode(heightMeasureSpec);
//模式是wrap_content的时候需要最小宽高
int width = 0;
int height = 0;
//当前行的宽高
int lineWidth = 0;
int lineHeight = 0;
int count = getChildCount();
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
if (child.getVisibility() == GONE) {
continue;
}
//测绘child
XYLayoutParams lp = (XYLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
getPaddingLeft() + this.getPaddingRight()+horizontalSpace*2, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
getPaddingTop() + getPaddingBottom()+verticalSpace*2, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
//相对父控件的位置
int posX = 0;
int posY = 0;
//获取当前child所需的宽高
int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin + horizontalSpace;
int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin + verticalSpace;
//当前行的宽度加上child的宽度超过当前行允许的宽度,就换行
if (lineWidth + childWidth > sizeWidth - getPaddingLeft() - getPaddingRight()-horizontalSpace*2) {
//需要换行
width = Math.max(width, lineWidth);
lineWidth = childWidth;
height += lineHeight;
lineHeight = childHeight;
posX = getPaddingLeft() + horizontalSpace;
posY = getPaddingTop() + height + verticalSpace;
} else {
//不需要换行
posX = getPaddingLeft() + lineWidth + horizontalSpace;
posY = getPaddingTop() + height + verticalSpace;
lineWidth += childWidth;
lineHeight = Math.max(lineHeight, childHeight);
}
lp.setPosition(posX, posY);
}
//实际控件所需的宽高加上最后一行
width = Math.max(lineWidth, width);
height += lineHeight;
setMeasuredDimension(
//如果是精确测绘模式就用原来的数据,不然就用计算所需的大小。
modeWidth == MeasureSpec.EXACTLY ? sizeWidth : width + getPaddingLeft() + getPaddingRight(),
modeHeight == MeasureSpec.EXACTLY ? sizeHeight : height + getPaddingTop() + getPaddingBottom()//
);
}
确定每个子View相对父View的位置
由于在onMeasure的时候使用自定义的XYLayoutParams记录了每个子View相对父容器的位置,所以在OnLayout中只需要取出其对应位置,然后调用子View的layout方法。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
final int count = getChildCount();
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
if (child.getVisibility() == View.GONE)
{
continue;
}
XYLayoutParams lp = (XYLayoutParams) child.getLayoutParams();
child.layout(lp.x, lp.y, lp.x + child.getMeasuredWidth(), lp.y + child.getMeasuredHeight());
}
}
项目地址:链接:http://pan.baidu.com/s/1sl1Dkpn 密码:no8x
网友评论