设计一个FrameLayout(Kotlin)

作者: 姜康 | 来源:发表于2018-03-27 09:22 被阅读93次

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

    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

    相关文章

      网友评论

        本文标题:设计一个FrameLayout(Kotlin)

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