实现LinearLayout(垂直布局,Gravity内容排布)
首先上Gravity的代码,Android原版的Gravity搞得挺复杂的,太高端了。但基本思路是使用位运算来做常量,我就自己消化了一些,按自己的思路来实现。
先上代码,在做分析。
1 package kross.android.widget; 2 3 /** 4 * 重力属性,控制容器内子控件的排布方式 5 * @author kross(krossford@foxmail.com) 6 * @update 2014-10-21 11:30:59 第一次编写完成 7 * @update 2014-10-21 11:51:32 更改了center的值,让left | right 可以变成 center_horizontal,垂直方向同理 8 * */ 9 public class KGravity { 10 11 /** 水平方向排布:左对齐 */ 12 public static final int LEFT = 0x01; 13 /** 水平方向排布:水平居中 */ 14 public static final int CENTER_HORIZONTAL = 0x03; 15 /** 水平方向排布:右对齐 */ 16 public static final int RIGHT = 0x02; 17 18 /** 垂直方向排布:顶对齐 */ 19 public static final int TOP = 0x10; 20 /** 垂直方向排布:垂直居中 */ 21 public static final int CENTER_VERTICAL = 0x30; 22 /** 垂直方向排布:底对齐 */ 23 public static final int BOTTOM = 0x20; 24 25 /** 居中 */ 26 public static final int CENTER = 0x33; 27 28 /** 重力属性值,默认为左上角对齐,也就是 LEFT | TOP */ 29 private int mValue = 0x11; 30 31 private KGravity() {} 32 33 private KGravity(int value) throws Exception{ 34 //取出水平分量和垂直分量 35 int hv = value & 0x0f; 36 int vv = value & 0xf0; 37 if (hv > 0x03 || vv > 0x30) { //分量超出范围 38 throw new Exception("a wrong gravity params"); 39 } 40 //如果分量为0,说明只有一部分参数,那么使用现有属性中的该分量 41 if (hv == 0) { 42 hv = mValue & 0x0f; 43 } 44 if (vv == 0) { 45 vv = mValue & 0xf0; 46 } 47 //合并水平分量和垂直分量 48 mValue = hv | vv; 49 } 50 51 /** 52 * @see #newInstance(int) 53 * */ 54 public static KGravity newInstance() { 55 return new KGravity(); 56 } 57 58 /** 59 * 创建一个KGravity,参数请在水平方向的参数和垂直方向的参数各挑一个,如:LEFT | TOP 60 * @see #LEFT 61 * @see #HORIZONTAL_CENTER 62 * @see #RIGHT 63 * @see #TOP 64 * @see #VERTICAL_CENTER 65 * @see #RIGHT 66 * */ 67 public static KGravity newInstance(int value){ 68 try { 69 return new KGravity(value); 70 } catch (Exception e) { 71 e.printStackTrace(); 72 } 73 return null; 74 } 75 76 /** 77 * 得到水平方向的排布属性 78 * @see #LEFT 79 * @see #HORIZONTAL_CENTER 80 * @see #RIGHT 81 * */ 82 public int getHorizontalGravity() { 83 return mValue & 0x0f; 84 } 85 86 /** 87 * 得到垂直方向上的排布属性 88 * @see #TOP 89 * @see #VERTICAL_CENTER 90 * @see #RIGHT 91 * */ 92 public int getVerticalGravity() { 93 return mValue & 0xf0; 94 } 95 }
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的代码,只完成了垂直布局的部分。先上代码,再做解释:
1 package kross.android.widget; 2 3 import android.content.Context; 4 import android.util.Log; 5 import android.view.View; 6 import android.view.ViewGroup; 7 import android.widget.LinearLayout; 8 9 /** 10 * 自己实现的LinearLayout 11 * @author kross(krossford@foxmail.com) 12 * @update 2014-10-16 19:42:47 第一次编写,实现垂直布局 13 * @update 2014-10-20 20:17:45 完成Gravity 14 * */ 15 public class KLinearLayout extends ViewGroup { 16 17 private static final String TAG = "KLinearLayout"; 18 19 /** 垂直布局 */ 20 public static final byte ORITENTATION_VERTICAL = 0x1; 21 /** 水平布局 */ 22 public static final byte ORITENTATION_HORIZONTAL = 0x0; 23 24 /** 线性布局的方向,默认值为水平 25 * @see #ORITENTATION_HORIZONTAL 26 * @see #ORITENTATION_VERTICAL */ 27 private int mOritentation = ORITENTATION_HORIZONTAL; 28 29 /** 最终的宽度 */ 30 private int mWidth; 31 /** 最终的高度 */ 32 private int mHeight; 33 34 /** 是否遍历过子控件的大小 */ 35 private boolean mIsTraversalForChildSize = false; 36 /** 子控件的总宽度 */ 37 private int mChildsTotalWidth = 0; 38 /** 子控件的总高度 */ 39 private int mChildsTotalHeight = 0; 40 41 private KGravity mGravity = null; 42 43 public KLinearLayout(Context context) { 44 super(context); 45 mOritentation = ORITENTATION_HORIZONTAL; 46 mGravity = KGravity.newInstance(); 47 } 48 49 /** 50 * 设置线性布局的方向:垂直或水平 51 * @param oritentation 52 * @see #ORITENTATION_HORIZONTAL 53 * @see #ORITENTATION_VERTICAL 54 * */ 55 public void setOritentation(byte oritentation) { 56 mOritentation = oritentation; 57 } 58 59 public void setGravity(KGravity gravity) { 60 mGravity = gravity; 61 } 62 63 public KGravity getGravity() { 64 return mGravity; 65 } 66 67 @Override 68 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 69 Log.i(TAG, "onMeasure"); 70 if (mOritentation == ORITENTATION_HORIZONTAL) { 71 measureHorizontal(widthMeasureSpec, heightMeasureSpec); 72 } else { 73 measureVertical(widthMeasureSpec, heightMeasureSpec); 74 } 75 } 76 77 @Override 78 protected void onLayout(boolean changed, int l, int t, int r, int b) { 79 Log.i(TAG, "onLayout l:" + l + " t:" + t + " r:" + r + " b:" + b); 80 81 if (mOritentation == ORITENTATION_HORIZONTAL) { 82 layoutHorizontal(l, t, r, b); 83 } else { 84 layoutVertical(l, t, r, b); 85 } 86 } 87 88 /** 89 * 垂直测量 90 * */ 91 private void measureVertical(int widthMeasureSpec, int heightMeasureSpec) { 92 Log.i(TAG, "measureVertical"); 93 94 int widthMode = MeasureSpec.getMode(widthMeasureSpec); 95 int widthSize = MeasureSpec.getSize(widthMeasureSpec); 96 97 int heightMode = MeasureSpec.getMode(heightMeasureSpec); 98 int heightSize = MeasureSpec.getSize(heightMeasureSpec); 99 100 /** 101 * 已经使用了的高度,容器是空的,已经使用的高度为0,如果已经存在一个高度为x的子控件,这个值为x。 102 * 这个值也表示,所有的子控件所需要的高度总值。 103 */ 104 int heightUsed = 0; 105 View childTemp = null; 106 for (int index = 0; index < getChildCount(); index++) { //遍历子控件 107 childTemp = getChildAt(index); 108 if (childTemp.getVisibility() == View.GONE) { 109 continue; 110 } 111 measureChildWithMargins(childTemp, widthMeasureSpec, 0, heightMeasureSpec, heightUsed); //获取子控件并测量它的大小 112 LinearLayout.LayoutParams childLp = (LinearLayout.LayoutParams)childTemp.getLayoutParams(); 113 114 //子控件的高度,包括子控件的上下外边距一起累加到heightUsed值中 115 heightUsed = heightUsed + childTemp.getMeasuredHeight() + childLp.topMargin + childLp.bottomMargin; 116 //因为是垂直布局,所以宽度直选最大的一个 117 mWidth = Math.max(mWidth, childTemp.getMeasuredWidth() + childLp.leftMargin + childLp.rightMargin); 118 } 119 120 mWidth = mWidth + getPaddingLeft() + getPaddingRight(); //加上左右内边距 121 122 switch (widthMode) { 123 case MeasureSpec.UNSPECIFIED: 124 case MeasureSpec.AT_MOST: //wrap_parent 125 mWidth = Math.min(widthSize, mWidth); //因为是包裹内容,所以宽度应该是尽可能的小 126 break; 127 case MeasureSpec.EXACTLY: //match_parent 128 mWidth = widthSize; //与父控件一样大,那么宽度应该是父控件给的,也就是参数所给的 129 break; 130 } 131 132 mHeight = heightUsed + getPaddingTop() + getPaddingBottom(); //所有子控件的高度和 + 上下内边距 133 134 switch (heightMode) { 135 case MeasureSpec.UNSPECIFIED: 136 case MeasureSpec.AT_MOST: //wrap_parent 137 mHeight = Math.min(heightSize, mHeight); 138 break; 139 case MeasureSpec.EXACTLY: //match_parent 140 mHeight = heightSize; 141 break; 142 } 143 144 setMeasuredDimension(mWidth, mHeight); 145 } 146 147 /** 148 * 水平测量 149 * */ 150 private void measureHorizontal(int widthMeasureSpec, int heightMeasureSpec) { 151 Log.i(TAG, "measureHorizontal"); 152 setMeasuredDimension(100, 100); 153 } 154 155 /** 156 * 垂直布局 157 * */ 158 private void layoutVertical(int l, int t, int r, int b) { 159 160 int avaliableLeft = getPaddingLeft(); 161 int avaliableTop = 0; 162 163 //垂直排布,top值只需要初始化一次,后续不断叠加height + marginTop + marginBottom即可得到下一个child的top值 164 switch (mGravity.getVerticalGravity()) { 165 case KGravity.TOP: 166 avaliableTop = getPaddingTop(); 167 break; 168 case KGravity.CENTER_VERTICAL: 169 traversalChildsForTotalSizeWithMargins(); 170 avaliableTop = mHeight / 2 - mChildsTotalHeight / 2; 171 break; 172 case KGravity.BOTTOM: 173 traversalChildsForTotalSizeWithMargins(); 174 avaliableTop = mHeight - getPaddingBottom() - mChildsTotalHeight; 175 break; 176 } 177 178 //开始遍历排布 179 View childTemp = null; 180 for (int i = 0; i < getChildCount(); i++) { 181 childTemp = getChildAt(i); 182 if (childTemp.getVisibility() == View.GONE) { 183 childTemp.layout(0, 0, 0, 0); 184 continue; 185 } 186 187 LinearLayout.LayoutParams childLp = (LinearLayout.LayoutParams)childTemp.getLayoutParams(); 188 189 int childLeft = 0; //child的left值,因为和gravity值相关,所以遍历的时候才能确定。 190 switch (mGravity.getHorizontalGravity()) { 191 case KGravity.LEFT: 192 childLeft = avaliableLeft + childLp.leftMargin; 193 break; 194 case KGravity.CENTER_HORIZONTAL: 195 childLeft = mWidth / 2 - childTemp.getMeasuredWidth() / 2; 196 break; 197 case KGravity.RIGHT: 198 childLeft = mWidth - getPaddingRight() - childLp.rightMargin - childTemp.getMeasuredWidth(); 199 break; 200 } 201 202 //layout()方法会确切的限制View的显示大小,真正显示到屏幕上的矩形区域,是由layout的四个参数所决定的。 203 //指定的是控件本身四个顶点的位置,不包括margin 204 childTemp.layout(childLeft, 205 avaliableTop + childLp.topMargin, 206 childTemp.getMeasuredWidth() + childLeft, 207 childTemp.getMeasuredHeight() + avaliableTop + childLp.topMargin); 208 //top值叠加 209 avaliableTop = avaliableTop + childTemp.getMeasuredHeight() + childLp.topMargin + childLp.bottomMargin; 210 } 211 } 212 213 /** 214 * 遍历一遍所有占空间的子控件,将他们的高度宽度(包括外边距)累加起来 215 * @TODO 稍有重复 216 * */ 217 private void traversalChildsForTotalSizeWithMargins() { 218 if (mIsTraversalForChildSize) { 219 return; 220 } 221 for (int i = 0; i < getChildCount(); i++) { 222 View child = getChildAt(i); 223 if (child.getVisibility() == View.GONE) { 224 continue; 225 } 226 LinearLayout.LayoutParams lp = (android.widget.LinearLayout.LayoutParams) child.getLayoutParams(); 227 mChildsTotalWidth += child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin; 228 mChildsTotalHeight += child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; 229 } 230 mIsTraversalForChildSize = true; 231 } 232 233 /** 234 * 水平布局 235 * */ 236 private void layoutHorizontal(int l, int t, int r, int b) { 237 238 } 239 }
思路就是按照之前分析过的《Android UI测量、布局、绘制过程探究》,我们只需要挨个实现onMeasure(),和onLayout()方法就可以了(LinearLayout作为一个容器而已,不需要实现onDraw)。
1.68行,onMeasure()方法,根据mOritentation的值,来选择调用是measureVertical()还是measureHorizontal()。和Android原版的LinearLayout一样,mOritentation的默认值是水平的。
2.91行,measureVertical()方法,首先需要明确,LinearLayout中的子控件是线性排布的,并且是垂直的线性排布,那么如果是match_parent,LinearLayout的高度应该和父控件一样大,如果所有子控件的高度叠在一起加上它们所有的上下外边距都超过了父控件可用的高度,也没有关系,父控件依然是直接使用onMeasure()传进来的值。具体的情况我用下图表示,一目了然。
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的代码:
1 public class MainActivity extends Activity { 2 3 4 @SuppressLint("ServiceCast") @Override 5 protected void onCreate(Bundle savedInstanceState) { 6 super.onCreate(savedInstanceState); 7 LinearLayout root = (LinearLayout)LayoutInflater.from(this).inflate(R.layout.activity_main, null); 8 setContentView(root); 9 10 KLinearLayout myLinearLayout = new KLinearLayout(this); 11 myLinearLayout.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); 12 myLinearLayout.setPadding(10, 20, 30, 40); 13 myLinearLayout.setGravity(KGravity.newInstance(KGravity.BOTTOM)); 14 myLinearLayout.setOritentation(KLinearLayout.ORITENTATION_VERTICAL); 15 16 root.addView(myLinearLayout); 17 18 TextView tv3 = new TextView(this); 19 tv3.setText("abcd哈哈你好"); 20 tv3.setTextSize(50); 21 LayoutParams tv3lp = new LayoutParams(100, 100); 22 tv3lp.setMargins(10, 10, 10, 10); 23 tv3.setLayoutParams(tv3lp); 24 25 myLinearLayout.addView(tv3); 26 27 TextView tv1 = new TextView(this); 28 tv1.setText("adbcdsaf"); 29 tv1.setVisibility(View.VISIBLE); 30 tv1.setLayoutParams(new LayoutParams(200, 200)); 31 32 myLinearLayout.addView(tv1); 33 34 35 TextView tv2 = new TextView(this); 36 tv2.setText("abcd哈哈你好"); 37 tv2.setTextSize(50); 38 LayoutParams tv2lp = new LayoutParams(200, 200); 39 tv2lp.setMargins(20, 20, 20, 20); 40 tv2.setLayoutParams(tv2lp); 41 42 myLinearLayout.addView(tv2); 43 } 44 }
我将效果做成一个gif图片,来展示3*3排布的效果。如下所示
以上。