相信大家都用过android里的padding(android:padding 或者 android:paddingLeft/Top/Right/Bottom),我们也都知道padding的作用是定义控件内容与控件边界的距离(margin是定义两个控件之间的距离)。也许大家都会用这一个属性了,但是也许不清楚这个属性是如何起作用的,这篇文章主要就是来解释这个问题。
比如,有这样一个布局:(注意:下面所有的分析都是基于orientation为vertical的LinearLayout基础上的)
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<LinearLayout
android:id="@+id/layout"
android:layout_width="300dp"
android:layout_height="300dp"
android:layout_gravity="center"
android:background="#8285BF"
android:orientation="vertical" >
<ImageView
android:id="@+id/image"
android:layout_width="200dp"
android:layout_height="200dp"
android:background="#FFFFAA"
android:scaleType="fitXY"
android:src="@drawable/ic_launcher" />
</LinearLayout>
</LinearLayout>
下面三张图分别是设置id为R.id.layout的LinearLayout的paddingTop为0,200,-200的效果图:
当padding设置为正数的时候,我们一般容易理解,但是当设置为负数的时候,我们要怎样去理解呢?
我们先对这个现象做一个结论:
我们可以用通俗一些的比喻来描述padding。比如咱们的手机都比较大,但是显示屏幕并没有占满整个手机,它的周围会有一个边框,这个边框就是上面说的padding>0的效果,它减小了Child View的可用空间,这个时候手机就是我们的LinearLayout,手机屏幕就是我们的ImageView。而padding<0的效果,就像我们通过窗户看天空,虽然看到的只有一小片天空,但其实还有很大的天空没有被我们看到,这时候窗户就是我们的LinearLayout,而天空就是ImageView。
所以,虽然我们的LinearLayout只有300dp的高度,但是因为padding,改变了它的可以装载Child View的空间大小,当padding>0的时候,减小了这个装载空间的大小,当padding<0的时候,增大了这个装载空间的大小,虽然增大的这一部分我们没有看到,但它确实存在。
上面的结论其实可以用上面的三幅图来解释。我们需要知道LinearLayout的默认Gravity属性为Gravity.TOP,即放在它里面的Child View会从LinearLayout可用空间的顶部依次向下排列,第一幅图不用说,ImageView正好对齐LinearLayout的顶部,而第二幅图因为LinearLayout有一个正数的padding,所以减小了一部分空间,第三幅图中ImageView跑到上面去了,说明它去了上面才能和LinearLayout的顶部对齐,也就是说这个LinearLayout上面还有一部分我们看不见但真实存在的空间。
如果想要了解padding的具体工作流程,就必须去查看源码了,而如果只是想了解padding的效果,下面的内容就可以不用看了。
padding之所以造成上面的现象,主要是影响了View的布局过程,即layout过程。我们依然以LinearLayout为例(orientation=vertical):
LinearLayout中的onLayout()方法会根据当前的方向来调用不同的方法来进行layout,比如orientation=vertical时会调用下面的方法:
void layoutVertical() {
final int paddingLeft = mPaddingLeft;
int childTop = mPaddingTop;
int childLeft;
// Where right end of child should go
final int width = mRight - mLeft;
int childRight = width - mPaddingRight;
// Space available for child
int childSpace = width - paddingLeft - mPaddingRight;
final int count = getVirtualChildCount();
final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
final int minorGravity = mGravity & Gravity.HORIZONTAL_GRAVITY_MASK;
if (majorGravity != Gravity.TOP) {
switch (majorGravity) {
case Gravity.BOTTOM:
// mTotalLength contains the padding already, we add the top
// padding to compensate
childTop = mBottom - mTop + mPaddingTop - mTotalLength;
break;
case Gravity.CENTER_VERTICAL:
childTop += ((mBottom - mTop) - mTotalLength) / 2;
break;
}
}
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child == null) {
childTop += measureNullChild(i);
} else if (child.getVisibility() != GONE) {
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
final LinearLayout.LayoutParams lp =
(LinearLayout.LayoutParams) child.getLayoutParams();
int gravity = lp.gravity;
if (gravity < 0) {
gravity = minorGravity;
}
switch (gravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
case Gravity.LEFT:
childLeft = paddingLeft + lp.leftMargin;
break;
case Gravity.CENTER_HORIZONTAL:
childLeft = paddingLeft + ((childSpace - childWidth) / 2)
+ lp.leftMargin - lp.rightMargin;
break;
case Gravity.RIGHT:
childLeft = childRight - childWidth - lp.rightMargin;
break;
default:
childLeft = paddingLeft;
break;
}
childTop += lp.topMargin;
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
i += getChildrenSkipCount(child, i);
}
}
}
首先在第4行记录下了默认的childTop,这是一个关键的变量,代表的是第一个Child View在LinearLayout中的位置。为mPaddingTop,即我们给LinearLayout设置的paddingTop,16、17行是根据当前LinearLayout的方向来获得主方向上的gravity属性和次方向上的gravity属性,比如这里的orientation为vertical,所以我们的主方向的gravity属性就为垂直方向上的gravity属性。
19行用来判断当前主方向上的gravity,从这行就可以看到,默认的gravity就是Gravity.TOP,如果我们的gravity==Gravity.TOP,那childTop=mPaddingTop。如果我们改变了gravity属性,如果是Gravity.BOTTOM,进入21行,22行中改变了childTop的值,这个值是怎么计算出来的呢?我们首先需要知道当gravity为Gravity.BOTTOM的时候,Child View该怎么排列,显然,Child View中的最后一个View需要和LinearLayout的底部对齐。而默认情况下(即什么都不做的情况),我们的的第一个View是和LinearLayout的顶部对齐的,所以我们需要移动整个Child View以保证最后一个View和LinearLayout底部对齐,移动之后,显然childTop的值就变了。22行就是用来计算这个操作之后的childTop的值的。而如果我们想要明白这个操作,需要先明白在layout过程之前,已经进行了measure过程,即每个View的大小都已经获取到。在LinearLayout的onMeasure()中,用一个成员变量mTotalLength记录下了“所有Child View的高度和(包括了margin) + mPadddingTop + mPaddingBottom”,我们可以记录为mTotalLength = contentHeight + mPaddingTop + mPaddingBottom,contentHeight为所有ChildView的高度。所以22行可以看做是:mBottom - mTop + mPaddingTop - (contentHeight + mPaddingTop + mPaddingBottom),即mBottom - mTop - mPaddingBottom - contentHeight,所以,Gravity.BOTTOM的情况下,是没有考虑mPaddingTop的,而上面的(mBottom - mTop - mPaddingBottom)可以看作是"真实的可以装载Child View的空间",用它减去contentHeight,就代表了新的childTop,我们的Child View和这个控件的底部刚好对齐【如果想象Child View和LinearLayout的顶部对齐(不要考虑mPaddingTop),这个公式就好理解了】。28行的计算和上面类似,可以化为:((mBottom - mTop - mPaddingTop - mPaddingBottom) - contentHeight) / 2,其中,(mBottom - mTop - mPaddingTop - mPaddingBottom)就可以看作是我们"真正可以装载Child View的空间"。用它减去Child View的高度,得到一个装载空间和装载内容的差值,再除以2,得到的就是我们的Child View需要偏移的距离。
之所以花这么大的力气解释这个childTop,就是因为它决定了第一个Child View的位置,后面的Child View都以它为基础来进行排列。从69~72行中可以看到,前一个Child View布局完成后,会改变childTop的值,后面的Child View布局的时候会以新的childTop值为标准。总之,我们记住这个childTop值是一个代表位置的值就好了。