设计一个FrameLayout(Kotlin)

96
姜康
0.1 2018.03.27 09:22 字数 675

拆零件,然后再把零件拼装回去,来来回回对其结构也就熟悉了

FrameLayout的特点

  1. 子View按照添加顺序层叠显示
  2. FrameLayout的尺寸与其最大子View(可见的)的尺寸相等(加上padding值)
  3. 如果要让GONE的子View参与计算,则需要把setMeasureAllChildren(boolean) ,setConsiderGoneChildrenWhenMeasuring()设置为true
  4. 支持通过layout_gravity控制子View的布局

根据上述特点,我也要实现一个简单的FrameLayout,应该怎么开始呢?

这里自然考虑继承ViewGroup,然后重写onMeasure,onLayout方法,而且onLayout方法必须实现(因为这是一个抽象方法,子类必须实现)

测量

这里测量的过程是:遍历该ViewGroup,调用子View的measure方法进行测量,同时通过子View的LayoutParams获取到子View的Margin信息,然后综合View的测量尺寸与Margin值,算出该ViewGroup的尺寸,然后设置该尺寸为ViewGroup的测量尺寸。

/*
    * 尺寸测量
    * */
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {

        var maxWidth: Int = 0
        var maxHeight: Int = 0

        //遍历子View进行测量
        for (i in 0 until childCount) {
            val child = getChildAt(i)
            if (child.visibility != View.GONE) {

                //调用ViewGroup的一个实例方法(本质上是调用View的measure方法),
                // 由于FrameLayout的特点,子View之间并不干扰尺寸大小,所以已经使用的空间为0
                //该计算过程会计算padding和margin
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0)

                var layoutParams = child.layoutParams as LayoutParams

                //得到最大宽高,并考虑子View的Margin
                maxWidth = Math.max(maxWidth, child.measuredWidth + layoutParams.leftMargin + layoutParams.rightMargin)
                maxHeight = Math.max(maxHeight, child.measuredHeight + layoutParams.topMargin + layoutParams.bottomMargin)

                //考虑FrameLayout本身的padding值
                maxWidth += paddingLeft + paddingRight
                maxHeight += paddingTop + paddingBottom

                //设置尺寸
                setMeasuredDimension(
                        resolveSize(maxWidth, widthMeasureSpec),
                        View.resolveSize(maxHeight, heightMeasureSpec)
                )

            }
        }


    }

注意一下,这里的LayoutParams并不是ViewGroup的LayoutParams,因为ViewGroup中的LayoutParams本身并不支持margin,而只支持宽高的属性,不过没关系,ViewGroup类中还有一个MarginLayoutParams,添加了对margin的支持。

而这里我们打算设计一个FrameLayout,那么,就得支持Margin与layout_gravity;

/*
    * 子View通过LayoutParams告诉父View它想怎么布局
    * {@link android.R.styleable#ViewGroup_Layout ViewGroup Layout Attributes} 包含了LayoutParams类支持的所有属性
    * 包括layout_width和layout_height
    * 作为基类的的LayoutParams 仅仅只描述View想要的宽高尺寸,对于每一个尺寸,可选则MATCH_PARENT和WRAP_CONTENT
    * 其中MATCH_PARENT表示子View希望像父View一样大
    * WRAP_CONTENT表示View需要足够大,以容纳它的内容(包括padding)
    *
    * 不同的ViewGroup子类具有不同的LayoutParams之类实现,用来添加各自的特性
    * 如本类中的MarginLayoutParams可以用来添加对Margin和gravity的支持
    * */
     class LayoutParams : MarginLayoutParams {

        companion object {
            val UNSPECIFIED_GRAVITY = -1
        }

        //添加gravity支持
        var gravity = UNSPECIFIED_GRAVITY

        constructor(c: Context, attrs: AttributeSet?) : super(c, attrs) {
            val typedArray = c.obtainStyledAttributes(R.styleable.KFrameLayout_Layout)
            gravity = typedArray.getInt(R.styleable.KFrameLayout_Layout_layout_gravity, UNSPECIFIED_GRAVITY)
            typedArray.recycle()
        }

        constructor(width: Int, height: Int) : super(width, height)
        constructor(source: MarginLayoutParams?) : super(source)
        constructor(source: ViewGroup.LayoutParams?) : super(source)
        constructor(width: Int, height: Int, gravity: Int) : super(width, height) {
            this.gravity = gravity
        }

        constructor(source: LayoutParams) : super(source) {
            this.gravity = source.gravity
        }


    }

