Android继承ViewGroup自定义流式布局

继承ViewGroup自定义布局主要分两步

1.重写onMeasure()方法,自己根据子view的大小测量容器的宽高

2.重写onLayout()方法,自己对每一个子view进行布局

下面分享一下自己继承View Group实现流式布局的过程

import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup

class MyFlowLayout : ViewGroup {

    //存放所有View的容器
    private val allViewLists = ArrayList>()
    //实例一个List 用于存放一行子View
    private val singleLineView = ArrayList()

    @JvmOverloads
    constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0): super(context, attrs, defStyleAttr)

    override fun generateLayoutParams(attrs: AttributeSet?): LayoutParams {
        return MarginLayoutParams(context,attrs)
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        allViewLists.clear()
        singleLineView.clear()
        //获取自身的模式和宽高
        val widthModel = MeasureSpec.getMode(widthMeasureSpec)
        val heightModel = MeasureSpec.getMode(heightMeasureSpec)
        val widthSize = MeasureSpec.getSize(widthMeasureSpec)
        val heightSize = MeasureSpec.getSize(heightMeasureSpec)

        //测量子view的大小
        //注意测量了所有可见View,如果view 的Visiable 为GONE则不测量
        measureChildren(widthMeasureSpec, heightMeasureSpec)

        //自身的测量宽高
        var myWidth = 0
        var myHeight = 0

        //子View的测量数据
        var cParams: MarginLayoutParams?

        //计算单行每添加一个子view的宽度
        var singleLineChildrenWidth = 0

        for (i in 0 until childCount){
            val cView = getChildAt(i)
            //此处不用判断子View是否可见,不可见的子View在measureChildren()的时候已经被忽略掉了
            cParams = cView.layoutParams as MarginLayoutParams

            //当前所有View的宽度 大于 父View的可用宽度时 换行
            if (singleLineChildrenWidth + cView.measuredWidth + cParams.marginStart + cParams.marginEnd > widthSize - paddingStart - paddingEnd){
                //获取上一行的宽度,每次换行的时候,取出上一行最大的宽度作为父容器的宽度
                myWidth = myWidth.coerceAtLeast(getSingleLineWidth(singleLineView))
                //获取上一行的高度
                val lastLineHeight = getSingleLineHeight(singleLineView)
                //自身高度叠加
                myHeight += lastLineHeight
                //将上一行的View 添加到总的容器中,注意一定要copy一份,因为下面要清空singleLineView,否则将会只看到最后一行的View
                allViewLists.add(copyLineViews(singleLineView))

                //清空上一行的容器,和记录行宽行高的值
                singleLineView.clear()
                singleLineChildrenWidth = 0
            }
            //计算当前宽度
            singleLineChildrenWidth += cView.measuredWidth + cParams.marginStart + cParams.marginEnd

            singleLineView.add(cView)
        }
        //循环结束之后要计算最后一行的宽高,并决定父容器的宽高
        myWidth = myWidth.coerceAtLeast(getSingleLineWidth(singleLineView))
        myHeight += getSingleLineHeight(singleLineView)
        //把最后一行放入总容器
        allViewLists.add(singleLineView)

        setMeasuredDimension(
            if (widthModel == MeasureSpec.EXACTLY) widthSize else myWidth,
            if (heightModel == MeasureSpec.EXACTLY) heightSize else myHeight
        )
    }

    //拷贝一行view
    private fun copyLineViews(singleLineView: ArrayList): ArrayList {
        val copiedViews = ArrayList()
        for (i in 0 until singleLineView.size){
            copiedViews.add(singleLineView[i])
        }
        return copiedViews
    }

    //获取单行的宽度
    private fun getSingleLineWidth(singleLineView: ArrayList): Int{
        var singLineWidth = 0
        for (cView in singleLineView){
            val cParams = cView.layoutParams as MarginLayoutParams
            singLineWidth += cView.measuredWidth + cParams.marginStart + cParams.marginEnd
        }

        return singLineWidth
    }
    //获取单行高度
    private fun getSingleLineHeight(singleLineView: ArrayList): Int {
        var singleLineHeight = 0
        for (view in singleLineView){
            val cParams = view.layoutParams as MarginLayoutParams
            singleLineHeight = singleLineHeight.coerceAtLeast(view.measuredHeight + cParams.topMargin + cParams.bottomMargin)
        }
        return singleLineHeight
    }


    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
        //如果思路不清晰这个会比较麻烦
        //思路:
        //单个子View的left 和top是不断变化的,搞两个变量单独记录每一个子View的位置状态
        //在双层for循环中先对每一行布局,换行后记得重置left,增加top

        var left = paddingStart
        var top = paddingTop

        for (i in 0 until allViewLists.size){
            val thisLineViews = allViewLists[i]
            val thisLineHeight = getSingleLineHeight(thisLineViews)

            for (j in 0 until thisLineViews.size){
                val thisView = thisLineViews[j]
                val thisLp = thisView.layoutParams as MarginLayoutParams

                val cl = left + thisLp.marginStart
                val ct = top + thisLp.topMargin
                val cr = cl + thisView.measuredWidth
                val cb = ct + thisView.measuredHeight

                thisView.layout(cl,ct,cr,cb)

                left += thisView.measuredWidth + thisLp.marginEnd + thisLp.marginStart
            }
            //单行View循环完毕,走出来准备进入下一行之前,要重置left
            left = paddingStart
            top += thisLineHeight
        }
    }
}

代码中注释的比较清除了,这里就不再分析了,另外送上我最近学习AndroidView绘制的练习写的demo(主要是在知乎上HenCoder大佬发布的AndroidView系列教程的一些练习题),如果有兴趣欢迎参考

你可能感兴趣的:(android,kotlin)