首先上Gravity的代码,Android原版的Gravity搞得挺复杂的,太高端了。但基本思路是使用位运算来做常量,我就自己消化了一些,按自己的思路来实现。

先上代码,在做分析。

 package kross.android.widget;

 /**
* 重力属性,控制容器内子控件的排布方式
* @author kross([email protected])
* @update 2014-10-21 11:30:59 第一次编写完成
* @update 2014-10-21 11:51:32 更改了center的值,让left | right 可以变成 center_horizontal,垂直方向同理
* */
public class KGravity { /** 水平方向排布:左对齐 */
public static final int LEFT = 0x01;
/** 水平方向排布:水平居中 */
public static final int CENTER_HORIZONTAL = 0x03;
/** 水平方向排布:右对齐 */
public static final int RIGHT = 0x02; /** 垂直方向排布:顶对齐 */
public static final int TOP = 0x10;
/** 垂直方向排布:垂直居中 */
public static final int CENTER_VERTICAL = 0x30;
/** 垂直方向排布:底对齐 */
public static final int BOTTOM = 0x20; /** 居中 */
public static final int CENTER = 0x33; /** 重力属性值,默认为左上角对齐,也就是 LEFT | TOP */
private int mValue = 0x11; private KGravity() {} private KGravity(int value) throws Exception{
//取出水平分量和垂直分量
int hv = value & 0x0f;
int vv = value & 0xf0;
if (hv > 0x03 || vv > 0x30) { //分量超出范围
throw new Exception("a wrong gravity params");
}
//如果分量为0,说明只有一部分参数,那么使用现有属性中的该分量
if (hv == 0) {
hv = mValue & 0x0f;
}
if (vv == 0) {
vv = mValue & 0xf0;
}
//合并水平分量和垂直分量
mValue = hv | vv;
} /**
* @see #newInstance(int)
* */
public static KGravity newInstance() {
return new KGravity();
} /**
* 创建一个KGravity,参数请在水平方向的参数和垂直方向的参数各挑一个,如:LEFT | TOP
* @see #LEFT
* @see #HORIZONTAL_CENTER
* @see #RIGHT
* @see #TOP
* @see #VERTICAL_CENTER
* @see #RIGHT
* */
public static KGravity newInstance(int value){
try {
return new KGravity(value);
} catch (Exception e) {
e.printStackTrace();
}
return null;
} /**
* 得到水平方向的排布属性
* @see #LEFT
* @see #HORIZONTAL_CENTER
* @see #RIGHT
* */
public int getHorizontalGravity() {
return mValue & 0x0f;
} /**
* 得到垂直方向上的排布属性
* @see #TOP
* @see #VERTICAL_CENTER
* @see #RIGHT
* */
public int getVerticalGravity() {
return mValue & 0xf0;
}
}

Gravity的思路:

首先需要构想最终的效果。可以将内容的排布分为水平分量和垂直分量。也就是水平方向上可以靠左边,靠右边,靠中间。垂直方向上可以靠上,靠下,靠中间。

两个分量互不相关,那么3x3=9,总共就可以组合成9种不同的排布方式。

水平的三个值,分别为left(0x01),right(0x02),center_horizontal(0x03)。我希望水方向上的分量就只能是这三个值,其他的都是有问题的。垂直方向上的三个值也按照这个思路分别设置为top(0x10),bottom(0x20),center_vertical(0x30)。这样水平和垂直两组不同的值分别占据低4位和高4位互影响。

(PS.一开始我是将center_horizontal设置为0x02的,但是后来发现水平方向上两个较小的值(0x01和0x02)或在一起,就会变成第三个值,所以我想left | right -> center_horizontal,在某种程度上,也是一种有意义的做法吧。即:向左对齐的同时又向右对齐,那不就是水平居中嘛……)

1.55行,我写了一个newInstance()方法来构造KGravity对象,我打算将KGravity对象本身作为参数传给自己写的LinearLayout,我想这样更有意义一些。newInstance()方法调用了private的构造函数。

2.构造方法有两个,无参数的默认构造方法直接将Gravity的值设置为0x11,也就是左对齐,和上对齐。有参的构造方法先将水平,垂直分量取出来,然后分别进行判断,是否都大于了规定好的值,然后再判断是否为0,如果是0的话,可以理解为没有这方面的分量,那么就设置为默认值。最后再将两个分量通过或运算赋值给mValue方法。