如果就只是这样自定义了自己的LayoutParams,然后将子View获取到的LayoutParams强制转换到自身的LayoutParams,这个自定义View并不能起作用,具体原因后面会进行说明。

布局

经过了测量过程,这里我们可以拿到测量尺寸了,然后遍历子View,进行相应的布局,主要就是定义子View的左边界,上边界的距离。

 //ViewGroup的onLayout方法是抽象方法,子类必须实现
    //此处根据gravity布局子View
    //该过程的核心思想就是遍历子View,为子View找到四个坐标值,然后调用其子View自身的layout方法进行布局
    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
        layoutChildren(l, t, r, b)
    }
private fun layoutChildren(l: Int, t: Int, r: Int, b: Int) {

        val parentLeft = paddingLeft
        val parentRight = r - l - paddingRight

        val parentTop = paddingTop
        val parentBottom = b - t - paddingBottom

        for (i in 0 until childCount) {
            val child = getChildAt(i)
            val childWidth = child.measuredWidth
            val childHeight = child.measuredHeight
            if (child.visibility != View.GONE) {

                var childLeft: Int
                var childTop: Int

                val layoutParams: LayoutParams = child.layoutParams as LayoutParams
                var gravity = layoutParams.gravity
                if (gravity == -1) {
                    gravity = LayoutParams.UNSPECIFIED_GRAVITY
                }

                //从左到右布局,还是从右到左布局(国内极少使用)
                val absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection)

                val verticalGravity = gravity and Gravity.VERTICAL_GRAVITY_MASK

                //横向
                childLeft = when (absoluteGravity and Gravity.HORIZONTAL_GRAVITY_MASK) {
                    Gravity.CENTER_HORIZONTAL -> {
                        //此处计算得理清概念
                        parentLeft + layoutParams.rightMargin - layoutParams.leftMargin + (parentRight - parentLeft - childWidth) / 2
                    }
                    Gravity.RIGHT -> {
                        parentRight - layoutParams.rightMargin - childWidth
                    }
                    else -> {
                        parentLeft + layoutParams.leftMargin
                    }
                }


                //纵向
                childTop = when (verticalGravity) {
                    Gravity.CENTER_VERTICAL -> {
                        parentTop + layoutParams.bottomMargin - layoutParams.topMargin + (parentBottom - parentTop - childHeight) / 2
                    }
                    Gravity.BOTTOM -> {
                        parentBottom - layoutParams.bottomMargin
                    }
                    else -> {
                        parentTop + layoutParams.topMargin
                    }
                }

                child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight)

            }
        }
    }

这个布局的过程,得根据自身的特点进行设置,例如FrameLayout由于子View之间并不互相影响,属于层叠关系,因为就不用考虑兄弟 View之间的布局关系处理了,这样就容易得多。

此处还有一个难点就是,你得理解清楚margin,padding的真实含义,以及布局中的left,right,居中布局的实际运算过程。

完善

前面说到过,LayoutParams如果直接强行转换会有问题的,至于有什么问题,就得从View的加载机制开始说起,今天暂不说明,后面有空继续,这里先写出解决方案,就是得重写几个方法:

 override fun checkLayoutParams(p: ViewGroup.LayoutParams?): Boolean {
        return p is LayoutParams
    }

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

    override fun generateLayoutParams(p: ViewGroup.LayoutParams?): ViewGroup.LayoutParams {
        return LayoutParams(p)
    }

    override fun generateDefaultLayoutParams(): ViewGroup.LayoutParams {
        return LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,ViewGroup.LayoutParams.WRAP_CONTENT)
    }

源码

KComponent

https://github.com/jiangkang/KComponent

Android 源码阅读计划
Web note ad 1