自定义ViewGroup——FlowLayout

地址 FlowLayout
第一篇博客,从简单的自定义View写起吧
1.需求
与淘宝的历史搜索类似,当前行空间足够时添加到当前行,否则自动换行
支持padding和子View的margin

2.效果图

FlowLayout.jpg

3.实现
1.onMeasure
<pre>
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (getChildCount() == 0) { //子view数量为0,不需要测量了
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
return;
}

    measureChildren(widthMeasureSpec, heightMeasureSpec);

    final int childCount = getChildCount();

    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int width = MeasureSpec.getSize(widthMeasureSpec);
    int height = MeasureSpec.getSize(heightMeasureSpec);

    final int maxWidth = width - getPaddingEnd();

    int currentWidth = getPaddingStart();

    int currentTop = getPaddingTop();

    View child = getChildAt(0);
    MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
    currentTop += lp.topMargin;


    for (int i = 0; i < childCount; i++) {
        child = getChildAt(i);
        lp = (MarginLayoutParams) child.getLayoutParams();
        int calcRight = (currentWidth + getViewWidth(child) + lp.leftMargin);
        if (calcRight <= maxWidth) {
            currentWidth = currentWidth + child.getMeasuredWidth() + lp.rightMargin;
            Log.i(TAG, "onMeasure不换行: " + currentWidth);
            //currentWidth = currentWidth + child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
            //doNothing
        } else {//换行
            currentTop = currentTop + lp.topMargin + lp.bottomMargin + getViewHeight(child);
            currentWidth = getPaddingStart() + lp.leftMargin;
            //calcRight = (currentWidth + getViewWidth(child));//currentWidth变了 需要重新算
            //child.layout(currentWidth, currentTop, calcRight, currentTop + getViewHeight(child));
            Log.i(TAG, "onMeasure换行: " + currentWidth);
            currentWidth += child.getMeasuredWidth() + lp.rightMargin;
        }
    }//end for

    currentTop = currentTop + child.getMeasuredHeight() + lp.bottomMargin;

    setMeasuredDimension(width,heightMode == MeasureSpec.EXACTLY ? height : currentTop);

}

</pre>

首先,通过measureChildren方法通知所有子view测量自己,随后获取到系统提供给我们的宽高和测量模式。

测量过程十分简单,能摆放子view的宽度maxWidth是Flowlayout的宽度减去左右padding,所以只要用一个值记录下当前行的宽度,小于等于maxWidth就继续加,大于就归零并且换行。这里每一个子view高度margin都是相同的,所以随便拿一个当成当前行高就好了。

2.onLayout
<pre>

getViewHeight(child)就是child.getMeasureHeight</br>

protected void onLayout(boolean changed, int l, int t, int r, int b) {
//super.onLayout();
int currentWidth = getPaddingStart();
int currentTop = getPaddingTop();
int childCount = getChildCount();
int maxWidth = getWidthWithPadding();

    for (int i = 0; i < childCount; i++) {
        final View child = getChildAt(i);
        child.setTag(i);
        MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
        currentWidth += lp.leftMargin;
        //rowHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
        int calcRight = (currentWidth + getViewWidth(child));
        if (calcRight <= maxWidth) {
            child.layout(currentWidth, currentTop, calcRight, currentTop + getViewHeight(child));
            currentWidth += child.getMeasuredWidth() + lp.rightMargin;
            //doNothing
        } else {//需要换列
            currentTop = currentTop + lp.topMargin + lp.bottomMargin + getViewHeight(child);
            currentWidth = getPaddingStart() + lp.leftMargin;
            calcRight = (currentWidth + getViewWidth(child));//currentWidth变了 需要重新算
            if(calcRight > maxWidth)
                calcRight = maxWidth;
            child.layout(currentWidth, currentTop, calcRight, currentTop + getViewHeight(child));
            currentWidth += child.getMeasuredWidth() + lp.rightMargin;
        }
    }
}

</pre>

onLayout中的代码和onMeasure十分相似,重点就在
<code>child.layout(currentWidth,currentTop,calcRight,currentTop+getViewHeight(child));
</code>
这个方法接受4个参数,分别是子view在父view中的左 上 右 下。注意的是Android中的坐标系x轴正向是右,但是y轴的正向是下。</br>
到这里 FlowLayout基本上就实现了。</br>

推荐阅读更多精彩内容