Android自定义ViewGroup之流式布局

前言

以前总是喜欢用别人写好的东西。但是真的说自己写自定义View还未必能写的出来。这篇文章完全是即时兴起,想看看在没有看其他人的博客的情况下自己是否真的能自定义View,所以就有了这篇文章。

github地址:https://github.com/Amoryan/FlowLayout

Android自定义ViewGroup之流式布局_第1张图片
image.png

为什么要重写onMeasure

我们首先看看View里面这个的默认实现

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

它调用了getDefaultSize方法,而这个方法里面是这样处理的

public static int getDefaultSize(int size, int measureSpec) {
    int result = size;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);

    switch (specMode) {
    case MeasureSpec.AT_MOST:
    case MeasureSpec.EXACTLY:
        result = specSize;
        break;
    }
    return result;
}

这里我删除掉了UNSPECIFIED的情况,这个我们暂时用不到。可以看到这个方法中不管是AT_MOST还是EXACTLY都会将result设置为specSize,而specSize是父容器能给予的最大大小。这样就导致一个问题,如果我们用默认的实现,自定义的View就不支持wrap_content。

所以实际上我们重写onMeasure方法一个很重要的目的是为了让它支持wrap_content。

而ViewGroup除了这个目的之外,它还需要测量子控件大小。具体我们可以看看ViewGroup的measureChildren方法

protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
    final int size = mChildrenCount;
    final View[] children = mChildren;
    for (int i = 0; i < size; ++i) {
        final View child = children[i];
        if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
        }
    }
}

而measureChild里面是这样处理的

protected void measureChild(View child, int parentWidthMeasureSpec,
        int parentHeightMeasureSpec) {
    final LayoutParams lp = child.getLayoutParams();

    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
            mPaddingLeft + mPaddingRight, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
            mPaddingTop + mPaddingBottom, lp.height);

    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

这里就会调用子控件的measure方法。

onMeasure

好了,开始自定义我们的流式布局,首先重写onMeasure方法,并逐个测量子控件,于是我们添加一个measureChildBeforeLayout用来测量子控件的大小

private fun measureChildBeforeLayout(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    for (i in 0..(childCount - 1)) {
        var child = getChildAt(i)

        if (View.GONE == child.visibility) continue

        measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0)
    }
}

测量完子控件后我们开始确定自身的大小,如下所示

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {

    measureChildBeforeLayout(widthMeasureSpec, heightMeasureSpec)

    val wSpecMode = MeasureSpec.getMode(widthMeasureSpec)
    val wSpecSize = MeasureSpec.getSize(widthMeasureSpec)
    var wResult = wSpecSize

    when (wSpecMode) {
        MeasureSpec.AT_MOST -> {
            wResult = Math.min(wResult, getChildTotalWidth())
        }
    }

    val hSpecMode = MeasureSpec.getMode(heightMeasureSpec)
    val hSpecSize = MeasureSpec.getSize(heightMeasureSpec)
    var hResult = hSpecSize

    when (hSpecMode) {
        MeasureSpec.AT_MOST -> {
            hResult = Math.min(hResult, getChildTotalHeight(wResult))
        }
    }

    setMeasuredDimension(wResult, hResult)
}

当宽度为wrap_content的时候,所有子控件的宽度和不能大于父控件给予的最大宽度。当高度为wrap_content的时候所有子控件的高度和不能大于父控件给予的最大高度,计算高度的时候得确认排列的行数,然后确定每一行的高度后求和。