3.最后82行,92行设置了两个public方法供外部使用,通过与运算的特性分别取出想要的分量即可。

接下来再看自己写的LinearLayout的代码,只完成了垂直布局的部分。先上代码,再做解释:

 package kross.android.widget;

 import android.content.Context;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout; /**
* 自己实现的LinearLayout
* @author kross([email protected])
* @update 2014-10-16 19:42:47 第一次编写,实现垂直布局
* @update 2014-10-20 20:17:45 完成Gravity
* */
public class KLinearLayout extends ViewGroup { private static final String TAG = "KLinearLayout"; /** 垂直布局 */
public static final byte ORITENTATION_VERTICAL = 0x1;
/** 水平布局 */
public static final byte ORITENTATION_HORIZONTAL = 0x0; /** 线性布局的方向,默认值为水平
* @see #ORITENTATION_HORIZONTAL
* @see #ORITENTATION_VERTICAL */
private int mOritentation = ORITENTATION_HORIZONTAL; /** 最终的宽度 */
private int mWidth;
/** 最终的高度 */
private int mHeight; /** 是否遍历过子控件的大小 */
private boolean mIsTraversalForChildSize = false;
/** 子控件的总宽度 */
private int mChildsTotalWidth = 0;
/** 子控件的总高度 */
private int mChildsTotalHeight = 0; private KGravity mGravity = null; public KLinearLayout(Context context) {
super(context);
mOritentation = ORITENTATION_HORIZONTAL;
mGravity = KGravity.newInstance();
} /**
* 设置线性布局的方向:垂直或水平
* @param oritentation
* @see #ORITENTATION_HORIZONTAL
* @see #ORITENTATION_VERTICAL
* */
public void setOritentation(byte oritentation) {
mOritentation = oritentation;
} public void setGravity(KGravity gravity) {
mGravity = gravity;
} public KGravity getGravity() {
return mGravity;
} @Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Log.i(TAG, "onMeasure");
if (mOritentation == ORITENTATION_HORIZONTAL) {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
} else {
measureVertical(widthMeasureSpec, heightMeasureSpec);
}
} @Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
Log.i(TAG, "onLayout l:" + l + " t:" + t + " r:" + r + " b:" + b); if (mOritentation == ORITENTATION_HORIZONTAL) {
layoutHorizontal(l, t, r, b);
} else {
layoutVertical(l, t, r, b);
}
} /**
* 垂直测量
* */
private void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
Log.i(TAG, "measureVertical"); int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec); /**
* 已经使用了的高度,容器是空的,已经使用的高度为0,如果已经存在一个高度为x的子控件,这个值为x。
* 这个值也表示,所有的子控件所需要的高度总值。
*/
int heightUsed = 0;
View childTemp = null;
for (int index = 0; index < getChildCount(); index++) { //遍历子控件
childTemp = getChildAt(index);
if (childTemp.getVisibility() == View.GONE) {
continue;
}
measureChildWithMargins(childTemp, widthMeasureSpec, 0, heightMeasureSpec, heightUsed); //获取子控件并测量它的大小
LinearLayout.LayoutParams childLp = (LinearLayout.LayoutParams)childTemp.getLayoutParams(); //子控件的高度,包括子控件的上下外边距一起累加到heightUsed值中
heightUsed = heightUsed + childTemp.getMeasuredHeight() + childLp.topMargin + childLp.bottomMargin;
//因为是垂直布局,所以宽度直选最大的一个
mWidth = Math.max(mWidth, childTemp.getMeasuredWidth() + childLp.leftMargin + childLp.rightMargin);
} mWidth = mWidth + getPaddingLeft() + getPaddingRight(); //加上左右内边距 switch (widthMode) {
case MeasureSpec.UNSPECIFIED:
case MeasureSpec.AT_MOST: //wrap_parent
mWidth = Math.min(widthSize, mWidth); //因为是包裹内容,所以宽度应该是尽可能的小
break;
case MeasureSpec.EXACTLY: //match_parent
mWidth = widthSize; //与父控件一样大,那么宽度应该是父控件给的,也就是参数所给的
break;
} mHeight = heightUsed + getPaddingTop() + getPaddingBottom(); //所有子控件的高度和 + 上下内边距 switch (heightMode) {
case MeasureSpec.UNSPECIFIED:
case MeasureSpec.AT_MOST: //wrap_parent
mHeight = Math.min(heightSize, mHeight);
break;
case MeasureSpec.EXACTLY: //match_parent
mHeight = heightSize;
break;
} setMeasuredDimension(mWidth, mHeight);
} /**
* 水平测量
* */
private void measureHorizontal(int widthMeasureSpec, int heightMeasureSpec) {
Log.i(TAG, "measureHorizontal");
setMeasuredDimension(100, 100);
} /**
* 垂直布局
* */
private void layoutVertical(int l, int t, int r, int b) { int avaliableLeft = getPaddingLeft();
int avaliableTop = 0; //垂直排布,top值只需要初始化一次,后续不断叠加height + marginTop + marginBottom即可得到下一个child的top值
switch (mGravity.getVerticalGravity()) {
case KGravity.TOP:
avaliableTop = getPaddingTop();
break;
case KGravity.CENTER_VERTICAL:
traversalChildsForTotalSizeWithMargins();
avaliableTop = mHeight / 2 - mChildsTotalHeight / 2;
break;
case KGravity.BOTTOM:
traversalChildsForTotalSizeWithMargins();
avaliableTop = mHeight - getPaddingBottom() - mChildsTotalHeight;
break;
} //开始遍历排布
View childTemp = null;
for (int i = 0; i < getChildCount(); i++) {
childTemp = getChildAt(i);
if (childTemp.getVisibility() == View.GONE) {
childTemp.layout(0, 0, 0, 0);
continue;
} LinearLayout.LayoutParams childLp = (LinearLayout.LayoutParams)childTemp.getLayoutParams(); int childLeft = 0; //child的left值,因为和gravity值相关,所以遍历的时候才能确定。
switch (mGravity.getHorizontalGravity()) {
case KGravity.LEFT:
childLeft = avaliableLeft + childLp.leftMargin;
break;
case KGravity.CENTER_HORIZONTAL:
childLeft = mWidth / 2 - childTemp.getMeasuredWidth() / 2;
break;
case KGravity.RIGHT:
childLeft = mWidth - getPaddingRight() - childLp.rightMargin - childTemp.getMeasuredWidth();
break;
} //layout()方法会确切的限制View的显示大小,真正显示到屏幕上的矩形区域,是由layout的四个参数所决定的。
//指定的是控件本身四个顶点的位置,不包括margin
childTemp.layout(childLeft,
avaliableTop + childLp.topMargin,
childTemp.getMeasuredWidth() + childLeft,
childTemp.getMeasuredHeight() + avaliableTop + childLp.topMargin);
//top值叠加
avaliableTop = avaliableTop + childTemp.getMeasuredHeight() + childLp.topMargin + childLp.bottomMargin;
}
} /**
* 遍历一遍所有占空间的子控件,将他们的高度宽度(包括外边距)累加起来
* @TODO 稍有重复
* */
private void traversalChildsForTotalSizeWithMargins() {
if (mIsTraversalForChildSize) {
return;
}
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if (child.getVisibility() == View.GONE) {
continue;
}
LinearLayout.LayoutParams lp = (android.widget.LinearLayout.LayoutParams) child.getLayoutParams();
mChildsTotalWidth += child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
mChildsTotalHeight += child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
}
mIsTraversalForChildSize = true;
} /**
* 水平布局
* */
private void layoutHorizontal(int l, int t, int r, int b) { }
}

