前言
- 自定义View是Android开发者必须了解的基础
- 网上有大量关于自定义View原理的文章,但存在一些问题:内容不全、思路不清晰、无源码分析、简单问题复杂化等等
- 今天,我将全面总结自定义View原理中的layout过程,我能保证这是市面上的最全面、最清晰、最易懂的
- 文章较长,建议收藏等充足时间再进行阅读
目录
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。
- 如:制作一个支持加载网络图片的ImageView
- 特别注意:自定义View在大多数情况下都有替代方案,利用图片或者组合动画来实现,但是使用后者可能会面临内存耗费过大,制作麻烦更诸多问题。
单一View的layout过程如下图所示:
下面我将一个个方法进行详细分析。
3.1.1 layout()
- 作用:确定View本身的位置。
即设置View本身的四个顶点位置
- 源码分析如下:(仅贴出关键代码)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
3.2 onLayout()
- 作用:空实现。
对于单一View来说,由于在layout()
中已经对自身View进行了位置计算,所以单一View的layout()已经完成了。
- 源码分析:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
至此,单一View的layout过程已经分析完毕。
3.1.3 总结
单一View的layout过程解析如下:
3.2 ViewGroup的layout过程
-
应用场景
自定义ViewGroup一般是利用现有的组件根据特定的布局方式来组成新的组件,大多继承自ViewGroup或各种Layout(含有子View)。如:底部导航条中的条目,一般都是上图标(ImageView)、下文字(TextView),那么这两个就可以用自定义ViewGroup组合成为一个Veiw,提供两个属性分别用来设置文字和图片,使用起来会更加方便。
-
原理(步骤)
步骤1: ViewGroup调用layout()计算自身的位置;
步骤2: ViewGroup调用onLayout()遍历子View并调用子View layout()确定自身子View的位置。
步骤2类似于单一View的layout过程
这样自上而下、一层层地传递下去,直到完成整个View树的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()
- 源码分析如下:(仅贴出关键代码)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
3.2.2 onLayout()
-
作用:计算该ViewGroup包含所有的子View在父容器的位置()
- 定义为抽象方法,需要重写
- 原因:由于子View的确定位置与具体布局有关,所以
onLayout()
在ViewGroup没有实现。
-
源码分析:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 无论是系统提供的LinearLayout还是我们自定义的View视图,都需要继承自ViewGroup类
- 假如需要确定该ViewGroup包含所有子View在父容器的位置,则需要重写onLayout方法(因为onLayout()在ViewGroup中被定义为抽象方法)
所以在自定义ViewGroup时必须重写onLayout()!!!!!
根据上面说的原理描述,在ViewGroup调用layout()计算完自身的位置后,是需要ViewGroup调用onLayout()遍历子View并调用子View layout()确定自身子View的位置。
所以,重写ViewGroup的onLayout()的本质是:遍历子View并调用子View的layout()确定子View的位置。复写的套路如下:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
详细请看上面的单一View的layout过程
3.2.3 总结
对于ViewGroup的layout过程,如下:
至此,ViewGroup的layout过程已经讲解完毕。
4. 实例讲解
为了让大家更好地理解ViewGroup的layout过程(特别是复写onLayout()),接下来,我将用两个实例来加深对ViewGroup layout过程的理解。- 实例1:系统提供的LinearLayout(ViewGroup的子类)
- 实例2:自定义View(继承了ViewGroup类)
4.1 实例解析1(LinearLayout)
4.1.1 原理:
- 计算出LinearLayout在父布局的位置
- 计算出LinearLayout中子View在容器中的位置。
4.1.2 具体流程
4.1.2 源码分析
在上述流程中,对于LinearLayout的layout()
的实现与上面所说是一样的,这里不作过多阐述,直接进入LinearLayout复写的onLayout()
代码分析:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 由于垂直 / 水平方向类似,所以此处仅分析垂直方向(Vertical)的处理过程
- 源码分析如下:(注释非常清楚)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
在setFrame()实际上是调用了子View的layout()从而实现子View的位置计算,和上面类似,这里就不作过多描述。
4.2 实例解析2:自定义View
- 上面讲的例子是系统提供的、已经封装好的ViewGroup - LinearLayout
- 但是,一般来说我们使用的都是自定义View;
- 接下来,我用一个简单的例子讲下自定义View的layout()过程
4.2.1 实例视图说明
实例的视图是一个ViewGroup(灰色视图),包含一个黄色的子View,如下图:
4.2.2 原理
- 计算出ViewGroup在父布局的位置
- 计算出ViewGroup中子View在容器中的位置。
4.2.3 具体计算逻辑
- 具体计算逻辑是指计算子View的位置,即计算四顶点位置 = 计算Left、Top、Right和Bottom;
- 主要是写在复写的onLayout()
- 计算公式如下:
- 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()`- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
效果图
好了,你是不是发现,粘了我的代码但是画不出来?!(如下图)
因为我还没说draw流程啊哈哈哈!
draw流程是负责将View绘制出来的。
layout()过程讲到这里讲完了,接下来我将继续将自定义View的最后一个流程draw流程,有兴趣就继续关注我啦啦!!
5. 细节问题
问:getWidth() ( getHeight())与 getMeasuredWidth() (getMeasuredHeight())获取的宽 (高)有什么区别?
答:
首先明确定义:- getWidth() ( getHeight()):View最终的宽 / 高
- getMeasuredWidth() (getMeasuredHeight()):View的测量的宽 / 高:
先分别看下各自的源码:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
二者的区别具体如下:
上面标红:一般情况下,二者获取的宽 / 高是相等的。那么,“非一般”情况是什么?
答:人为设置。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
虽然这样的人为设置没有实际意义,但是证明了View的最终宽 / 高和测量宽 / 高大100px是可以不一样。
特别注意
网上流传这么一个原因描述:
- 实际上在当屏幕可以包裹内容的时候,他们的值是相等的;
- 只有当view超出屏幕后,才能看出他们的区别:getMeasuredWidth()是实际View的大小,与屏幕无关,而getHeight的大小此时则是屏幕的大小。当超出屏幕后getMeasuredWidth()等于getWidth()加上屏幕之外没有显示的大小
这个结论是错的!详细请这个博客
结论
getWidth() ( getHeight())获得的宽 / 高与getMeasuredWidth() (getMeasuredHeight())获取的宽 (高)在非人为设置的情况下,永远是相等的。
6. 总结
-
对于ViewGroup的layout过程
- ViewGroup调用
layout()
计算自身的位置 - ViewGroup调用
onLayout()
遍历子View并调用子Viewlayout()
确定自身子View的位置
此步骤就是复写onLayout()的逻辑
- ViewGroup调用
- 如此不断循环确定所有子View的位置,直到全部确定即layout过程完毕
-
对于View的layout过程
调用layout()
计算自身的位置即可。 -
一个图总结自定义View - Layout过程,如下图:
- 接下来可以开始看自定义View的原理了:
自定义View基础 - 最易懂的自定义View原理系列(1)
自定义View Measure过程 - 最易懂的自定义View原理系列(2)
自定义View Layout过程 - 最易懂的自定义View原理系列(3)
自定义View Draw过程- 最易懂的自定义View原理系列(4) -
接下来我将继续对自定义View的应用进行讲解,有兴趣的可以继续关注Carson_Ho的安卓开发笔记
-
简书id:Carson_Ho