private fun getChildTotalHeight(maxWidth: Int): Int {

    var totalHeight = paddingTop + paddingBottom
    var colIndex = 0
    var rowIndex = 0
    var rowWidth = paddingLeft + paddingRight
    var rowHeight = 0

    for (i in 0..(childCount - 1)) {
        val child = getChildAt(i)
        val lp = child.layoutParams as MarginLayoutParams

        if (View.GONE == child.visibility) continue

        val childWidth = child.measuredWidth + lp.leftMargin + lp.rightMargin
        val childHeight = child.measuredHeight + lp.topMargin + lp.bottomMargin

        if (colIndex == 0) {
            if (rowWidth + childWidth > maxWidth) {
                totalHeight += childHeight
                if (rowIndex != 0) {
                    totalHeight += mVerticalSpace.toInt()
                }
                rowWidth = paddingLeft + paddingRight
                rowHeight = 0
                rowIndex++
            } else {
                colIndex++
                rowWidth += childWidth
                if (rowIndex == 0) {
                    rowHeight = Math.max(rowHeight, childHeight)
                } else {
                    rowHeight = Math.max(rowHeight, childHeight + mVerticalSpace.toInt())
                }
            }
        } else {
            if (rowWidth + childWidth + mHorizontalSpace.toInt() > maxWidth) {
                totalHeight += rowHeight
                rowWidth = paddingLeft + paddingRight + childWidth
                rowHeight = childHeight + mVerticalSpace.toInt()
                colIndex = 1
                rowIndex++
            } else {
                colIndex++
                rowWidth += childWidth + mHorizontalSpace.toInt()
                if (rowIndex == 0) {
                    rowHeight = Math.max(rowHeight, childHeight)
                } else {
                    rowHeight = Math.max(rowHeight, childHeight + mVerticalSpace.toInt())
                }
            }
        }
    }
    totalHeight += rowHeight
    return totalHeight
}

onLayout

ViewGroup的onLayout主要是为了确定子控件的排放位置。这里添加一个layoutChild方法来确定每一行排列的子控件,然后再对每一行的子控件进行排列

override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
    mViews.clear()/*多次调用这个方法的情况*/

    layoutChild()

    var rowTop = paddingTop

    for (i in 0..(mViews.size - 1)) {
        val lineViews = mViews[i]
        var rowLeft = paddingLeft

        for (i in 0..(lineViews.size - 1)) {
            val view = lineViews[i]
            val lp = view.layoutParams as MarginLayoutParams

            var left = rowLeft + lp.leftMargin
            val top = rowTop + lp.topMargin
            var space = view.measuredWidth + lp.leftMargin + lp.rightMargin
            if (i != 0) {/*不是第一个控件就加上间距*/
                left += mHorizontalSpace.toInt()
                space += mHorizontalSpace.toInt()
            }
            view.layout(left, top, left + view.measuredWidth, top + view.measuredHeight)
            rowLeft += space
        }
        rowTop += getRowMaxHeight(i)
    }
}

private fun layoutChild() {
    var lineViews = ArrayList()
    var colIndex = 0
    var rowWidth = paddingLeft + paddingRight

    for (i in 0..(childCount - 1)) {
        val child = getChildAt(i)
        val lp = child.layoutParams as MarginLayoutParams

        if (View.GONE == child.visibility) continue

        val childWidth = child.measuredWidth + lp.leftMargin + lp.rightMargin

        if (colIndex == 0) {/*这一行的第一个控件是不用添加间距的*/
            if (rowWidth + childWidth > measuredWidth) {
                lineViews.add(child)
                mViews.add(lineViews)
                lineViews = ArrayList()
                rowWidth = paddingLeft + paddingRight
            } else {
                colIndex++
                rowWidth += childWidth
                lineViews.add(child)
            }
        } else {/*一行的非第一个控件需要添加间距*/
            if (rowWidth + childWidth + mHorizontalSpace > measuredWidth) {
                mViews.add(lineViews)
                lineViews = ArrayList()
                lineViews.add(child)
                rowWidth = paddingLeft + paddingRight + childWidth
                colIndex = 1
            } else {
                colIndex++
                rowWidth += childWidth + mHorizontalSpace.toInt()
                lineViews.add(child)
            }
        }
    }
    mViews.add(lineViews)
}

你可能感兴趣的:(Android自定义ViewGroup之流式布局)