View学习(三)- View的布局(layout)过程
前段开始学习View的工作原理,前两篇博客的草稿都已经写好了,本想一鼓作气写完所有的相关文章,然后经历了一段连续加班,结果今天准备继续写文章时,把之前写好的东西都忘记了,又重新梳理了一遍,所以说那怕就是已经掌握的知识,也要记得温故而知新。
言归正传,之前我们讨论过了measure过程,measure过程完成之后,我们就可以通过 getMeasuredWidth
或getMeasuredHeight
来得到View的宽高尺寸了。而知道了宽高尺寸之后,剩下的就是布局(layout)过程了。说直白点,怎么把具一个宽高已定的View摆放在屏幕上。
我们先来看一个demo。
/**
*@author www.yaoxiaowen.com
*/
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/linearLayout1"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@color/google_yellow"
>
<LinearLayout
android:id="@+id/linearLayout2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="@color/white"
>
<TextView
android:id="@+id/textView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="textView1"
android:background="@color/green"
android:paddingLeft="20dp"
android:paddingRight="40dp"
android:paddingTop="10dp"
/>
<Button
android:id="@+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="button1"
android:layout_marginLeft="20dp"
android:background="@color/blue"
/>
</LinearLayout>
<TextView
android:id="@+id/text2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="text2"
android:layout_marginLeft="40dp"
android:background="@color/red"
/>
</LinearLayout>
具体的实现效果,是这个样子。
我们暂且不讨论代码当中的实现原理之类的,仅仅就想想一种场景,给你一些尺寸固定但不同的盒子,摆放在一个大房间的地面上,我们应该怎么摆放呢。
之所以要根据这个demo来设想这种场景,是因为我觉的,不管代码怎么写,但是基本的道理都不复杂,正所谓大道至简,(毕竟我们不是做那些高大上的算法的)。自己根据场景去设想怎么摆放,同样的也有助于理解代码。
假设我们按照 LinearLayout
竖直方式的规则来摆放,那么开始摆放第一个盒子肯定从房间的左上角开始计算位置,根据父容器padding,本身的margin等来决定具体摆放的坐标。然后再在竖直方向上 向前推进摆放第二个盒子。
我并不知道上面那段文字有没有能清楚的表达出我的意思, 但是这是我前段时间关于这个layout的思考过程。思考过程本身就很难表达,但是我想这样思考是有助于理解这个过程的。
和measure过程一样,layout过程也是一个递归过程,并且ViewGroup类本身不 应该具体实现onLayout
,具体实现过程应该放在Framelayout
,LinearLayout
,RelativieLayout
等容器类中。
当layout过程完成之后,我们就可以得到View的左上角和右下角的坐标了。(就是left,top,right,bottom)。值得注意的是这个坐标是相对坐标,就是相对于View父容器的坐标。所以通过递归来计算是最自然最方便的方式。
这几天看了关于编程语言历史方面的文章, 在早期的编程语言中,是没有递归这个概念的,是后来有人想出了这个概念,才逐渐的在各种编程语言中实现。所以说现在我们来看稀奇平常很自然的概念,但是当初第一个想出这个概念的人,那就是天才了。
下面我们就从源码角度来进行分析。
performTraversals
调用了PerformLayout
方法。
//ViewRootImpl.java www.yaoxiaowen.com
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
//...
final View host = mView;
//...
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
//...
}
host
其实就是DecorView
了,在执行layout
时,注意传递的头两个参数都是0,这说明 整个layout过程是从屏幕坐标系的 左上角开始执行的。
下一步还是走到了View#Layout()
, View#Layout
方法如下:
//View.java www.yaoxiaowen.com
public void layout(int l, int t, int r, int b) {
//...
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
//setOpticalFrame方法内部还是调用了 setFrame()方法,所以无论如何,最终都会执行setFrame()方法。
// setFrame()方法会将View新的left,top,right,bottom存储到View的成员变量中,并且返回一个boolean值,
//返回true,则表示View的位置或尺寸发生了变化;否则就是未发生变化。
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
//如果View布局发生了变化,或者存在 PFLAG_LAYOUT_REQUIRED 标记,则执行以下代码
//首先 触发onLayout 方法,View中默认的onLayout是个空方法。
//(因为摆放位置是父容器负责的,View中存在该方法是为了遍历循环的需要)。
//但是 容器类的ViewGroup则需要实现onLayout方法,从而在 onLayout()方法中依次循环子View,
//并调用他们的Layout方法。
onLayout(changed, l, t, r, b);
//...
//我们可以通过 View的addOnLayoutChangeListener(View.onLayoutChangeListener)方法
//向View 中添加多个 Layout 发生变化的事件监听器。
//这些监听器都存储在 mListenerInfo.mOnLayoutChangeListeners 这个List当中
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLayoutChangeListeners != null) {
// 首先对 mOnLayoutChangeListeners 中的事件监听器 进行拷贝。
ArrayList<OnLayoutChangeListener> listenersCopy =
(ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
int numListeners = listenersCopy.size();
for (int i = 0; i < numListeners; ++i) {
// 遍历注册的事件监听器,依次调用其 onLayoutChange 方法,这样 Layout事件监听器就得到了相应。
listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
}
}
}
//....
}
setFrame()
方法的作用也不复杂。
//View.java www.yaoxiaowen.com
// setFrame()方法会将View新的left,top,right,bottom存储到View的成员变量中,并且返回一个boolean值,
//返回true,则表示View的位置或尺寸发生了变化;否则就是未发生变化。
protected boolean setFrame(int left, int top, int right, int bottom) {
//...
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom;
mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
//...
}
通过分析以上代码可以知道,在layout()
方法当中,主要做的就是将left,top,right,bottom方法保存起来,而View自身怎么被布局(其实我觉的用摆放这个词更合适)。则是父容器需要完成的工作。
因为总所周知的原因(其实就是各个容器类ViewGroup布局子元素的方式差异很大)。ViewGroup并没有实现具体的onLayout
方法,各个具体的ViewGroup,比如LinearLayout
,RelativeLayout
,FrameLayout
等,它们都有它们具体的 onLayout
实现方式。
我们以LinearLayout
为例来进行分析。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mOrientation == VERTICAL) {
layoutVertical(l, t, r, b);
} else {
layoutHorizontal(l, t, r, b);
}
}
和measure过程是一样的套路,也是分为两种不同的情况,我们就以 竖直方向为例。(源码比较多,并且实际也要考虑使用layout_weight
的情况,不过我们仅仅只抽取一部分分析基本原理)。
//LinearLayout.java www.yaoxiaowen.com
void layoutVertical(int left, int top, int right, int bottom) {
final int paddingLeft = mPaddingLeft;
int childTop;
int childLeft;
//...
//遍历子元素
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child == null) {
childTop += measureNullChild(i);
} else if (child.getVisibility() != GONE) {
//经过了measure过程,我们已经知道了 View的宽高
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
//布局文件中,可能设置了 margin等
final LinearLayout.LayoutParams lp =
(LinearLayout.LayoutParams) child.getLayoutParams();
//...
//随着遍历过程,childTop是逐步增加的。所以说在竖直方向上,后续的View都被摆放在靠下的位置。
childTop += lp.topMargin;
childLeft = mPaddingLeft + lp.leftMargin;
//在设置子View时,传递的是 子View 左上角的坐标和宽高尺寸
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight);
//childTop逐步的向下增加,并且在增加的过程中,也考虑了 子View的margin,以及偏移量。
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
i += getChildrenSkipCount(child, i);
}
}
}
在该方法中,通过调用setChildFrame
来为子元素指定相应的位置。
而setChildFrame
只是很简单的一句话。
private void setChildFrame(View child, int left, int top, int width, int height) {
child.layout(left, top, left + width, top + height);
}
而child.layout
其实不就是继续调用 onLayout
之类的嘛。所以这样就形成了完美的递归。直到把整个View树都完成了layout过程。
自此,我们就分析完了layout过程,相比与measure过程,简单了很多。并且理解起来也更容易,就想想在一个大房间的地面上,我们怎么使用递归的方式来摆放很多的盒子。
最后再来看看几个方法。
public final int getWidth() {
return mRight - mLeft;
}
public final int getHeight() {
return mBottom - mTop;
}
public final int getMeasuredHeight() {
return mMeasuredHeight & MEASURED_SIZE_MASK;
}
public final int getMeasuredWidth() {
return mMeasuredWidth & MEASURED_SIZE_MASK;
}
同样都是得到宽高的方式,这两组方法有什么区别呢?现在我们了解了Measure和layout过程,那么我们就知道了,getMeasuredWidth()
方法是在measure过程完成之后可以调用的,而getWidth()
方法则是在 layout过程之后可以调用的。并且几乎在所有情况下,两者的返回值都是相等的。
github:
https://github.com/yaowen369
欢迎对于本人的博客内容批评指点,如果问题,可评论或邮件(yaowen369@gmail.com)联系
<p >
欢迎转载,转载请注明出处.谢谢
</p>
<script type="text/javascript">
function Curgo()
{
window.open(window.location.href);
}
</script>
View学习(三)- View的布局(layout)过程的更多相关文章
- View学习(二)-View的测量(measure)过程
在上一篇文章中,我们介绍了DecorView与MeasureSpec, 下面的文章就开始讨论View的三大流程. View的三大流程都是通过ViewRoot来完成的.ViewRoot对应于ViewRo ...
- React Native 学习(三)之 FlexBox 布局
React Native 学习(三)之 FlexBox 布局
- [Android学习笔记]view的layout过程学习
View从创建到显示到屏幕需要经历几个过程: measure -> layout -> draw measure过程:计算view所占屏幕大小layout过程:设置view在屏幕的位置dr ...
- View绘制详解(四),谝一谝layout过程
上篇博客我们介绍了View的测量过程,这只是View显示过程的第一步,第二步就是layout了,这个我们一般译作布局,其实就是在View测量完成之后根据View的大小,将其一个一个摆放在ViewGro ...
- [Android学习笔记]View的measure过程学习
View从创建到显示到屏幕需要经历几个过程: measure -> layout -> draw measure过程:计算view所占屏幕大小layout过程:设置view在屏幕的位置dr ...
- View学习(四)-View的绘制(draw)过程
View的draw过程相比之于measrue过程,也是比较简单的.并且在我们自定义View时,也经常需要重写onDraw方法,来绘制出我们要实现的效果. 如之前的文章所说,绘制的流程也是起始于View ...
- View的三次measure,两次layout和一次draw
我在<Android视图结构>这篇文章中已经描述了Activity,Window和View在视图架构方面的关系.前天,我突然想到为什么在setContentView中能够调用findVie ...
- [Android学习笔记]View的draw过程学习
View从创建到显示到屏幕需要经历几个过程: measure -> layout -> draw measure过程:计算view所占屏幕大小layout过程:设置view在屏幕的位置dr ...
- [Android FrameWork 6.0源码学习] View的重绘过程之Layout
View绘制的三部曲,测量,布局,绘画现在我们分析布局部分测量部分在上篇文章中已经分析过了.不了解的可以去我的博客里找一下 View的布局和测量一样,都是从ViewRootImpl中发起,ViewRo ...
随机推荐
- 【转】HTTP长连接与短连接(2)
一.什么是长连接 HTTP1.1规定了默认保持长连接(HTTP persistent connection ,也有翻译为持久连接),数据传输完成了保持TCP连接不断开(不发RST包.不四次握手),等待 ...
- Github 开源:升讯威 Winform 开源控件库( Sheng.Winform.Controls)
Github 地址:https://github.com/iccb1013/Sheng.Winform.Controls 本控件库中的代码大约写于10年前(2007年左右),难免有不成熟与欠考虑之处, ...
- AtomicInteger的使用
JDK API 1.7相关介绍 可以用原子方式更新的 int 值.有关原子变量属性的描述,请参阅 java.util.concurrent.atomic 包规范.AtomicInteger 可用在应用 ...
- 使用sqlserver搭建高可用双机热备的Quartz集群部署【附源码】
一般拿Timer和Quartz相比较的,简直就是对Quartz的侮辱,两者的功能根本就不在一个层级上,如本篇介绍的Quartz强大的序列化机制,可以序列到 sqlserver,mysql,当然还可以在 ...
- linux下部署php项目-Apache、php、mysql关联
linux下部署php项目环境可以分为两种,一种使用Apache,php,mysql的压缩包安装,一种用yum命令进行安装. 使用三种软件的压缩包进行安装,需要手动配置三者之间的关系.apache和p ...
- WEB前端:浏览器(IE+Chrome+Firefox)常见兼容问题处理--02
兼容问题目录 8.IE6不支持固定定位 9.IE6下前面元素浮动,后面元素不浮动后他们之间会有间隙 10.IE6下双边距问题 11.IE67下父级有边框,子级有margin的话会不起作用 12.IE6 ...
- css3转盘抽奖
做到一个活动,需要转盘抽奖,于是想到使用css3的动画效果,其中主要包含transition的动画过渡,transform的rotate的旋转效果,在这里只用到2d的旋转, 特别强调的是,因为需要和后 ...
- 我的第一个jQuery插件--表格隔行变色
虽然网上有大量的插件供我们去使用,但不一定有一款适合你的,必要的时候还是要自己动手去敲的.下面,开始我的第一个插件... 参考<锋利的JQuery>,JQuery为开发插件增设了俩个方法: ...
- ionic中应用sass
在学习ionic过程中看到sass,总结了一下基本用法和问题解决办法1.首先需要一个ionic项目,并执行下面的命令ionic start CustomSass blank && cd ...
- Thinkphp中的内置标签用法
Thinkphp中的内置标签有:Volist,Foreach,For,Switch,比较标签,范围判断标签,IF,Present,Empty,Defined,Assign,Define,标签嵌套,im ...