目录

目录

1. 知识基础

具体请看我写的另外一篇文章:(1)自定义View基础 - 最易懂的自定义View原理系列


2. 作用

计算View视图的位置。

即计算View的四个顶点位置:Left、Top、Right和Bottom


3. layout过程详解

同measure过程一样,layout过程根据View的类型分为两种情况:

  1. 如果View = 单一View,则仅计算本身View的位置;
  2. 如果View = VieGroup,除了计算自身View的位置外,还需要确定子View在父容器中的位置。

    View树的位置是由包含的每个子视图的位置所决定,所以想计算整个View树的位置,就需要递归去计算每一个子视图的位置(与measure过程同理)

接下来,我将详细分析这两种情况下的layout过程。

3.1 单一View的layout过程

  • 应用场景
    在没有现成的View,需要自己实现的时候,就使用自定义View,一般继承自View、SurfaceView等,特点是:不包含子View。

    1. 如:制作一个支持加载网络图片的ImageView
    2. 特别注意:自定义View在大多数情况下都有替代方案,利用图片或者组合动画来实现,但是使用后者可能会面临内存耗费过大,制作麻烦更诸多问题。

单一View的layout过程如下图所示:

单一View的layout过程

下面我将一个个方法进行详细分析。

3.1.1 layout()

  • 作用:确定View本身的位置。

    即设置View本身的四个顶点位置

  • 源码分析如下:(仅贴出关键代码)
public void layout(int l, int t, int r, int b) {  

    // 当前视图的四个顶点
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight; // setFrame() / setOpticalFrame():确定View的位置
// 即初始化四个顶点的值,然后判断当前View大小和位置是否发生了变化并返回 (具体请看下面源码分析)
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b); //如果视图的大小和位置发生变化,会调用onLayout()
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) { // onLayout():确定该View所有的子View在父容器的位置
// 由于单一View是没有子View的,所以onLayout()是一个空实现(后面会详细说)
onLayout(changed, l, t, r, b); // 由于确定位置与具体布局有关,所以onLayout()在ViewGroup和View均没有实现。
// 在单一View中,onLayout()是一个空实现(后面会详细说)
// 在ViewGroup中,onLayout()被定义为抽象方法
// 所以onLayout()需要ViewGroup的子类去重写实现(后面会详细说)
... } /*
* setOpticalFrame()源码分析
**/ private boolean setOpticalFrame(int left, int top, int right, int bottom) {
Insets parentInsets = mParent instanceof View ?
((View) mParent).getOpticalInsets() : Insets.NONE;
Insets childInsets = getOpticalInsets(); // setOpticalFrame()实际上是调用setFrame()
return setFrame(
left + parentInsets.left - childInsets.left,
top + parentInsets.top - childInsets.top,
right + parentInsets.left + childInsets.right,
bottom + parentInsets.top + childInsets.bottom);
} /*
* setFrame()源码分析
**/ protected boolean setFrame(int left, int top, int right, int bottom) {
...
// 通过以下赋值语句记录下了视图的位置信息,即确定View的四个顶点
// 即确定了视图的位置
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom; mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
}

下面,我们继续分析在layout()中调用的onLayout()

3.2 onLayout()

  • 作用:空实现。

对于单一View来说,由于在layout()中已经对自身View进行了位置计算,所以单一View的layout()已经完成了。

  • 源码分析:
// 当这个view和其子view被分配一个大小和位置时,被layout()调用。 即单个View的情况
// View的onLayout()为空实现 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { // 参数说明
* @param changed 当前View的大小和位置改变了
* @param left 左部位置
* @param top 顶部位置
* @param right 右部位置
* @param bottom 底部位置 }

至此,单一View的layout过程已经分析完毕。

3.1.3 总结

单一View的layout过程解析如下:

单一View的layout过程

3.2 ViewGroup的layout过程

  • 应用场景
    自定义ViewGroup一般是利用现有的组件根据特定的布局方式来组成新的组件,大多继承自ViewGroup或各种Layout(含有子View)。

    如:底部导航条中的条目,一般都是上图标(ImageView)、下文字(TextView),那么这两个就可以用自定义ViewGroup组合成为一个Veiw,提供两个属性分别用来设置文字和图片,使用起来会更加方便。

    Paste_Image.png
  • 原理(步骤)
    步骤1:ViewGroup调用layout()计算自身的位置;
    步骤2:ViewGroup调用onLayout()遍历子View并调用子View layout()确定自身子View的位置。

    步骤2类似于单一View的layout过程

