自己定义View Layout过程 - 最易懂的自己定义View原理系列(3)
前言
- 自己定义View是Android开发人员必须了解的基础
- 网上有大量关于自己定义View原理的文章。但存在一些问题:内容不全、思路不清晰、无源代码分析、简单问题复杂化等等
- 今天,我将全面总结自己定义View原理中的layout过程,我能保证这是市面上的最全面、最清晰、最易懂的
- 文章较长。建议收藏等充足时间再进行阅读
文件夹
imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="文件夹" title="">
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本身的四个顶点位置
- 源代码分析例如以下:(仅贴出关键代码)
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过程解析例如以下:
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()
- 源代码分析例如以下:(仅贴出关键代码)
// 与单一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在父容器的位置()
- 定义为抽象方法。须要重写
- 原因:由于子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过程,例如以下:
imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="ViewGroup的layout过程" title="">
至此。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()
代码分析:
// 复写的逻辑和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,例如以下图:
imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="自己定义View的视图" title="">
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()`
@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 >
效果图
好了,你是不是发现。粘了我的代码可是画不出来?。(例如以下图)
由于我还没说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过程
- ViewGroup调用
layout()
计算自身的位置 - ViewGroup调用
onLayout()
遍历子View并调用子Viewlayout()
确定自身子View的位置
此步骤就是复写onLayout()的逻辑
- ViewGroup调用
- 如此不断循环确定全部子View的位置,直到全部确定即layout过程完成
对于View的layout过程
调用layout()
计算自身的位置就可以。一个图总结自己定义View - Layout过程,例如以下图:
imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="总结" title="">
- 接下来能够開始看自己定义View的原理了:
自己定义View基础 - 最易懂的自己定义View原理系列(1)
自己定义View Measure过程 - 最易懂的自己定义View原理系列(2)
自己定义View Layout过程 - 最易懂的自己定义View原理系列(3)
自己定义View Draw过程- 最易懂的自己定义View原理系列(4) 接下来我将继续对自己定义View的应用进行解说。有兴趣的能够继续关注Carson_Ho的安卓开发笔记
简书id:Carson_Ho
imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="简书ID" title="">
请帮顶或评论点赞。由于你们的赞同/鼓舞是我写作的最大动力!
自己定义View Layout过程 - 最易懂的自己定义View原理系列(3)的更多相关文章
- 自定义View Layout过程 (3)
目录 目录 1. 知识基础 具体请看我写的另外一篇文章:(1)自定义View基础 - 最易懂的自定义View原理系列 2. 作用 计算View视图的位置. 即计算View的四个顶点位置:Left.To ...
- 自定义View Draw过程(4)
目录 目录 1. 知识基础 具体请看我写的另外一篇文章:自定义View基础 - 最易懂的自定义View原理系列 2. draw过程作用 绘制View视图 3. draw过程详解 同measure.la ...
- View绘制详解(四),谝一谝layout过程
上篇博客我们介绍了View的测量过程,这只是View显示过程的第一步,第二步就是layout了,这个我们一般译作布局,其实就是在View测量完成之后根据View的大小,将其一个一个摆放在ViewGro ...
- [Android学习笔记]view的layout过程学习
View从创建到显示到屏幕需要经历几个过程: measure -> layout -> draw measure过程:计算view所占屏幕大小layout过程:设置view在屏幕的位置dr ...
- 【Android - 自定义View】之View的layout过程解析
layout(布局)的作用是ViewGroup用来确定子元素的位置,在这个过程中会用到两个核心方法: layout() 和 onLayout() .layout()方法用来确定View本身的位置,on ...
- 自定义View Measure过程(2)
目录 目录 1. 作用 测量View的宽/高 在某些情况下,需要多次测量(measure)才能确定View最终的宽/高: 在这种情况下measure过程后得到的宽/高可能是不准确的: 建议在layou ...
- android绘制view的过程
1 android绘制view的过程简单描述 简单描述可以解释为:计算大小(measure),布局坐标计算(layout),绘制到屏幕(draw): 下面看看每一步的动作到底是 ...
- Android View绘制过程
Android的View绘制是从根节点(Activity是DecorView)开始,他是一个自上而下的过程.View的绘制经历三个过程:Measure.Layout.Draw.基本流程如下图: per ...
- 【转】Android绘制View的过程研究——计算View的大小
Android绘制View的过程研究——计算View的大小 转自:http://liujianqiao398.blog.163.com/blog/static/18182725720121023218 ...
随机推荐
- [置顶] 从零开始学C++之STL(二):实现简单容器模板类Vec(vector capacity 增长问题、allocator 内存分配器)
首先,vector 在VC 2008 中的实现比较复杂,虽然vector 的声明跟VC6.0 是一致的,如下: C++ Code 1 2 template < class _Ty, ...
- 什么是.Net, IL, CLI, BCL, FCL, CTS, CLS, CLR, JIT
什么是.NET? 起源:比尔盖茨在2000年的Professional Developers Conference介绍了一个崭新的平台叫作Next Generation Windows Service ...
- UIProgressView 详解
自定义progressView 包括背景图片和进度条的图片以及进度条的高度. //进度条 UIProgressView *aProgressView = [[UIProgressView allo ...
- Map HashMap 排序 迭代循环 修改值
HashMap dgzhMap = Dict.getDict("dgzh"); Iterator it_d = dgzhMap.entrySet().iterator(); whi ...
- Android Studio 出现 Gradle's dependency cache may be corrupt 错误分析
http://blog.csdn.net/u014231734/article/details/41913775 情况说明: 之前下载了 Android Studio 1.0rc2候选版,那时候把 S ...
- Android 4.4 Kitkat Phone工作流程浅析(八)__Phone状态分析
本文来自http://blog.csdn.net/yihongyuelan 转载请务必注明出处 本文代码以MTK平台Android 4.4为分析对象.与Google原生AOSP有些许差异.请读者知悉. ...
- 一个exception
今天调错,发生了一个错误:java.lang.IllegalStateException: ApplicationEventMulticaster not initialized [closed] 后 ...
- 血族第四季/全集The Strain迅雷下载
当第四季开始时,故事时间已经过去九个月.世界陷入黑暗,斯特里高伊吸血鬼控制了一切.第三季结尾的爆炸引发了一场全球核灾难,核冬天的到来令地表变得暗无天日,斯特里高伊获得解放.它们大白天也能出来活动,帮助 ...
- 【凯子哥带你学Framework】Activity界面显示全解析(下)
咱们接着上篇继续讲,上篇没看的请戳:[凯子哥带你学Framework]Activity界面显示全解析(上) 如何验证上一个问题 首先,说明一下运行条件: //主题 name="AppThem ...
- 通用的Bitmap压缩算法,进一步节约内存(推荐)
前几天我写了一篇通过压缩Bitmap,减少OOM的文章,那篇文章的目的是按照imageview的大小来压缩bitmap,让bitmap的大小正好是imageview.但是那种算法的通用性比较差,仅仅能 ...