自定义ViewGroup时,如何处理子控件的外边距

首先需要明确的是,内边距padding属于控件内部的尺寸,由其自己管理,而外边距margin则由其所在的ViewGroup管理。
当我们自定义ViewGroup时,默认是不支持子控件的外边距属性的,原因是因为默认的LayoutParams中没有外边距相关的逻辑,即使我们在xml中设置了margin也不会其任何作用。所以为了支持margin属性,我们就应该替换掉默认的LayoutParams,使用支持marginMarginLayoutParams,同时在ViewGroup的测量和布局时,都要考虑到各种margin所占的尺寸。综上所属,为了使我们自定义的ViewGroup支持外边距需要实现以下几点:

  1. 重写LayoutParams相关方法,使用支持marginMarginLayoutParams
  2. 测量时使用measureChildWithMargins,而不使用measureChildmeasureChildren
  3. 测量的时候确定的尺寸要加上外边距;布局的时候,子控件放置的坐标要考虑外边距。

根据以上几点,详细展开讲解。

  1. 重写方法,设置MarginLayoutParams
override fun generateDefaultLayoutParams(): LayoutParams {
	 return MarginLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
}

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

override fun generateLayoutParams(p: LayoutParams?): LayoutParams {
	return MarginLayoutParams(p)
}	 
  1. 我们以实现一个垂直放置子控件的布局为例,看一下如何测量和布局才能使外边距生效。
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
	// 初始的总高度,考虑到ViewGroup自身的padding
	var totalHeight = paddingTop + paddingBottom
	var maxWidth = paddingStart + paddingEnd
	var childWidth:Int
	var childHeight = 0
	
	for (index in 0 until childCount) {
		val child = getChildAt(index) // 获取子控件
		// 获取子控件的布局参数类
		val params = child.layoutParams as MarginLayoutParams
		val childTopMargin = params.topMargin
		val childBottomMargin = params.bottomMargin
		val childStartMargin = params.marginStart
		val childEndMargin = params.marginEnd
		totalHeight += childHeight // 累加高度
		// 使用考虑外边距的测量方法,避免子控件分配的空间错误
		measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0)
		// 获取子控件的宽高
		childHeight = child.measuredHeight + childTopMargin + childBottomMargin
		childWidth = child.measuredWidth + childStartMargin + childEndMargin
		// 宽度取子控件中最宽的作为ViewGroup的宽度
		maxWidth = max(childWidth, maxWidth)
        totalHeight += childHeight	
	}
		// 考虑到传递的测量规格的不同,调用resolveSize方法获取最终的宽高
		val width = resolveSize(maxWidth, widthMeasureSpec)
		val height = resolveSize(totalHeight, heightMeasureSpec)
		// 设置测量尺寸
		setMeasuredDimension(width, height)	
}	
	

override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
	val childStart = paddingStart
	var childTop = paddingTop
	for (index in 0 until childCount) {
		val child = getChildAt(index)
		val params = child.layoutParams as MarginLayoutParams
		val childTopMargin = params.topMargin
		val childBottomMargin = params.bottomMargin
		val childStartMargin = params.marginStart
		val childEndMargin = params.marginEnd

		// 将外边距包含在子控件的宽度中
		val childWidth = child.measuredWidth + childStartMargin + childEndMargin
		// 高度则由于需要累加,左上角的坐标只和TopMarin有关,所以分开处理
		val childHeight = child.measuredHeight
		// 定位时考虑到TopMargin,它影响子控件的左上角坐标,进而影响整个子		控件的位置
		childTop += childTopMargin
		// 确定了子控件的坐标,调用layout方法,放置子控件
		child.layout(childStart, childTop, childStart + childWidth, childTop + childHeight)
		// 当前子控件如果设置了BottomMarin,则其会影响下一个子控件的放置位置,所以需要加上BottomMargin
		childTop += childHeight + childBottomMargin
	}
}	