Paste_Image.png

这样自上而下、一层层地传递下去,直到完成整个View树的layout()过程

  • ViewGroup的layout过程
    如下图所示:

ViewGroup的layout过程

这里需要注意的是:
ViewGroup和View同样拥有layout()onLayout(),二者是不一样的。

  • 一开始计算ViewGroup位置时,调用的是ViewGroup的layout()onLayout()
  • 当开始遍历子View计算子View位置时,调用的是子View的layout()onLayout()

    类似于单一View的layout过程

下面我将一个个方法进行详细分析。

3.2.1 layout()

  • 作用:确定ViewGroup本身的位置。

    这里是ViewGroup的layout()

  • 源码分析如下:(仅贴出关键代码)
// 与单一View的layout()源码是一致的。
public void layout(int l, int t, int r, int b) { // 当前视图的四个顶点
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight; // setFrame() / setOpticalFrame():确定View的位置
// 即初始化四个顶点的值,然后判断当前View大小和位置是否发生了变化并返回 (具体请看下面源码分析)
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b); //如果视图的大小和位置发生变化,会调用onLayout()
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) { // onLayout():确定该ViewGroup所有子View在父容器的位置
// 由于单一View是没有子View的,所以onLayout()是一个空实现(后面会详细说)
onLayout(changed, l, t, r, b); // 由于确定位置与具体布局有关,所以onLayout()在ViewGroup没有实现。(被定义为抽象方法)
// 所以onLayout()需要ViewGroup的子类去重写实现(后面会详细说)
... } /*
* setOpticalFrame()源码分析
**/ private boolean setOpticalFrame(int left, int top, int right, int bottom) {
Insets parentInsets = mParent instanceof View ?
((View) mParent).getOpticalInsets() : Insets.NONE;
Insets childInsets = getOpticalInsets(); // setOpticalFrame()实际上是调用setFrame()
return setFrame(
left + parentInsets.left - childInsets.left,
top + parentInsets.top - childInsets.top,
right + parentInsets.left + childInsets.right,
bottom + parentInsets.top + childInsets.bottom);
} /*
* setFrame()源码分析
**/ protected boolean setFrame(int left, int top, int right, int bottom) {
...
// 通过以下赋值语句记录下了视图的位置信息,即确定View的四个顶点
// 即确定了视图的位置
mLeft = left;
mTop = top;
mRight = right;
mBottom = bottom; mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
}

下面,我们继续分析在layout()中调用的onLayout()

3.2.2 onLayout()

  • 作用:计算该ViewGroup包含所有的子View在父容器的位置()

    1. 定义为抽象方法,需要重写
    2. 原因:由于子View的确定位置与具体布局有关,所以onLayout()在ViewGroup没有实现。
  • 源码分析:
// 其中,ViewGroup的抽象方法onLayout()也被override标注,所以也是重写的方法
// 重写的是其父类view中的onLayout(),即单一View中的onLayout() (为空实现) @Override
protected abstract void onLayout(boolean changed, int l, int t, int r, int b); // 参数说明
* @param changed 当前View的大小和位置改变了
* @param left 父View的左部位置
* @param top 父View的顶部位置
* @param right 父View的右部位置
* @param bottom 父View的底部位置
  • 无论是系统提供的LinearLayout还是我们自定义的View视图,都需要继承自ViewGroup类
  • 假如需要确定该ViewGroup包含所有子View在父容器的位置,则需要重写onLayout方法(因为onLayout()在ViewGroup中被定义为抽象方法)

    所以在自定义ViewGroup时必须重写onLayout()!!!!!

根据上面说的原理描述,在ViewGroup调用layout()计算完自身的位置后,是需要ViewGroup调用onLayout()遍历子View并调用子View layout()确定自身子View的位置。

所以,重写ViewGroup的onLayout()的本质是:遍历子View并调用子View的layout()确定子View的位置。复写的套路如下:

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) { // 参数说明
* @param changed 当前View的大小和位置改变了
* @param l 即left,父View的左部位置
* @param t 即top,父View的顶部位置
* @param r 即right,父View的右部位置
* @param b 即bottom,父View的底部位置 // 循环所有子View
for (int i=0; i<getChildCount(); i++) {
View child = getChildAt(i); // 计算当前子View的四个位置值
// 计算的逻辑需要自己实现,也是自定义View的关键
... // 对计算后的位置值进行赋值
int mLeft = Left
int mTop = Top
int mRight = Right
int mBottom = Bottom // 调用子view的layout()并传递计算过的参数
// 从而计算出子View的位置
child.layout(mLeft, mTop, mRight, mBottom);
}
}
}

