View绘制的三大流程,指的是measure(测量)、layout(布局)、draw(绘制)

measure负责确定View的测量宽/高,也就是该View需要占用屏幕的大小,确定完View需要占用的屏幕大小后,就会通过layout确定View的最终宽/高和四个顶点在手机界面上的位置,等通过measure和layout过程确定了View的宽高和要显示的位置后,就会执行draw绘制View的内容到手机屏幕上。
在详细介绍这三大流程之前,需要简单了解一下ViewRootImpl,View绘制的三大步骤都是通过ViewRootImpl实现的,ViewRootImpl是连接WindowManager窗口管理和DecorView顶层视图的纽带。View的绘制流程从ViewRootImpl的performTraversals方法开始,顺序执行measure、layout、draw这三个流程,最终完成对View的绘制工作,在performTraversals方法中,会调用measure、layout、draw这三个方法,这三个方法内部也会调用其对应的onMeasure、onLayout、onDraw方法,通常我们在自定义View时,也就是重写的这三个方法来实现View的具体绘制逻辑
下面详细了解下各个步骤经历的主要方法(这里贴的源码版本为API 23)
一、measure
在performTraversals方法中,第一个需要进行的就是measure过程,获取到必要信息后,performTraversals方法中首先会调用measureHierarchy方法,接着measureHierarchy方法里再去调用performMeasure方法,在performMeasure方法中最终就会去调用View的measure方法,从而开始进行测量过程
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
try {
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
mView其实指的就是DecorView顶层视图,从源码可以看出,measure的递归过程就是从DecorView开始的
View和ViewGroup的测量方法有一定区别,View通过measure方法就可以完成自身的测量过程,而ViewGroup不仅需要调用measure方法测量自己,还需要去遍历其子元素的measure方法,其子元素如果是ViewGroup,则该子元素需使用同样的方法再次递归下去。
View
来看看View是如何测量自己的宽高的
先在View源码中找到measure方法
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
// ......
if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
widthMeasureSpec != mOldWidthMeasureSpec ||
heightMeasureSpec != mOldHeightMeasureSpec) {
// ......
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// measure ourselves, this should set the measured dimension flag back
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
// .....
}
View的measure过程就是通过measure方法来完成,View中的measure方法是由ViewGroup的measureChild方法调用的,ViewGroup在调用该子View的measure方法的同时还传入了子View的widthMeasureSpec和heightMeasureSpec值。该方法被定义为final类型,也就是说其measure过程是固定的,在measure中调用了onMeasure方法,如果想要自定义测量过程的话,需要重写onMeasure方法。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
Google在介绍该方法的时候也说了
Measure the view and its content to determine the measured width and the measured height. This method is invoked by {@link #measure(int, int)} and should be overridden by subclasses to provide accurate and efficient measurement of their contents.
该方法需要被子类覆盖,让子类提供精准、有效的测量数据,所以我们一般在进行自定义View开发时,需要自定义测量过程就需要复写此方法。
setMeasuredDimension方法的作用就是设置View的测量宽高,其实我们在使用getMeasuredWidth/getMeasuredHeight 方法获取的宽高值就是此处设置的值。
如果不复写此onMeasure方法,则默认使用getDefaultSize方法得到的值。
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
可以发现,传入的measureSpec数值被MeasureSpec解析成了对应的数据,这里简单介绍下MeasureSpec,它的作用就是告诉View应该以哪一种模式测量这个View,SpecMode有三种模式:
• UNSPECIFIED:表示父容器不对View有任何限制,这种模式主要用于系统内部多次Measure的情况,不需要过多关注
• AT_MOST:父容器已经指定了大小,View的大小不能大于这个值,相当于布局中使用的wrap_content模式
• EXACTLY:表示View已经定义了精确的大小,使用这个指定的精确大小specSize作为该View的大小,相当于布局中我们指定了66dp这种精确数值或者match_parent模式
传入的measureSpec值经过MeasureSpec.getMode方法获取它的测量模式,MeasureSpec.getSize方法获取对应模式下的规格大小,从而确定了其最终的测量大小。
ViewGroup
ViewGroup是一个继承至View的抽象类,ViewGroup没有实现测量自己的具体过程,因为其过程是需要各个子类根据自己的需要再具体实现,比如LinearLayout、RelativeLayout等布局的特性都是不同的,不能统一的去管理,所以就交给其子类自己去实现
ViewGroup在measure时,除了实现自身的测量,还需要对它的每个子元素进行measure,在ViewGroup内部提供了一个measureChildren的方法
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
其中,mChilderenCount指的是该ViewGroup所拥有的子元素的个数,通过一个for循环调用measureChild方法来测量其所有子元素
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
该方法先通过child.getLayoutParams方法取得子元素的LayoutParams,然后调用getChildMeasureSpec方法计算出该子元素正确的MeasureSpec,再使用child.measure方法把这个MeasureSpec传递给View进行测量。
通过这一系列过程,就能让各个子元素依次进入measure了
二、layout
通过之前的measure过程,View已经测量出了自己需要的宽高大小,performTraversals方法接下来就会执行layout过程
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
layout的过程主要是用来确定View的四个顶点所在屏幕上的位置
layout过程首先从View中的layout方法开始
public void layout(int l, int t, int r, int b) {
if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLayoutChangeListeners != null) {
ArrayList<OnLayoutChangeListener> listenersCopy =
(ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
int numListeners = listenersCopy.size();
for (int i = 0; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
}
}
}
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}
layout(int l, int t, int r, int b)方法里的四个参数分别指的是左、上、右、下的位置,这四个值是通过ViewRootImpl类里的performTraversals方法传入的
layout方法用来确定View自身的位置,mLeft、mTop、mBottom、mRight的值最终会由setOpticalFrame和setFrame方法确定,其实setOpticalFrame内部最后也是通过调用setFrame方法设置的
private boolean setOpticalFrame(int left, int top, int right, int bottom) {
Insets parentInsets = mParent instanceof View ?
((View) mParent).getOpticalInsets() : Insets.NONE;
Insets childInsets = getOpticalInsets();
return setFrame(
left + parentInsets.left - childInsets.left,
top + parentInsets.top - childInsets.top,
right + parentInsets.left + childInsets.right,
bottom + parentInsets.top + childInsets.bottom);
}
确定完View的四个顶点位置后,就相当于View在父容器中的位置被确定了,接下来会调用onLayout方法,这个方法是没有具体实现的
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}
和ViewGroup的onMeasure类似,onLayout方法的具体实现也是需要根据各个View或ViewGroup的特性来决定的,所以源码中是个空方法,有兴趣的可以去看看LinearLayout、RelativeLayout等实现了onLayout方法的ViewGroup子类
之前的measure过程,得到的是测量宽高,而通过onLayout方法,进一步确定了View的最终宽高,一般情况下,measure过程的测量宽高和layout过程确定的最终宽高是一样的
三、draw
经过以上步骤,View已经确定好了大小和屏幕中显示的位置,接着就可以绘制自身需要显示的内容了
在performTraversals方法中,会调用performDraw方法,performDraw方法中调用draw方法,draw方法中接着调用drawSoftware方法
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
boolean scalingRequired, Rect dirty) {
// Draw with software renderer.
final Canvas canvas;
try {
......
canvas = mSurface.lockCanvas(dirty);
}
try {
......
try {
mView.draw(canvas);
}
}
}
首先会通过lockCanvas方法取得一个Canvas画布对象,接着由mView(DecorView)顶层视图去调用View的draw方法,并传入一个Canvas画布对象
其实Google的工程师已经把draw的绘制过程注释的非常详细了
Draw traversal performs several drawing steps which must be executed
in the appropriate order:
1. Draw the background
2. If necessary, save the canvas' layers to prepare for fading
3. Draw view's content
4. Draw children
5. If necessary, draw the fading edges and restore layers
6. Draw decorations (scrollbars for instance)
1. 绘制View的背景
如果该View设置了背景,则绘制背景。此背景指的是我们在布局文件中通过android:background属性,或代码中使用setBackgroundResource、setBackgroundColor等方法设置的背景图片或背景颜色
if (!dirtyOpaque) {
drawBackground(canvas);
}
dirtyOpaque属性用来判断该View是否是透明的,如果是透明的则不执行某些步骤,比如绘制背景,绘制内容等
2. 如果有必要的话,保存这个canvas画布,为该层边缘的fading效果作准备
第2步和第5步是配套的,我们一般不用管2和5,源码中的注释也说了,其中的2和5方法在通常情况下是直接跳过的(skip step 2 & 5 if possible (common case)),其主要作用是实现一些如同View滑动到边缘时产生的阴影效果,可以不用过多关注
3. 绘制View的内容
该步骤调用了onDraw方法,这个方法是一个空实现
/**
* Implement this to do your drawing.
*
* @param canvas the canvas on which the background will be drawn
*/
protected void onDraw(Canvas canvas) {
}
每个子View需要展示的内容肯定是不相同的,所以onDraw的详细过程需要子类自己去实现
4. 绘制子View
和第3步一样,此方法也是一个空实现
/**
* Called by draw to draw the child views. This may be overridden
* by derived classes to gain control just before its children are drawn
* (but after its own view has been drawn).
* @param canvas the canvas on which to draw the view
*/
protected void dispatchDraw(Canvas canvas) {
}
对于单纯的View来说,它是没有子View的,所以不需要实现该方法,该方法主要是被ViewGroup重写了,找到ViewGroup中重写的dispatchDraw方法
@Override
protected void dispatchDraw(Canvas canvas) {
......
for (int i = 0; i < childrenCount; i++) {
while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
final View transientChild = mTransientViews.get(transientIndex);
if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
transientChild.getAnimation() != null) {
more |= drawChild(canvas, transientChild, drawingTime);
}
transientIndex++;
if (transientIndex >= transientCount) {
transientIndex = -1;
}
}
int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i;
final View child = (preorderedList == null)
? children[childIndex] : preorderedList.get(childIndex);
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
more |= drawChild(canvas, child, drawingTime);
}
}
......
}
在ViewGroup的dispatchDraw方法中通过for循环调用drawChild方法
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
drawChild方法里调用子视图的draw方法,从而让其子视图进入draw过程
5. 绘制View边缘的渐变褪色效果,类似于阴影效果
当第2个步骤保存了canvas画布后,就可以为这个画布实现阴影效果
6. 绘制View的装饰物
View的装饰物,指的是View除了背景、内容、子View的其它部分,比如滚动条这些
四、常见问题
1.在Activity中获取View的宽高,得到的值为0
通过上面的measure分析可以知道,View的measure过程和Activity的生命周期方法不是同步的,所以无法保证Activity的某个生命周期执行后View就一定能获取到值,当我们在View还没有完成measure过程就去获取它的宽高,当然获取不到了,解决这问题的方法有很多,这里推荐使用以下方法
(1)在View的post方法中获取:
这个方法简单快捷,推荐使用
mView.post(new Runnable() {
@Override
public void run() {
width = mView.getMeasuredWidth();
height = mView.getMeasuredHeight();
}
});
post方法中传入的Runnable对象将会在View的measure、layout过程后触发,因为UI的事件队列是按顺序执行的,所以任何post到队列中的请求都会在Layout发生变化后执行。
(2)使用View的观察者ViewTreeObserver
ViewTreeObserver是视图树的观察者,其中OnGlobalLayoutListener监听的是一个视图树中布局发生改变或某个视图的可视状态发生改变时,就会触发此类监听事件,其中onGlobalLayout回调方法会在View完成layout过程后调用,此时是获取View宽高的好时机
ViewTreeObserver observer = mView.getViewTreeObserver();
observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
mView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
width = mScanIv.getMeasuredWidth();
height = mScanIv.getMeasuredHeight();
}
});
使用这个方法需要注意,随着View树的状态改变,onGlobalLayout方法会被回调多次,所以在进入onGlobalLayout回调方法时,就移除这个观察者,保证onGlobalLayout方法只被执行一次就好了
(3)在onWindowFocusChanged回调中获取
此方法是在View已经初始化完成,measure和layout过程已经执行完成,UI视图已经渲染完成时被回调,此时View的宽高肯定也已经被确定了,这个时候就可以去获取View的宽高了
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus) {
width = mView.getMeasuredWidth();
height = mView.getMeasuredHeight();
}
}
这个方法在Activity界面发生变化时也会被多次回调,如果只需要获取一次宽高的话,建议加上标记加以限制
除了以上方法,还有其它的方法也能获取到宽高,比如在onClick方法中获取,手动调用measure方法,使用postDelayed等,了解了View绘制原理后,这些都是很容易就能理解的。
以上这篇浅谈Android View绘制三大流程探索及常见问题就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持。
# android
# view绘制流程
# Android View 绘制流程(Draw)全面解析
# Android视图的绘制流程(上) View的测量
# Android应用开发中View绘制的一些优化点解析
# Android使用自定义View绘制渐隐渐现动画
# Android View如何绘制
# Android自定义View实现绘制虚线的方法详解
# 深入理解Android中View绘制的三大流程
# 子类
# 就会
# 回调
# 指的是
# 自己的
# 自定义
# 递归
# 这三个
# 三大
# 会在
# 重写
# 确定了
# 的是
# 都是
# 是一个
# 也会
# 就可以
# 不需要
# 说了
# 推荐使用
相关文章:
如何在Tomcat中配置并部署网站项目?
如何在Windows 2008云服务器安全搭建网站?
深圳防火门网站制作公司,深圳中天明防火门怎么编码?
湖北网站制作公司有哪些,湖北清能集团官网?
如何高效配置香港服务器实现快速建站?
制作宣传网站的软件,小红书可以宣传网站吗?
专业网站设计制作公司,如何制作一个企业网站,建设网站的基本步骤有哪些?
魔方云NAT建站如何实现端口转发?
天河区网站制作公司,广州天河区如何办理身份证?需要什么资料有预约的网站吗?
韩国服务器如何优化跨境访问实现高效连接?
学校免费自助建站系统:智能生成+拖拽设计+多端适配
如何通过VPS建站无需域名直接访问?
大型企业网站制作流程,做网站需要注册公司吗?
定制建站价位费用解析与套餐推荐全攻略
php能控制zigbee模块吗_php通过串口与cc2530 zigbee通信【介绍】
ui设计制作网站有哪些,手机UI设计网址吗?
建站之星伪静态规则如何正确配置?
免费网站制作appp,免费制作app哪个平台好?
Dapper的Execute方法的返回值是什么意思 Dapper Execute返回值详解
导航网站建站方案与优化指南:一站式高效搭建技巧解析
深圳网站制作费用多少钱,读秀,深圳文献港这样的网站很多只提供网上试读,但有些人只要提供试读的文章就能全篇下载,这个是怎么弄的?
建站之星免费版是否永久可用?
厦门模型网站设计制作公司,厦门航空飞机模型掉色怎么办?
存储型VPS适合搭建中小型网站吗?
建站10G流量真的够用吗?如何应对访问高峰?
教育培训网站制作流程,请问edu教育网站的域名怎么申请?
建站主机选择指南:服务器配置与SEO优化实战技巧
常州自助建站工具推荐:低成本搭建与模板选择技巧
制作网站的过程怎么写,用凡科建站如何制作自己的网站?
如何通过西部建站助手安装IIS服务器?
代购小票制作网站有哪些,购物小票的简要说明?
沈阳制作网站公司排名,沈阳装饰协会官方网站?
陕西网站制作公司有哪些,陕西凌云电器有限公司官网?
如何在IIS管理器中快速创建并配置网站?
盘锦网站制作公司,盘锦大洼有多少5G网站?
网站制作需要会哪些技术,建立一个网站要花费多少?
魔毅自助建站系统:模板定制与SEO优化一键生成指南
常州自助建站:操作简便模板丰富,企业个人快速搭建网站
建站DNS解析失败?如何正确配置域名服务器?
制作网站外包平台,自动化接单网站有哪些?
如何选择美橙互联多站合一建站方案?
再谈Python中的字符串与字符编码(推荐)
新网站制作渠道有哪些,跪求一个无线渠道比较强的小说网站,我要发表小说?
微网站制作教程,不会写代码,不会编程,怎么样建自己的网站?
行程制作网站有哪些,第三方机票电子行程单怎么开?
如何在腾讯云免费申请建站?
高配服务器限时抢购:企业级配置与回收服务一站式优惠方案
seo网站制作优化,网站SEO优化步骤有哪些?
建站之星多图banner生成与模板自定义指南
,想在网上投简历,哪几个网站比较好?
*请认真填写需求信息,我们会在24小时内与您取得联系。