(3)自定义View Layout过程 - 最易懂的自定义View原理系列
Carson_Ho
关注
2017.02.20 10:56*字数 2059阅读 9549评论 15喜欢 106赞赏 1
前言
自定义View是Android开发者必须了解的基础
网上有大量关于自定义View原理的文章,但存在一些问题:内容不全、思路不清晰、无源码分析、简单问题复杂化 等
今天,我将全面总结自定义View原理中的Layout过程,我能保证这是市面上的最全面、最清晰、最易懂的
文章较长,建议收藏等充足时间再进行阅读
阅读本文前,请先阅读文章
(1)自定义View基础 - 最易懂的自定义View原理系列
(2)自定义View Measure过程 - 最易懂的自定义View原理系列
目录
示意图
1. 作用
计算视图(View)的位置
即计算View的四个顶点位置:Left、Top、Right和Bottom
2. 知识储备
具体请看文章:(1)自定义View基础 - 最易懂的自定义View原理系列
3. layout过程详解
类似measure过程,layout过程根据View的类型分为2种情况:
示意图
接下来,我将详细分析这2种情况下的layout过程
3.1 单一View的layout过程
应用场景
在无现成的控件View满足需求、需自己实现时,则使用自定义单一View
如:制作一个支持加载网络图片的ImageView控件
注:自定义View在多数情况下都有替代方案:图片 / 组合动画,但二者可能会导致内存耗费过大,从而引起内存溢出等问题。
具体使用
继承自View、SurfaceView或 其他View;不包含子View
具体流程
示意图
下面我将一个个方法进行详细分析
源码分析
layout过程的入口 =layout(),具体如下:
/**
* 源码分析:layout()
* 作用:确定View本身的位置,即设置View本身的四个顶点位置
*/publicvoidlayout(intl,intt,intr,intb){// 当前视图的四个顶点intoldL = mLeft;intoldT = mTop;intoldB = mBottom;intoldR = mRight;// 1. 确定View的位置:setFrame() / setOpticalFrame()// 即初始化四个顶点的值、判断当前View大小和位置是否发生了变化 & 返回 // ->>分析1、分析2booleanchanged = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);// 2. 若视图的大小 & 位置发生变化// 会重新确定该View所有的子View在父容器的位置:onLayout()if(changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) { onLayout(changed, l, t, r, b);// 对于单一View的laytou过程:由于单一View是没有子View的,故onLayout()是一个空实现->>分析3// 对于ViewGroup的laytou过程:由于确定位置与具体布局有关,所以onLayout()在ViewGroup为1个抽象方法,需重写实现(后面会详细说)...}/**
* 分析1:setFrame()
* 作用:根据传入的4个位置值,设置View本身的四个顶点位置
* 即:最终确定View本身的位置
*/protectedbooleansetFrame(intleft,inttop,intright,intbottom){ ...// 通过以下赋值语句记录下了视图的位置信息,即确定View的四个顶点// 从而确定了视图的位置mLeft = left; mTop = top; mRight = right; mBottom = bottom; mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom); }/**
* 分析2:setOpticalFrame()
* 作用:根据传入的4个位置值,设置View本身的四个顶点位置
* 即:最终确定View本身的位置
*/privatebooleansetOpticalFrame(intleft,inttop,intright,intbottom){ Insets parentInsets = mParentinstanceofView ? ((View) mParent).getOpticalInsets() : Insets.NONE; Insets childInsets = getOpticalInsets();// 内部实际上是调用setFrame()returnsetFrame( left + parentInsets.left - childInsets.left, top + parentInsets.top - childInsets.top, right + parentInsets.left + childInsets.right, bottom + parentInsets.top + childInsets.bottom); }// 回到调用原处/**
* 分析3:onLayout()
* 注:对于单一View的laytou过程
* a. 由于单一View是没有子View的,故onLayout()是一个空实现
* b. 由于在layout()中已经对自身View进行了位置计算,所以单一View的layout过程在layout()后就已完成了
*/protectedvoidonLayout(booleanchanged,intleft,inttop,intright,intbottom){// 参数说明// changed 当前View的大小和位置改变了 // left 左部位置// top 顶部位置// right 右部位置// bottom 底部位置}
至此,单一View的layout过程已分析完毕。
总结
单一View的layout过程解析如下:
示意图
3.2 ViewGroup的layout过程
应用场景
利用现有的组件根据特定的布局方式来组成新的组件
具体使用
继承自ViewGroup或 各种Layout;含有子View
如:底部导航条中的条目,一般都是上图标(ImageView)、下文字(TextView),那么这两个就可以用自定义ViewGroup组合成为一个Veiw,提供两个属性分别用来设置文字和图片,使用起来会更加方便。
Paste_Image.png
原理(步骤)
计算自身ViewGroup的位置:layout()
遍历子View& 确定自身子View在ViewGroup的位置(调用子View的layout()):onLayout()
a. 步骤2 类似于 单一View的layout过程
b. 自上而下、一层层地传递下去,直到完成整个View树的layout()过程
示意图
流程
示意图
此处需注意:
ViewGroup和View同样拥有layout()和onLayout(),但二者不同的:
一开始计算ViewGroup位置时,调用的是ViewGroup的layout()和onLayout();
当开始遍历子View& 计算子View位置时,调用的是子View的layout()和onLayout()
类似于单一View的layout过程
下面我将一个个方法进行详细分析:layout过程入口为layout()
/**
* 源码分析:layout()
* 作用:确定View本身的位置,即设置View本身的四个顶点位置
* 注:与单一View的layout()源码一致
*/publicvoidlayout(intl,intt,intr,intb){// 当前视图的四个顶点intoldL = mLeft;intoldT = mTop;intoldB = mBottom;intoldR = mRight;// 1. 确定View的位置:setFrame() / setOpticalFrame()// 即初始化四个顶点的值、判断当前View大小和位置是否发生了变化 & 返回 // ->>分析1、分析2booleanchanged = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);// 2. 若视图的大小 & 位置发生变化// 会重新确定该View所有的子View在父容器的位置:onLayout()if(changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) { onLayout(changed, l, t, r, b);// 对于单一View的laytou过程:由于单一View是没有子View的,故onLayout()是一个空实现(上面已分析完毕)// 对于ViewGroup的laytou过程:由于确定位置与具体布局有关,所以onLayout()在ViewGroup为1个抽象方法,需重写实现 ->>分析3...}/**
* 分析1:setFrame()
* 作用:确定View本身的位置,即设置View本身的四个顶点位置
*/protectedbooleansetFrame(intleft,inttop,intright,intbottom){ ...// 通过以下赋值语句记录下了视图的位置信息,即确定View的四个顶点// 从而确定了视图的位置mLeft = left; mTop = top; mRight = right; mBottom = bottom; mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom); }/**
* 分析2:setOpticalFrame()
* 作用:确定View本身的位置,即设置View本身的四个顶点位置
*/privatebooleansetOpticalFrame(intleft,inttop,intright,intbottom){ Insets parentInsets = mParentinstanceofView ? ((View) mParent).getOpticalInsets() : Insets.NONE; Insets childInsets = getOpticalInsets();// 内部实际上是调用setFrame()returnsetFrame( left + parentInsets.left - childInsets.left, top + parentInsets.top - childInsets.top, right + parentInsets.left + childInsets.right, bottom + parentInsets.top + childInsets.bottom); }// 回到调用原处/**
* 分析3:onLayout()
* 作用:计算该ViewGroup包含所有的子View在父容器的位置()
* 注:
* a. 定义为抽象方法,需重写,因:子View的确定位置与具体布局有关,所以onLayout()在ViewGroup没有实现
* b. 在自定义ViewGroup时必须复写onLayout()!!!!!
* c. 复写原理:遍历子View 、计算当前子View的四个位置值 & 确定自身子View的位置(调用子View layout())
*/protectedvoidonLayout(booleanchanged,intleft,inttop,intright,intbottom){// 参数说明// changed 当前View的大小和位置改变了 // left 左部位置// top 顶部位置// right 右部位置// bottom 底部位置// 1. 遍历子View:循环所有子Viewfor(inti=0; i
总结
对于ViewGroup的layout过程,如下:
示意图
此处需注意:
ViewGroup和View同样拥有layout()和onLayout(),但二者不同的:
一开始计算ViewGroup位置时,调用的是ViewGroup的layout()和onLayout();
当开始遍历子View& 计算子View位置时,调用的是子View的layout()和onLayout()
类似于单一View的layout过程
至此,ViewGroup的layout过程已讲解完毕。
4. 实例讲解
为了更好理解ViewGroup的layout过程(特别是复写onLayout())
下面,我将用2个实例来加深对ViewGroup layout过程的理解
系统提供的ViewGroup的子类:LinearLayout
自定义View(继承了ViewGroup类)
4.1 实例解析1(LinearLayout)
4.1.1 原理
计算出LinearLayout本身在父布局的位置
计算出LinearLayout中所有子View在容器中的位置
4.1.2 具体流程
示意图
4.1.2 源码分析
在上述流程中,对于LinearLayout的layout()的实现与上面所说是一样的,此处不作过多阐述
故直接进入LinearLayout复写的onLayout()分析
/**
* 源码分析:LinearLayout复写的onLayout()
* 注:复写的逻辑 和 LinearLayout measure过程的 onMeasure()类似
*/@OverrideprotectedvoidonLayout(booleanchanged,intl,intt,intr,intb){// 根据自身方向属性,而选择不同的处理方式if(mOrientation == VERTICAL) { layoutVertical(l, t, r, b); }else{ layoutHorizontal(l, t, r, b); } }// 由于垂直 / 水平方向类似,所以此处仅分析垂直方向(Vertical)的处理过程 ->>分析1/**
* 分析1:layoutVertical(l, t, r, b)
*/voidlayoutVertical(intleft,inttop,intright,intbottom){// 子View的数量finalintcount = getVirtualChildCount();// 1. 遍历子Viewfor(inti =0; i < count; i++) {finalView child = getVirtualChildAt(i);if(child ==null) { childTop += measureNullChild(i); }elseif(child.getVisibility() != GONE) {// 2. 计算子View的测量宽 / 高值finalintchildWidth = child.getMeasuredWidth();finalintchildHeight = child.getMeasuredHeight();// 3. 确定自身子View的位置// 即:递归调用子View的setChildFrame(),实际上是调用了子View的layout() ->>分析2setChildFrame(child, childLeft, childTop + getLocationOffset(child), childWidth, childHeight);// childTop逐渐增大,即后面的子元素会被放置在靠下的位置// 这符合垂直方向的LinearLayout的特性childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child); i += getChildrenSkipCount(child, i); } } }/**
* 分析2:setChildFrame()
*/privatevoidsetChildFrame( View child,intleft,inttop,intwidth,intheight){// setChildFrame()仅仅只是调用了子View的layout()而已child.layout(left, top, left ++ width, top + height); }// 在子View的layout()又通过调用setFrame()确定View的四个顶点// 即确定了子View的位置// 如此不断循环确定所有子View的位置,最终确定ViewGroup的位置
4.2 实例解析2:自定义View
上面讲的例子是系统提供的、已经封装好的ViewGroup子类:LinearLayout
但是,一般来说我们使用的都是自定义View;
接下来,我用一个简单的例子讲下自定义View的layout()过程
4.2.1 实例视图说明
实例视图 = 1个ViewGroup(灰色视图),包含1个黄色的子View,如下图:
示意图
4.2.2 原理
计算出ViewGroup在父布局的位置
计算出ViewGroup中子View在容器中的位置
原理流程
4.2.3 具体计算逻辑
具体计算逻辑是指计算子View的位置,即计算四顶点位置 = 计算Left、Top、Right和Bottom;
主要是写在复写的onLayout()
计算公式如下:
示意图
r = Left + width + Left;// 因左右间距一样b = Top + height + Top;// 因上下间距一样Left = (r - width) /2;Top = (b - height) /2;Right = width + Left;Bottom = height + Top;
4.2.3 代码分析
因为其余方法同上,这里不作过多描述,所以这里只分析复写的onLayout()
/**
* 源码分析:LinearLayout复写的onLayout()
* 注:复写的逻辑 和 LinearLayout measure过程的 onMeasure()类似
*/@OverrideprotectedvoidonLayout(booleanchanged,intl,intt,intr,intb){// 参数说明// changed 当前View的大小和位置改变了 // left 左部位置// top 顶部位置// right 右部位置// bottom 底部位置// 1. 遍历子View:循环所有子View// 注:本例中其实只有一个for(inti=0; i
布局文件如下:
效果图
need-to-insert-img
示意图
好了,你是不是发现,粘了我的代码但是画不出来?!(如下图)
need-to-insert-img
实际示意图
因为我还没说draw流程啊哈哈哈!
draw流程:将View最终绘制出来
layout()过程讲到这里讲完了,接下来我将继续将自定义View的最后一个流程draw流程,有兴趣就继续关注我啦啦!!
5. 细节问题:getWidth() ( getHeight())与 getMeasuredWidth() (getMeasuredHeight())获取的宽 (高)有什么区别?
答:
首先明确定义:
getWidth()/getHeight():获得View最终的宽 / 高
getMeasuredWidth()/getMeasuredHeight():获得View测量的宽 / 高
先看下各自的源码:
// 获得View测量的宽 / 高publicfinalintgetMeasuredWidth(){returnmMeasuredWidth & MEASURED_SIZE_MASK;// measure过程中返回的mMeasuredWidth}publicfinalintgetMeasuredHeight(){returnmMeasuredHeight & MEASURED_SIZE_MASK;// measure过程中返回的mMeasuredHeight}// 获得View最终的宽 / 高publicfinalintgetWidth(){returnmRight - mLeft;// View最终的宽 = 子View的右边界 - 子view的左边界。}publicfinalintgetHeight(){returnmBottom - mTop;// View最终的高 = 子View的下边界 - 子view的上边界。}
二者的区别:
need-to-insert-img
示意图
上面标红:一般情况下,二者获取的宽 / 高是相等的。那么,“非一般”情况是什么?
答:人为设置:通过重写View的layout()强行设置
@Overridepublicvoidlayout(intl ,intt,intr ,intb){// 改变传入的顶点位置参数super.layout(l,t,r+100,b+100);// 如此一来,在任何情况下,getWidth() / getHeight()获得的宽/高 总比 getMeasuredWidth() / getMeasuredHeight()获取的宽/高大100px// 即:View的最终宽/高 总比 测量宽/高 大100px}
虽然这样的人为设置无实际意义,但证明了View的最终宽 / 高 与 测量宽 / 高是可以不一样
特别注意
网上流传这么一个原因描述:
实际上在当屏幕可包裹内容时,他们的值是相等的;
只有当view超出屏幕后,才能看出他们的区别:getMeasuredWidth()是实际View的大小,与屏幕无关,而getHeight的大小此时则是屏幕的大小。当超出屏幕后getMeasuredWidth()等于getWidth()加上屏幕之外没有显示的大小
这个结论是错的!详细请点击文章
结论
在非人为设置的情况下,View的最终宽/高(getWidth()/getHeight())
与View的测量宽/高 (getMeasuredWidth()/getMeasuredHeight())永远是相等
6. 总结
本文主要讲解了自定义View中的Layout过程,总结如下:
need-to-insert-img
示意图
need-to-insert-img
示意图
如果希望继续了解自定义View的原理,请参考我写的文章:
(1)自定义View基础 - 最易懂的自定义View原理系列
(2)自定义View Measure过程 - 最易懂的自定义View原理系列
(3)自定义View Layout过程 - 最易懂的自定义View原理系列
(4)自定义View Draw过程- 最易懂的自定义View原理系列
接下来,我我将继续对自定义View的应用进行分析,有兴趣的可以继续关注Carson_Ho的安卓开发笔记
请点赞!因为你们的赞同/鼓励是我写作的最大动力!
相关文章阅读
Android开发:最全面、最易懂的Android屏幕适配解决方案
Android开发:史上最全的Android消息推送解决方案
Android开发:最全面、最易懂的Webview详解
Android开发:JSON简介及最全面解析方法!
Android四大组件:Service服务史上最全面解析
Android四大组件:BroadcastReceiver史上最全面解析
欢迎关注Carson_Ho的!
不定期分享关于安卓开发的干货,追求短、平、快,但却不缺深度。
need-to-insert-img
上一篇
目录
下一篇
小礼物走一走,来关注我
赞赏支持
自定义View、动画
© 著作权归作者所有
举报文章
自定义View、动画
4.4万字 ·34.2万阅读·386人关注
关注
喜欢
106
下载app生成长微博图片
更多分享
被以下专题收入,发现更多相似内容
收入我的专题
安卓资源收集
Android...
Android开发
Android知识
程序员
Android开发
Android...
展开更多
自定义View Measure过程 - 最易懂的自定义View原理系列(2)
前言 自定义View是Android开发者必须了解的基础 网上有大量关于自定义View原理的文章,但存在一些问题:内容不全、思路不清晰、无源码分析、简单问题复杂化 等 今天,我将全面总结自定义View原理中的measure过程,我能保证这是市面上的最全面、最清晰、最易懂的 ...
Carson_Ho
Android View的测量、布局、绘制流程源码分析及自定义View实例演示
在Android知识体系中,Android系统提供了一个GUI库,里面有很多原生控件,但是很多时候我们并不满足于系统提供的原生控件,那么怎么才能做出所需要的控件?那就要自定义View,在实际开发中我们仅仅了解常用原生控件的使用方法是无法作出需要的复杂的自定义View的。因此...
ForeverCy
Android应用程序窗口(Activity)的测量(Measure)、布局(Layout)和...
在前面一篇文章中,我们分析了Android应用程序窗口的绘图表面的创建过程。Android应用程序窗口的绘图表面在创建完成之后,我们就可以从上到下地绘制它里面的各个视图了,即各个UI元素了。不过在绘制这些UI元素之前,我们还需要从上到下地测量它们实际所需要的大小,以及对它们...
army魔君
自定义view - 收藏集 - 掘金
Android 从 0 开始自定义控件之 View 的 draw 过程 (九) - Android - 掘金转载请标明出处: http://blog.csdn.net/airsaid/article/details/53872349 本文出自:周游的博客 ... Andri...
掘金官方
掘金 Android 文章精选合集
用两张图告诉你,为什么你的 App 会卡顿? - Android - 掘金Cover 有什么料? 从这篇文章中你能获得这些料: 知道setContentView()之后发生了什么? ... Android 获取 View 宽高的常用正确方式,避免为零 - 掘金相信有很多朋友...
掘金官方
送儿远行
正月初五,许多人还沉浸在春节的欢乐中,可是,我和丈夫却不得不送儿远行了。 火车是中午11点15分发车,正常情况提前两小时就没问题,只因国庆节的时候高速路堵车,儿子差点没赶上火车。一朝被蛇咬,十年怕井绳,所以儿子决定提前三小时出发。没想到这次是一路畅通,...
真水无香xsq
端端要说大人话
端端要说大人话 端端觉得自己长大了,要说大人话。玩小伴龙游戏学了不少新词,其中有不少成语。她就在什么地方都用。 那天,起床穿衣服。奶奶给她穿了这件,她要穿那件。刚穿好这件,被她脱去,闹着要穿那件。奶奶没好气的说:“这个娃娃太赖皮了,费事的很。今天要给老太太过乜贴,大人忙着呢...
雄风05yz
妈妈日记-----泪为何?
妈妈日记----泪为何? 时间:2017年4月10日 一,意想不到的故事 1.意想不到, 儿子自从去了初中,就对我冷漠多了了,话也不多了,见面机会很少,沟通的机会也少多了,这让我有了不少纠结,不知道该如何做? 昨天,陪儿子吃饭,逛商场,看到儿子在身边快乐的样子,跟我说心里话...
天堂島書吧
如果程序员找不到对象,一般有三种情况!
抛开那些名花名草有主的人,剩下的都是单身汪了,眼瞅着马上就是老大不小了,看着你们这般捉急达妹表示也是可以理解的! 今天,来分析分析吧~这结果也是没sei了! 1、 程序员找不到对象,一般有三种情况:C#、JAVA都有对象,但是经常找不到对象。ASM C直接没有对象。java...
科技男
如何找到自己摆脱困惑和焦虑
转眼半年过去了,几次的聊天当中,得知老于已经是有十多年工作经验,先后在多个知名的零售,快时尚外资品牌做过副总,店总经理职务。 所带团队都在几十人以上,月度的销售也都在7位数。然而在奔四的年龄上,老于颇感焦虑,现有职位很难有提升的空间,换工作也都是在同行圈内,发自内心深处的些...
晨曦然
{"user_signed_in":true,"locale":"zh-CN","os":"windows","read_mode":"day","read_font":"font2","current_user":{"id":6846566,"nickname":"锋_9eee","slug":"43947f220457","avatar":"http://upload.jianshu.io/users/upload_avatars/6846566/faa7de43-17f6-4424-90bf-beb97d803c01","unread_counts":{"chats":0,"others":1,"total":1},"has_editable_collection":false},"note_show":{"is_author":false,"is_following_author":false,"is_liked_note":false,"follow_state":0,"uuid":"21b95c5c-e5ef-46ae-8c32-cd8ce19fc1fe"},"note":{"id":9209333,"slug":"158736a2549d","user_id":944365,"notebook_id":9976005,"commentable":true,"likes_count":106,"views_count":9549,"public_wordage":2059,"comments_count":15,"featured_comments_count":2,"total_rewards_count":1,"is_author":false,"paid_type":"fbook_free","paid":false,"paid_content_accessible":false,"author":{"nickname":"Carson_Ho","total_wordage":301568,"followers_count":26559,"total_likes_count":19024}}} (function(){
var bp = document.createElement('script');
var curProtocol = window.location.protocol.split(':')[0];
if (curProtocol === 'https') {
bp.src = 'https://zz.bdstatic.com/linksubmit/push.js';
}
else {
bp.src = 'http://push.zhanzhang.baidu.com/push.js';
}
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(bp, s);
})();