美文网首页
自定义View之流布局

自定义View之流布局

作者: 0246eafe46bd | 来源:发表于2021-07-28 12:46 被阅读0次

效果图

image-20210726223212865.png

流布局是自定义ViewGroup的一种,简单来说,就是将其子View按照从左到右、从上到下依次进行排列,如果超过流布局的宽度,就换一行,继续进行排列,很多邮箱的发件人和收件人就是这种效果

实现步骤

  1. 继承ViewGroup,用一个列表childrenRect存储所有子View的位置信息
  2. 重写onLayout方法,在onLayout中遍历所有子View,使用childrenRect的位置信息依次对所有子View进行布局
  3. 重写generateLayoutParams方法,返回一个MarginLayoutParams对象,因为要支持Margin,就需要获得子View的Margin值,重写这个方法可以将子View的LayoutParams转为MarginLayoutParams,从而获取Margin值
  4. 重写onMeasure方法,在这个方法中,遍历所有子View,调用measureChildWithMargins测量子View的宽高
  5. 再从左到右、从上到下依次进行排列,当排到一个子View,当前行已用宽度加上该子View的宽度后超过了ViewGroup的宽度,就换行排列,并累加行高
  6. 最后setMeasuredDimension设置流布局本身的高度

代码

class FlowLayout(context: Context, attrs: AttributeSet) : ViewGroup(context, attrs) {

    private val childrenRect = mutableListOf<Rect>()
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY) {
            //父ViewGroup必须为确定的宽度,否则直接报错
            throw RuntimeException("FlowLayout's width must be EXACTLY!")
        }
        //父ViewGroup的宽度
        val groupWidth = MeasureSpec.getSize(widthMeasureSpec)
        //父ViewGroup的高度
        var groupHeight = paddingTop
        //当前子View行测量后进行位置摆放使用的高度
        var lineHeight = paddingTop
        //当前子View行测量后进行位置摆放使用的宽度
        var lineWidth = paddingLeft
        //当前行最高高度
        var lineMaxHeight = 0
        for (i in 0 until childCount) {
            //遍历获取子View并测量子View的宽高
            val child = getChildAt(i)
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0)
            //保存每一个子View的位置,要考虑子View的Margin
            val childRect: Rect = initRect(i)
            val childParams: MarginLayoutParams = child.layoutParams as MarginLayoutParams
            //设置位置和尺寸
            setRect(childRect, lineWidth, childParams, lineHeight, child)

            //累加当前行child的宽度
            lineWidth += child.measuredWidth + childParams.leftMargin + childParams.rightMargin
            //如果当前行已经放满了子View,换一行放置这个子View
            if (lineWidth > groupWidth) {
                //清零lineWidth,但不是直接赋值为0,要考虑padding
                lineWidth = paddingLeft

                //将当前行使用的高度加上上一行统计的最高高度,为下一行布局做准备
                lineHeight += lineMaxHeight
                groupHeight += lineMaxHeight

                //清零当前行最高高度,因为已经换行
                lineMaxHeight = 0

                //设置换行后的子View位置
                setRect(childRect, lineWidth, childParams, lineHeight, child)
                //累加当前行child的宽度
                lineWidth += child.measuredWidth + childParams.leftMargin + childParams.rightMargin
            }
            //得到当前行的最高子View
            var childHeight = childRect.height() + childParams.topMargin + childParams.bottomMargin
            lineMaxHeight = max(lineMaxHeight, childHeight)
        }
        //遍历完成后,要加上最后一行的高度
        groupHeight += lineMaxHeight
        groupHeight = View.resolveSize(groupHeight, heightMeasureSpec)
        setMeasuredDimension(groupWidth, groupHeight)
    }

    /**
     * 为子View设置位置和尺寸
     */
    private fun setRect(
        childRect: Rect,
        lineWidth: Int,
        childParams: MarginLayoutParams,
        lineHeight: Int,
        child: View
    ) {
        childRect.left = lineWidth + childParams.leftMargin
        childRect.top = lineHeight + childParams.topMargin
        childRect.right = childRect.left + child.measuredWidth
        childRect.bottom = childRect.top + child.measuredHeight
    }

    /**
     * 初始化第i个子View的Rect,如果已有,返回第i个子View的Rect
     *
     * @param i
     * @return
     */
    private fun initRect(i: Int): Rect {
        val rect: Rect
        if (childrenRect.size <= i) {
            rect = Rect()
            childrenRect.add(rect)
        } else {
            rect = childrenRect.get(i)
        }
        return rect
    }

    /**
     * 遍历所有子View,进行布局
     */
    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
        for (i in childrenRect.indices) {
            getChildAt(i).layout(
                childrenRect[i].left,
                childrenRect[i].top,
                childrenRect[i].right,
                childrenRect[i].bottom
            )
        }
    }

    /**
     * 因为要支持Margin,返回一个MarginLayoutParams对象
     */
    override fun generateLayoutParams(attrs: AttributeSet?): LayoutParams {
        return MarginLayoutParams(context, attrs)
    }
}

相关文章

网友评论

      本文标题:自定义View之流布局

      本文链接:https://www.haomeiwen.com/subject/ihixvltx.html