在复写的onLayout()会调用子View的layout()onLayout(),这两个过程类似于单一View的layout过程中的layout()onLayout(),这里不作过多描述

详细请看上面的单一View的layout过程

3.2.3 总结

对于ViewGroup的layout过程,如下:

ViewGroup的layout过程

至此,ViewGroup的layout过程已经讲解完毕。


4. 实例讲解

为了让大家更好地理解ViewGroup的layout过程(特别是复写onLayout()),接下来,我将用两个实例来加深对ViewGroup layout过程的理解。

  • 实例1:系统提供的LinearLayout(ViewGroup的子类)
  • 实例2:自定义View(继承了ViewGroup类)

4.1 实例解析1(LinearLayout)

4.1.1 原理:

  1. 计算出LinearLayout在父布局的位置
  2. 计算出LinearLayout中子View在容器中的位置。

4.1.2 具体流程

LinearLayout的Layout流程

4.1.2 源码分析

在上述流程中,对于LinearLayout的layout()的实现与上面所说是一样的,这里不作过多阐述,直接进入LinearLayout复写的onLayout()代码分析:

// 复写的逻辑和LinearLayout measure过程的`onMeasure()`类似
@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);
}
}
  • 由于垂直 / 水平方向类似,所以此处仅分析垂直方向(Vertical)的处理过程
  • 源码分析如下:(注释非常清楚)
void layoutVertical(int left, int top, int right, int bottom) {

    // 子View的数量
final int count = getVirtualChildCount(); // 遍历子View
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child == null) {
childTop += measureNullChild(i);
} else if (child.getVisibility() != GONE) { // 子View的测量宽 / 高值
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight(); // 递归调用子View的setChildFrame():对子View的位置信息进行测量计算
// 实际上是调用了子View的layout(),请看下面源码分析
setChildFrame(child, childLeft, childTop + getLocationOffset(child),
childWidth, childHeight); // childTop逐渐增大,即后面的子元素会被放置在靠下的位置
// 这符合垂直方向的LinearLayout的特性
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child); i += getChildrenSkipCount(child, i);
}
}
} /*
*setChildFrame()代码分析
**/ private void setChildFrame( View child, int left, int top, int width, int height){ // setChildFrame()仅仅只是调用了子View的layout()而已
child.layout(left, top, left ++ width, top + height); } // 在layout()又通过调用setFrame()确定View的四个顶点
// 即确定了子View的位置
// 如此不断循环确定所有子View的位置,最终确定ViewGroup的位置

在setFrame()实际上是调用了子View的layout()从而实现子View的位置计算,和上面类似,这里就不作过多描述。

4.2 实例解析2:自定义View

  • 上面讲的例子是系统提供的、已经封装好的ViewGroup - LinearLayout
  • 但是,一般来说我们使用的都是自定义View;
  • 接下来,我用一个简单的例子讲下自定义View的layout()过程

4.2.1 实例视图说明

实例的视图是一个ViewGroup(灰色视图),包含一个黄色的子View,如下图:

自定义View的视图

4.2.2 原理

  1. 计算出ViewGroup在父布局的位置
  2. 计算出ViewGroup中子View在容器中的位置。

原理流程

4.2.3 具体计算逻辑

  • 具体计算逻辑是指计算子View的位置,即计算四顶点位置 = 计算Left、Top、Right和Bottom;
  • 主要是写在复写的onLayout()
  • 计算公式如下:

Paste_Image.png
  • Left = (r - width) / 2
  • Top = (b - height) / 2

    r = Left + width + Left(因为左右间距一样)
    b = Top + height + Top(因为上下间距一样)

  • Right = width + Left;
  • Bottom = height + Top;

4.2.3 代码分析

因为其余方法同上,这里不作过多描述,所以这里只分析复写的onLayout()

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) { // 参数说明
* @param changed 当前View的大小和位置改变了
* @param l 即left,父View的左部位置
* @param t 即top,父View的顶部位置
* @param r 即right,父View的右部位置
* @param b 即bottom,父View的底部位置 // 循环所有子View
// 其实就只有一个
for (int i=0; i<getChildCount(); i++) {
View child = getChildAt(i); // 取出当前子View宽 / 高
int width = child.getMeasuredWidth();
int height = child.getMeasuredHeight(); // 计算当前子View的mLeft和mTop值
int mLeft = (r - width) / 2;
int mTop = (b - height) / 2; // 调用子view的layout()并传递计算过的参数
// 从而计算出子View的位置
child.layout(mLeft, mTop, mLeft + width, mTop + height);
}
}
}