思路就是按照之前分析过的《Android UI测量、布局、绘制过程探究》,我们只需要挨个实现onMeasure(),和onLayout()方法就可以了(LinearLayout作为一个容器而已,不需要实现onDraw)。

1.68行,onMeasure()方法,根据mOritentation的值,来选择调用是measureVertical()还是measureHorizontal()。和Android原版的LinearLayout一样,mOritentation的默认值是水平的。

2.91行,measureVertical()方法,首先需要明确,LinearLayout中的子控件是线性排布的,并且是垂直的线性排布,那么如果是match_parent,LinearLayout的高度应该和父控件一样大,如果所有子控件的高度叠在一起加上它们所有的上下外边距都超过了父控件可用的高度,也没有关系,父控件依然是直接使用onMeasure()传进来的值。具体的情况我用下图表示,一目了然。

实现LinearLayout(垂直布局,Gravity内容排布)-LMLPHP

onMeasure()方法就是根据子控件的情况和自身的LayoutParams来设定好自己的高度宽度。

3.78行onLayout方法,根据mOritentation的值调用了layoutVertical()方法。

4.158行,layoutVertical()方法,它的目的是需要确定好每个子控件的位置,并调用子控件的layout()方法即可。如果是所谓的默认情况,也就是left|top的话,就好办了,就一种情况,就贴着左边,挨个垒起来就行了,但实际上,我们刚刚前面写了KGravity类,就是用来控制内部子控件排布方式的,因此需要对这些进行考虑,判断,做出正确的排布。