以上就完成了ViewGroup的测量和布局。其中注意,我们使用的是measureChildWithMargins方法,而不是measureChild方法。我们知道,在为子控件生成测量规格的时候,需要考虑ViewGroup为子控件留了多少可用空间,由于外边距不属于控件内容的尺寸,所以其会占用父控件为其保留的控件,所以当我们在测量时,需要将外边距的部分减去。比如,父控件为子控件保留了100dp高度的控件,但是由于子控件自己设置了topMargin 10dp, bottomMargin 10dp,那么实际上子控件能用的只有80dp。如果我们在测量的时候不将这一部分减去,那么子控件将获得更多的空间,这会导致我们从child.measuredHeightchild.measuredWidth方法中获取的尺寸不正确,所以我们应该使用measureChildWithMargins.

下面我们对比一下measureChildWithMargins方法和measureChild方法源码

// measureChild
    /**
     * Ask one of the children of this view to measure itself, taking into
     * account both the MeasureSpec requirements for this view and its padding.
     * The heavy lifting is done in getChildMeasureSpec.
     *
     * @param child The child to measure
     * @param parentWidthMeasureSpec The width requirements for this view
     * @param parentHeightMeasureSpec The height requirements for this view
     */
    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);
    }

在为getChildMeasureSpec方法传参时,第二个参数代表当前ViewGroup占用的尺寸,其中包括其自身的padding和子控件的margin。这里我们可以看到,measureChild方法只考虑了padding,并没有考虑子控件的margin。下面再看一下measureChildWithMargins方法的实现;

    /**
     * Ask one of the children of this view to measure itself, taking into
     * account both the MeasureSpec requirements for this view and its padding
     * and margins. The child must have MarginLayoutParams The heavy lifting is
     * done in getChildMeasureSpec.
     *
     * @param child The child to measure
     * @param parentWidthMeasureSpec The width requirements for this view
     * @param widthUsed Extra space that has been used up by the parent
     *        horizontally (possibly by other children of the parent)
     * @param parentHeightMeasureSpec The height requirements for this view
     * @param heightUsed Extra space that has been used up by the parent
     *        vertically (possibly by other children of the parent)
     */
    protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

很明显,measureChildWithMargins中获取了子控件的MarginLayoutParams ,从而获取了其外边距属性,并将其添加到了getChildMeasureSpec方法的第二个参数中,这样一来,为子控件保留的尺寸将只用于它自己的内容,而不包括它设置的外边距。这些外边距将由当前ViewGroup来管理。

为了验证两个方法的区别,我们做一下实验:
我们在代码中引用自己开发的VerticalLayout,并为其子控件外边距,代码如下;

<com.example.viewdemos.viewgroup.VerticalLayout
	android:layout_width="match_parent"
	android:layout_height="match_parent">
	<TextView
		android:layout_width="match_parent"
		android:layout_height="match_parent"
		android:layout_marginTop="50dp"
		android:layout_marginBottom="50dp"
		android:background="@color/design_default_color_primary"
		android:gravity="center"
		android:text="dassssssssssssssssssssssssdasdasdasd" />
</com.example.viewdemos.viewgroup.VerticalLayout>		

我们的VerticalLayoutonMeasure中使用measureChildWithMargins,打印child.measureHeight,得到其值为1788像素。并且子控件显示正常,底部和顶部外边距效果正确。当我们将此方法修改为measureChild,打印child.measureHeight,结果为2088像素,根据当前设备像素密度,相差的结果为300像素或100dp,正好和我们设置的底部外边距与顶部外边距之和相等。并且同时子控件显示不正确,比设置的高度高了100dp,验证了我们前面的内容。所以当我们在自定义ViewGroup,并且需要考虑子控件外边距时,要使用measureChildWithMargins。当然使用measureChild也可以,你需要自己处理更多的逻辑,使用上measureChildWithMargins更方便,而且系统的FrameLayoutLinearLayout等布局中都是使用的measureChildWithMargins,可见其相对measureChild使用更广泛、更易用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

旅行者40

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值