布局文件如下:

<?xml version="1.0" encoding="utf-8"?>
<scut.carson_ho.layout_demo.Demo_ViewGroup xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#eee998"
tools:context="scut.carson_ho.layout_demo.MainActivity"> <Button
android:text="ChildView"
android:layout_width="200dip"
android:layout_height="200dip"
android:background="#333444"
android:id="@+id/ChildView" />
</scut.carson_ho.layout_demo.Demo_ViewGroup >

效果图

Paste_Image.png

好了,你是不是发现,粘了我的代码但是画不出来?!(如下图)

实际示意图

因为我还没说draw流程啊哈哈哈!

draw流程是负责将View绘制出来的。

layout()过程讲到这里讲完了,接下来我将继续将自定义View的最后一个流程draw流程,有兴趣就继续关注我啦啦!!


5. 细节问题

问:getWidth() ( getHeight())与 getMeasuredWidth() (getMeasuredHeight())获取的宽 (高)有什么区别?

答:

首先明确定义:

  • getWidth() ( getHeight()):View最终的宽 / 高
  • getMeasuredWidth() (getMeasuredHeight()):View的测量的宽 / 高:

先分别看下各自的源码:

// View的测量的宽 / 高:
public final int getMeasuredWidth() {
return mMeasuredWidth & MEASURED_SIZE_MASK;
// measure过程中返回的mMeasuredWidth
} public final int getMeasuredHeight() {
return mMeasuredHeight & MEASURED_SIZE_MASK;
// measure过程中返回的mMeasuredHeight
} // View最终的宽 / 高
public final int getWidth() {
return mRight - mLeft;
// View最终的宽 = 子View的右边界 - 子view的左边界。
} public final int getHeight() {
return mBottom - mTop;
// View最终的高 = 子View的下边界 - 子view的上边界。
}

二者的区别具体如下:

二者的区别

上面标红:一般情况下,二者获取的宽 / 高是相等的。那么,“非一般”情况是什么?

答:人为设置。

通过重写View的layout()强行设置,
@Override
public void layout( int l , int t, int r , int b){ // 改变传入的顶点位置参数
super.layout(l,t,r+100,b+100)
// 如此一来,在任何情况下,getWidth() ( getHeight())获得的宽 / 高总是比getMeasuredWidth() (getMeasuredHeight())获取的宽 (高)大100px
// View的最终宽 / 高总是比测量宽 / 高大100px
}

虽然这样的人为设置没有实际意义,但是证明了View的最终宽 / 高和测量宽 / 高大100px是可以不一样。

特别注意

网上流传这么一个原因描述:

  • 实际上在当屏幕可以包裹内容的时候,他们的值是相等的;
  • 只有当view超出屏幕后,才能看出他们的区别:getMeasuredWidth()是实际View的大小,与屏幕无关,而getHeight的大小此时则是屏幕的大小。当超出屏幕后getMeasuredWidth()等于getWidth()加上屏幕之外没有显示的大小

这个结论是错的!详细请看这个博客

结论

getWidth() ( getHeight())获得的宽 / 高与getMeasuredWidth() (getMeasuredHeight())获取的宽 (高)在非人为设置的情况下,永远是相等的。


6. 总结

  • 对于ViewGroup的layout过程
    1. ViewGroup调用layout()计算自身的位置
    2. ViewGroup调用onLayout()遍历子View并调用子View layout()确定自身子View的位置

      此步骤就是复写onLayout()的逻辑

    3. 如此不断循环确定所有子View的位置,直到全部确定即layout过程完毕
  • 对于View的layout过程
    调用layout()计算自身的位置即可。

  • 一个图总结自定义View - Layout过程,如下图:

总结

