首先需要明确的是,内边距padding
属于控件内部的尺寸,由其自己管理,而外边距margin
则由其所在的ViewGroup管理。
当我们自定义ViewGroup
时,默认是不支持子控件的外边距属性的,原因是因为默认的LayoutParams
中没有外边距相关的逻辑,即使我们在xml
中设置了margin
也不会其任何作用。所以为了支持margin
属性,我们就应该替换掉默认的LayoutParams
,使用支持margin
的MarginLayoutParams
,同时在ViewGroup
的测量和布局时,都要考虑到各种margin
所占的尺寸。综上所属,为了使我们自定义的ViewGroup
支持外边距需要实现以下几点:
- 重写
LayoutParams
相关方法,使用支持margin
的MarginLayoutParams
- 测量时使用
measureChildWithMargins
,而不使用measureChild
或measureChildren
- 测量的时候确定的尺寸要加上外边距;布局的时候,子控件放置的坐标要考虑外边距。
根据以上几点,详细展开讲解。
- 重写方法,设置
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)
}
- 我们以实现一个垂直放置子控件的布局为例,看一下如何测量和布局才能使外边距生效。
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.measuredHeight
或child.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>
我们的VerticalLayout
的onMeasure
中使用measureChildWithMargins
,打印child.measureHeight
,得到其值为1788像素。并且子控件显示正常,底部和顶部外边距效果正确。当我们将此方法修改为measureChild
,打印child.measureHeight
,结果为2088像素,根据当前设备像素密度,相差的结果为300像素或100dp,正好和我们设置的底部外边距与顶部外边距之和相等。并且同时子控件显示不正确,比设置的高度高了100dp,验证了我们前面的内容。所以当我们在自定义ViewGroup
,并且需要考虑子控件外边距时,要使用measureChildWithMargins
。当然使用measureChild
也可以,你需要自己处理更多的逻辑,使用上measureChildWithMargins
更方便,而且系统的FrameLayout
、LinearLayout
等布局中都是使用的measureChildWithMargins
,可见其相对measureChild
使用更广泛、更易用。