实现LinearLayout(垂直布局,Gravity内容排布)
首先上Gravity的代码,Android原版的Gravity搞得挺复杂的,太高端了。但基本思路是使用位运算来做常量,我就自己消化了一些,按自己的思路来实现。
先上代码,在做分析。
package kross.android.widget; /**
* 重力属性,控制容器内子控件的排布方式
* @author kross(krossford@foxmail.com)
* @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(krossford@foxmail.com)
* @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()传进来的值。具体的情况我用下图表示,一目了然。
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内容排布)的更多相关文章
- Andriod中textview垂直水平居中及LinearLayout内组件的垂直布局
1.textview 垂直水平居中的设置 Android:gravity="center_vertical|center" 2.LinearLayout中设置控件垂直布局,默认的是 ...
- CSS布局之div交叉排布与底部对齐--flex实现
最近在用wordpress写页面时,设计师给出了一种网页排布图样,之前从未遇到过,其在电脑上(分辨率大于768px)的效果图如下: 而在手机(分辨率小于等于768px)上要求这样排列: 我想到了两种方 ...
- 按照excel文档中的内容在当前cad图纸中自动排布实体
本例实现的主要功能是读取excel文档中的内容,其次是将按照读取的信息在当前cad图纸中添加相应的实体.下面先介绍实现代码: CString excelPath; //外部excel文档的地址 Upd ...
- Android LinearLayout线性布局
LinearLayout是线性布局控件:要么横向排布,要么竖向排布 决定性属性:必须有的! android:orientation:vertical (垂直方向) .horizontal(水平方向) ...
- Android精通:View与ViewGroup,LinearLayout线性布局,RelativeLayout相对布局,ListView列表组件
UI的描述 对于Android应用程序中,所有用户界面元素都是由View和ViewGroup对象构建的.View是绘制在屏幕上能与用户进行交互的一个对象.而对于ViewGroup来说,则是一个用于存放 ...
- Android零基础入门第25节:最简单最常用的LinearLayout线性布局
原文:Android零基础入门第25节:最简单最常用的LinearLayout线性布局 良好的布局设计对于UI界面至关重要,在前面也简单介绍过,目前Android中的布局主要有6种,创建的布局文件默认 ...
- android 59 LinearLayout 线性布局
##常见的布局* LinearLayout 线性布局线性布局往左右拉是拉不动的,> 线性布局的朝向 vertical|horizontal> 线性布局的权重 weight 和 0dip一起 ...
- Android布局管理详解(1)—— LinearLayout 线性布局
Android的布局方式共有6种,分别是LinearLayout(线性布局).TableLayout(表格布局).FrameLayout(帧布局).RelativeLayout(相对布局).GridL ...
- LinearLayout 线性布局
android:orientation 设置布局管理器内组件的排列方式,可设置为 horizontal (水平排列).vertical (垂直排列) android:gravity 设置布局管理器内组 ...
随机推荐
- 非局部均值滤波算法的python实现
如题,比opencv自带的实现效果好 #coding:utf8 import cv2 import numpy as np def psnr(A, B): return 10*np.log(255*2 ...
- Failed to chmod /Users/fei/Library/Developer/CoreSimulator/Devices/DB5AC3C0错误的解决办法
当XCode遇到此问题的时候,可通过重启模拟器和XCode来解决,拿走不谢
- matlab GUI工作原理
例如,用GUIDE创建名为ceshi的GUI程序,其m文件的主函数有如下形式.那么,打开该GUI时,它到底是怎么运行的呢?以下略作小结,欢迎大家补充 function varargout = cesh ...
- 在Javascript中 声明时用"var"与不用"var"的区别,== 和 ===的区别
今天,被问到两个JS问题,当时没回答到重点,问题虽然看起来简单,但是细节却马虎不得,在此做下记录: 1. 在Javascript中 声明时用"var"与不用"var&qu ...
- requestAnimationFrame优势何在?
大概半年前,无意中在网上看到一个新的js函数requestAnimationFrame,据说,此函数可以优化传统的js动画效果,似乎是未来js动画的新方向. 当时我所在的项目正好用到了和js动画有关的 ...
- 大数据中Linux集群搭建与配置
因测试需要,一共安装4台linux系统,在windows上用vm搭建. 对应4个IP为192.168.1.60.61.62.63,这里记录其中一台的搭建过程,其余的可以直接复制虚拟机,并修改相关配置即 ...
- 用Unity的UGUI实现简单摇杆
1.在Canvas下新建一个空对象作为我们的摇杆,命名为Joystick. 摇杆由背景和杆两部分组成,所以在Joystick下新建一个Image作为摇杆的背景,命名为BG. 在BG下新建一个Image ...
- PHP核心技术——反射
反射: 反射指在PHP运行状态中,扩展分析PHP程序,导出或提取出关于类.方法.属性.参数等的详细信息,包括注释.这种动态获取信息以及动态调用对象方法的功能称为反射API class person{ ...
- Datawhale MySQL 训练营 Task3 表操作
目录 学习内容 1.MySQL 表数据类型 2. 用SQL语句创建表 3. 用SQL语句向表中添加数据 4. 用SQL语句删除表 5. 用SQL语句修改表 作业 参考链接 学习内容 1.MySQL 表 ...
- UIWebView控件中 字体大小和字体样式的修改
修改UIWebView控件中字体的样式: NSString *htmlString = [NSString stringWithContentsOfFile:self.webPath encoding ...