自定义View Layout过程 (3)的更多相关文章

  1. 自定义View Draw过程(4)

    目录 目录 1. 知识基础 具体请看我写的另外一篇文章:自定义View基础 - 最易懂的自定义View原理系列 2. draw过程作用 绘制View视图 3. draw过程详解 同measure.la ...

  2. 自己定义View Layout过程 - 最易懂的自己定义View原理系列(3)

    前言 自己定义View是Android开发人员必须了解的基础 网上有大量关于自己定义View原理的文章.但存在一些问题:内容不全.思路不清晰.无源代码分析.简单问题复杂化等等 今天,我将全面总结自己定 ...

  3. 自定义View Measure过程(2)

    目录 目录 1. 作用 测量View的宽/高 在某些情况下,需要多次测量(measure)才能确定View最终的宽/高: 在这种情况下measure过程后得到的宽/高可能是不准确的: 建议在layou ...

  4. 自定义view布局过程详解

    布局过程,就是程序在运行时利用布局文件的代码来计算出实际尺寸的过程. 布局分为两个阶段:测量阶段和布局阶段. 测量阶段:从上到下递归地调用每个 View 或者 ViewGroup 的 measure( ...

  5. 自定义View和ViewGroup

    为了扫除学习中的盲点,尽可能多的覆盖Android知识的边边角角,决定对自定义View做一个稍微全面一点的使用方法总结,在内容上面并没有什么独特的地方,其他大神们的博客上面基本上都有讲这方面的内容,如 ...

  6. 自定义View和ViewGroup(有这一篇就够了)

    为了扫除学习中的盲点,尽可能多的覆盖Android知识的边边角角,决定对自定义View做一个稍微全面一点的使用方法总结,在内容上面并没有什么独特的地方,其他大神们的博客上面基本上都有讲这方面的内容,如 ...

  7. 【转载】自定义View,有这一篇就够了

    为了扫除学习中的忙点,尽可能多的覆盖Android知识的边边角角,决定对自定义View做一个稍微全面一点的使用方法总结,在内容上面并没有什么独特的地方,其他大神们博客上面基本上都有讲这方面的内容,如果 ...

  8. 自定义View系列教程06--详解View的Touch事件处理

    深入探讨Android异步精髓Handler 站在源码的肩膀上全解Scroller工作机制 Android多分辨率适配框架(1)- 核心基础 Android多分辨率适配框架(2)- 原理剖析 Andr ...

  9. 自定义view(一)

    为什么标题会是自定义view(一)呢?因为自定义view其实内容很多,变化也很多,所以我会慢慢更新博客,争取多写的有关的东西,同时,如果我以后学到了新的有关于自定义view的东西,我也会及时写出来. ...

随机推荐

  1. 779. K-th Symbol in Grammar

    class Solution { public: int kthGrammar(int N, int K) { return helper(N, K, false); } int helper(int ...

  2. Git ---游离状态下的commit 分支切换与找回,commit之后无法找到历史记录

    commit之后无法找到历史记录 https://blog.csdn.net/zyb2017/article/details/78307688

  3. python语言中threading.Thread类的使用方法

    1. 编程语言里面的任务和线程是很重要的一个功能.在python里面,线程的创建有两种方式,其一使用Thread类创建 # 导入Python标准库中的Thread模块 from threading i ...

  4. CFileDialog OFN_NOCHANGEDIR

    问题:CFileDialog 调用后变成了当前工作路径,变成了CFileDialog所选择的路径. 解决:在CFileDialog的dwFlags 设置标志OFN_NOCHANGEDIR就可以了,不会 ...

  5. 大中型 UGC 平台的反垃圾(anti-spam)工作

    本文来自网易云社区 随着互联网技术的日渐发展,相继诞生了垂直社区.社交平台.短视频应用.网络直播等越来越多样的产品.但在内容爆炸式增长的同时,海量UGC中也夹杂着各种违规垃圾信息,包括垃圾广告.诈骗信 ...

  6. Android学习记录(6)—将java中的多线程下载移植到Android中(即多线程下载在Android中的使用)③

    在这一节中,我们就来讲多线程下载以及断点续传在android中怎么使用,前两节是为本节做准备的,没有看前两节的同学,最好看完前面的两篇文章再来看这篇.其实在android端的应用和java基本上是差不 ...

  7. Python 绘制棋盘

    import turtle pen = turtle.Pen() pen.speed(10) width = 30 # 格子宽度 count = 18 # 横向纵向格子数 o = width * co ...

  8. 六 APPIUM Android 定位方式

    文本转自:http://www.cnblogs.com/sundalian/p/5629500.html APPIUM Android 定位方式   1.定位元素应用元素 1.1通过id定位元素 An ...

  9. freemaker示例

    第一步  创建一个User.java文件 来两个变量 public class User {      private String userName;       private String us ...

  10. JS 如何获取radio或者checkbox选中后的值

    废话不多说,直接上代码: 代码: <!DOCTYPE html> <html> <head> <meta charset="UTF-8"& ...