实现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 设置布局管理器内组 ...
随机推荐
- spark-client 一直 accepted,无法提交任务,报错Failed to connect to driver at
这个问题的原因有几个: 1.客户端安装的机器一般是虚拟机,虚拟机的名称可能是随便搞的,然而,yarn-client模式提交任务,是默认把本机当成driver的.所以导致其他的机器无法通过host的na ...
- [BZOJ4883][Lydsy1705月赛]棋盘上的守卫[最小基环树森林]
题意 有一大小为 \(n*m\) 的棋盘,要在一些位置放置一些守卫,每个守卫只能保护当前行列之一,同时在每个格子放置守卫有一个代价 \(w\) ,问要使得所有格子都能够被保护,需要最少多少的代价. \ ...
- flask 实现异步非阻塞----gevent
我们都知道,flask不支持异步非阻塞的请求,我们可以创建一个新项目去测试一下,推荐大家使用pycharm去开发我们的flask 使用特别的方便. rom flask import Flask im ...
- TCP Over HTTP 的Buffer问题
记录下备忘. 场景:要把TCP拆成一个个HTTP请求,通过Proxy 1.HTTP Client上载数据到CCProxy ,然后再到Web服务器的时候. 如果数据量比较小,例如10个字节,Proxy就 ...
- Jmeter性能指标分析
以下是下载了服务器监控插件的各个组件的功能介绍,有助于以后jmeter的性能测试 1.jp@gc - Actiive Threads Over Time:不同时间的活动用户数量展示(图表) 当前的时间 ...
- WebGL------osg框架学习二
今天我们继续来学习osg.js框架.上一篇我们介绍了DrawActor对象绘制操作类和Drawable可绘制对象类,我们大致知道了osg对Drawable可绘制对象的绘制流程管理.今天我们要继续介绍S ...
- c语言数字图像处理(八):噪声模型及均值滤波器
图像退化/复原过程模型 高斯噪声 PDF(概率密度函数) 生成高斯随机数序列 算法可参考<http://www.doc.ic.ac.uk/~wl/papers/07/csur07dt.pdf&g ...
- 基于zookeeper实现分布式锁(续)
测试代码: 效果图:
- Python处理PDF和Word文档常用的方法
Python处理PDF和Word文档的模块是PyPDF2,使用之前需要先导入. 打开一个PDF文档的操作顺序是:用open()函数打开文件并用一个变量来接收,然后把变量给传递给PdfFileReade ...
- impala 使用记录
在命令行里面直接输入类似下面的语句,就可以执行impala sql语句. impala-shell -q "select * from xxxc limit 10;" 当用pyth ...