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

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

先上代码,在做分析。

 1 package kross.android.widget;
 2
 3 /**
 4  * 重力属性,控制容器内子控件的排布方式
 5  * @author kross([email protected])
 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([email protected])
 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排布的效果。如下所示

以上。

时间: 2024-11-05 15:55:09

实现LinearLayout(垂直布局,Gravity内容排布)的相关文章

CSS布局之div交叉排布与底部对齐--flex实现

最近在用wordpress写页面时,设计师给出了一种网页排布图样,之前从未遇到过,其在电脑上(分辨率大于768px)的效果图如下: 而在手机(分辨率小于等于768px)上要求这样排列: 我想到了两种方法 第一种是用bootstrap的row.col-md配合col-md-push.col-md-pull来实现,代码如下: 1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="utf-8">

2.2.1 LinearLayout(线性布局)

本节引言 本节开始讲Android中的布局,Android中有六大布局,分别是: LinearLayout(线性布局),RelativeLayout(相对布局),TableLayout(表格布局) FrameLayout(帧布局),AbsoluteLayout(绝对布局),GridLayout(网格布局) 而今天我们要讲解的就是第一个布局,LinearLayout(线性布局),我们屏幕适配的使用 用的比较多的就是LinearLayout的weight(权重属性),在这一节里,我们会详细地解析 L

Android基础入门教程——2.2.1 LinearLayout(线性布局)

Android基础入门教程--2.2.1 LinearLayout(线性布局) 标签(空格分隔): Android基础入门教程 本节引言: 本节开始讲Android中的布局,Android中有六大布局,分别是: LinearLayout(线性布局),RelativeLayout(相对布局),TableLayout(表格布局) FrameLayout(帧布局),AbsoluteLayout(绝对布局),GridLayout(网格布局) 而今天我们要讲解的就是第一个布局,LinearLayout(线

New UI-布局之LinearLayout(线性布局)详解

New UI-布局之LinearLayout(线性布局)详解  --转载请注明出处:coder-pig,欢迎转载,请勿用于商业用途! 小猪Android开发交流群已建立,欢迎大家加入,无论是新手,菜鸟,大神都可以,小猪一个人的 力量毕竟是有限的,写出来的东西肯定会有很多纰漏不足,欢迎大家指出,集思广益,让小猪的博文 更加的详尽,帮到更多的人,O(∩_∩)O谢谢! 小猪Android开发交流群:小猪Android开发交流群群号:421858269 新Android UI实例大全目录:http://

Android精通:View与ViewGroup,LinearLayout线性布局,RelativeLayout相对布局,ListView列表组件

UI的描述 对于Android应用程序中,所有用户界面元素都是由View和ViewGroup对象构建的.View是绘制在屏幕上能与用户进行交互的一个对象.而对于ViewGroup来说,则是一个用于存放其他View和ViewGroup对象的布局容器! Android为我们提供了View和ViewGroup的两个子类的集合,提供常用的一些输入控件(比如按钮,图片和文本域等)和各种各样的布局模式(比如线程布局,相对布局,绝对布局,帧布局,表格布局等). 用户界面布局 在你APP软件上的,用户界面上显示

LinearLayout线性布局

作用 : 线性布局会将容器中的组件一个一个排列起来, LinearLayout可以控制组件横向或者纵向排列, 通过android:orientation属性控制; 不换行属性 : 线性布局中的组件不会自动换行, 如果组件一个一个排列到尽头之后, 剩下的组件就不会显示出来; 常用属性: (1)基线对齐 xml属性 : android:baselineAligned; 设置方法 : setBaselineAligned(boolean b); 作用 : 如果该属性为false, 就会阻止该布局管理器

布局Layouts之LinearLayout线性布局

从Hello world!开始,我们一直都是在一种布局下学习的,当然,对于基础内容的学习,还是没有任何问题的!但-- 在Android开发中UI设计也是十分重要的,当用户使用一个App时,最先感受到的不是这款软件的功能是否强大,而是界面设计是否赏心悦目,用户体验是否良好.也可以这样说,有一个好的界面设计去吸引用户的使用,才能让更多的用户体验到软件功能的强大. 那么,Android中几种常用布局则显得至关重要.各个布局既可以单独使用,也可以嵌套使用,我们应该在实际应用中应灵活变通. 第2章.Lin

android 59 LinearLayout 线性布局

##常见的布局* LinearLayout 线性布局线性布局往左右拉是拉不动的,> 线性布局的朝向 vertical|horizontal> 线性布局的权重 weight 和 0dip一起使用 <?xml version="1.0" encoding="utf-8"?> <!-- 线性布局控件自上而下整齐的排列 --> <LinearLayout xmlns:android="http://schemas.andr

布局Layouts之LinearLayout线性布局(转)

从Hello world!开始,我们一直都是在一种布局下学习的,当然,对于基础内容的学习,还是没有任何问题的!但—— 在Android开发中UI设计也是十分重要的,当用户使用一个App时,最先感受到的不是这款软件的功能是否强大,而是界面设计是否赏心悦目,用户体验是否良好.也可以这样说,有一个好的界面设计去吸引用户的使用,才能让更多的用户体验到软件功能的强大. 那么,Android中几种常用布局则显得至关重要.各个布局既可以单独使用,也可以嵌套使用,我们应该在实际应用中应灵活变通. LinearL