面对这样看似复杂的问题,我们需要把它分割成几个小问题来解决,首先确定一点,当前是垂直布局,所有的子控件都是从上到下垒在一起的,不管你怎么对齐,靠上靠下,靠左靠右都一样。

于是对于top值就有思路了。

对于TOP对齐的情况来说,第一个子控件的top值应该是父控件的paddingTop+自己的marginTop,下一个子控件的位置是上一个子控件位置的bottom+它的marginBottom再加上自己的marginTop,以此类推。

对于CENTER_VERTICAL的情况来说,先得把所有的子控件占用高度都算出来垒在一起。然后在用父控件高度的一半减去前面总数的一半就可以得到第一个控件的top值,后面的子控件top值的方法情况与上面相同。

对于BOTTOM的情况来说,一样要把子控件总的占用高度获取,然后用父控件的高度减去子控件总的占用高度得到第一个子控件的top值,剩下的子控件情况相同。

所以说:对于top值,我们要做的是根据不同的情况做好第一次初始化工作。大家如果不明白在纸上画画图就明白了。

而对于left值,就需要对每个控件逐个的进行计算了。

如果是LEFT对齐,那么大家的left值都是paddingLeft+自己的marginLeft。

如果是CENTER_HORIZONTAL,left值是父控件宽度的一半减去子控件宽度的一半。

如果是RIGHT对齐,那么大家就是贴着右边了。

不明白的,还是画画图,搞清楚这些数值的关系就好了。

以上这些就是layoutVertical内容的全部了,笔者也是经验不足,通过写了几个demo的测试,不断改Gravity的参数来检验布局的效果。然后修修改改的总算把这个功能给做好了。

最后贴一下测试demo的代码:

 public class MainActivity extends Activity {

     @SuppressLint("ServiceCast") @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
LinearLayout root = (LinearLayout)LayoutInflater.from(this).inflate(R.layout.activity_main, null);
setContentView(root); KLinearLayout myLinearLayout = new KLinearLayout(this);
myLinearLayout.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
myLinearLayout.setPadding(10, 20, 30, 40);
myLinearLayout.setGravity(KGravity.newInstance(KGravity.BOTTOM));
myLinearLayout.setOritentation(KLinearLayout.ORITENTATION_VERTICAL); root.addView(myLinearLayout); TextView tv3 = new TextView(this);
tv3.setText("abcd哈哈你好");
tv3.setTextSize(50);
LayoutParams tv3lp = new LayoutParams(100, 100);
tv3lp.setMargins(10, 10, 10, 10);
tv3.setLayoutParams(tv3lp); myLinearLayout.addView(tv3); TextView tv1 = new TextView(this);
tv1.setText("adbcdsaf");
tv1.setVisibility(View.VISIBLE);
tv1.setLayoutParams(new LayoutParams(200, 200)); myLinearLayout.addView(tv1); TextView tv2 = new TextView(this);
tv2.setText("abcd哈哈你好");
tv2.setTextSize(50);
LayoutParams tv2lp = new LayoutParams(200, 200);
tv2lp.setMargins(20, 20, 20, 20);
tv2.setLayoutParams(tv2lp); myLinearLayout.addView(tv2);
}
}

我将效果做成一个gif图片,来展示3*3排布的效果。如下所示

实现LinearLayout(垂直布局,Gravity内容排布)-LMLPHP

以上。

05-11 13:47