计算视图大小的过程(Measure)
视图大小,准确的来说应该是指视图的布局大小;我们在layout.xml中为每个UI控件设置的layout_width/layout_height两个属性被用来设置父视图给当前视图分配的“窗口”大小,为了开发的方便和对不同屏幕分辨率的兼容适配对这两个参数的赋值一般都使用相对值(也可以使用具体值,比如100dp),比如WRAP_CONTENT/MATCH_PARENT,在代码中用常量-2/-1表示;计算视图布局大小的过程本质上就是把视图布局时使用的“相对值”转换成具体值的过程;
Measure递归调用过程
View系统启动measure过程是从ViewRootImpl中调用host.measure(…)开始的,参见下图:
从上图中可以看出measure过程主要就是从顶层父视图向子视图的递归调用view.measure(…),注意以下几点:
- View.measure(...)该方法是final的,不允许重载,View子类只能通过重载onMeasure(...)来完善自己的测量逻辑;
- MeasureSpec测量规格在measure过程中经常作为输入参数,该值为int型,其值由两部分组成,高16位代表规格模式specMode,低16位代表具体尺寸specSize;其中specMode只有三种值:
- MeasureSpec.EXACTLY:确定模式,即父视图希望子视图的大小是确定的,由specSize决定;
- MeasureSpec.AT_MOST:最多模式,即父视图希望子视图的大小最多是specSize指定的值;
- MeasureSpec.UNSPECIFIED:未指定模式,此时父视图完全尊重子视图的设计;
- 最顶层视图DecorView测量时的MeasureSpec从何而来呢?
是在ViewRootImpl中调用 getRootMeasureSpec(..)获得,LayoutParam宽高参数均为MATCH_PARENT;
获得的specMode就是EXACTLY,specSize为物理屏幕大小; - 视图的布局大小由父视图和子视图共同决定;
layout.xml中对含有子视图的布局器中的layout_width/layout_height属性实际扮演了2个角色;一个是和父视图一起对布局器自身进行measure操作;另一个角色是作为其子视图的父视图参与子视图的measure操作; - ViewGroup类中提供了 measureChildWithMargins(…)方法,用来抽象和简化父子视图之间的padding、margin、实际内容区域间的测量和尺寸计算,让开发者可以无需关注这些公共的边界测量区域;
- 调用 child.getLayoutParams(),并强制转换成 MarginLayoutParams;只要是ViewGroup的子类,就要求LayoutParam继承于 MarginLayoutParams,即要在在 generateDefaultLayoutParams()中返回MarginLayoutParams或者其子类,否则将无法在layout.xml中使用layout_margin参数,并且代码中不能使用measureChildWithMargins(…)方法,否则会抛强制类型转换错误;
- 分别就宽高调用 getChildMeasureSpec(…);此时是传入父视图的测量规格,以及子视图在layout.xml中的设置值,返回的是子视图进行具体测量时的测量规格;父子视图之间的测量规格对应关系参见下图:
- 根据子视图自己的测量规格调用child.measure(…)进行最终的测量;
- 以上的逻辑由三方组成,包括父视图、开发者、子视图的设计者;父视图声明了自己可以提供的大小,而开发者在xml中使用layout_width/layout_height设置希望的大小,而视图设计者则进行最后的“拍板”,一般良好的视图设计者会根据子视图的layout_xx参数设置合适的布局大小,以尊重开发者的意图;
Layout递归调用过程
View系统启动layout过程是从ViewRootImpl中调用host.layout(…)开始的,参见下图:
从上图中可以看出layout过程也是从顶层父视图向子视图的递归调用view.layout(…),即父视图根据上一步measure子视图所得到的布局大小和布局参数,将子视图放在合适的位置上,布局参数核心指layout_gravity;注意以下几点:
- 与measure方法不同,View.layout(…)方法可被重载,ViewGroup.layout(…)为final的不可重载,ViewGroup.onLayout(…)为abstract的,子类必须重载,里面可以实现自己的位置分配逻辑;
- measure操作完成后得到的是对每个View经测量过的宽高measuredWidth/measuredHeight;
layout操作完成之后得到的是对每个View进行位置分配后的mLeft/mTop/mRight/mBottom; - layout_gravity一般出现于布局容器中,比如LinearLayout,他指的是当前容器内子视图的排列方式和顺序;
gravity一般出现于具体的View中,比如TextView,他指的就是TextView中实际文字的位置排放方式; - 凡是以layout_开头的布局参数基本都针对的是包含子视图的容器视图的,比如核心的layout_width/layout_height/layout_weight/layout_gravity,会在初始化LayoutParam时从xml中读取并转换为相应参数;
当对一个没有父容器的View设置相关layout_开头的属性时,实际上是没有任何意义的;
Draw递归调用过程
绘制过程就是把View对象绘制到屏幕上,如果该View是一个容器ViewGroup,则需要递归绘制其所包含的所有子视图;视图中可绘制的元素包括:
- View背景Backgroud:每个视图都可以有一个背景,背景可以是一个颜色值,也可以是一副图片,甚至可以是任何Drawable对象;
- 视图自身的内容:一般由视图设计者在视图的onDraw方法中完成具体的内容绘制,比如TextView的内容就是具体的文字,若是视图容器ViewGroup,则需要递归完成子视图的具体内容绘制;
只有这项内容是需要开发者设计并实现的,其余三项内容均由系统自动完成绘制; - 渐变边框FadingEdge:其作用是为了让视图的边框看起来更有层次感,其本质就是一个Shader对象,当然可以通过配置项关闭该效果;
- 滚动条ScrollBar:用来显示当前滚动的位置和状态,与PC不一样,该滚动条一般不能直接按住拖动;
View系统启动draw过程是从ViewRootImpl中调用host.draw(…)开始的,总体过程与measure/layout极其类似,即从顶级父视图开始向子视图进行递归调用view.draw(…),具体可参见下图:
下面就来看看这个过程中的几个核心方法都做了哪些具体的事情;
- ViewRootImpl.draw()内部流程参见下图:
里面要特别注意的一个核心点就是会根据根视图内部的Scroller对象来调用mScroller.computeScrollOffset()判断当前视图是否还处于滚动状态,若处于滚动状态,则会进行滚动偏移量计算,并且在最后再次调用scheduleTraversals()来发送一个异步重绘请求;
另外一点就是:Surface会按照底层驱动模式自动执行显卡模式或CPU模式;前者采用显卡来进行页面绘制,支持硬件图形加速,一般基于OpenGL实现;后者也被成为软件模式,即通过CPU及内存来模拟图形绘制,不支持硬件加速;目前的一些高端机型几乎都是显卡模式,低端机型更多采用CPU模式; - View.draw()内部流程主要就是为了完成前面提到的视图中的各种具体元素的绘制,大致过程如下:
- 绘制背景,由于可能会出现滚动条,所以绘制时可能会涉及到Canvas画布的平移和恢复,即 canvas.translate(scrollX, scrollY);
- 判断是否需要显示渐变框,若不需要则直接进入后续3、4、5绘制逻辑;
- 绘制视图自身内容,通过回调onDraw()实现;
- 调用dispatchDraw()绘制子视图,对于ViewGroup而言,默认已重载该方法,如有特别需求子类无需再次重载该函数;
- 调用onDrawScrollBars()绘制滚动条;
- ViewGroup.dispatchDraw()的作用是绘制父视图中包含的子视图,其本质就是给不同的子视图分配合适的画布Canvas,至于子视图具体如何绘制,则又会递归回调View.draw()方法;该方法内部将根据onLayout()中为子视图分配的具体区块调整Canvas的内部剪切区,从而让子视图认为画布是他自己独享的,坐标也是从(0,0)开始;其内部具体执行流程参见下图:
有几点需要注意:- 区分View动画和ViewGroup布局动画:前者指的是View自身的动画,可以通过 setAnimation(.)添加;而后者是专门针对ViewGroup而言的,指的是该ViewGroup在显示内部的子视图时而设置的动画,可以在layout.xml中对容器标签设置layoutAnimation属性,比如可以对LinearLayout设置子视图在显示时出现逐行显示、随机显示、或落下等不同的动画效果;
- 在获取画布剪切区时会自动处理掉padding,让子视图获取的画布无需关注这些附加逻辑;
- 默认情况下子视图的ViewGroup.drawChild()绘制顺序与子视图被添加的顺序一致,但开发者可以重载ViewGroup. getChildDrawingOrder()方法提供不同的顺序;
- 当给一个子视图添加了移除动画时,该子视图会被添加到 mDisappearingChildren队列中,在动画结束之前该子视图将一致存在,但此时该子视图无法被点击,也无法获得任何消息事件,仅仅是可见而已;
- ViewGroup.drawChild()的核心过程是为子视图分配合适的Canvas剪切区,其大小取决于child的布局大小,位置取决于child的内部滚动值、以及当前动画,其内部流程参见下图:
有几点需要注意:- 该方法在新的版本(比如4.2.2)中已经被放在View中了,ViewGroup中直接中转调用child.draw(canvas, this, drawingTime);
这是在看android源码时需要特别注意的,View/ViewGroup之间有很多方法重载,且存在父子视图的递归调用,经常会把人搞晕,看代码时需要非常平静才行; - 对于ViewGroup中子视图的动画支持有两种方式,一种是通过setAnimation(.)添加,另外一种是通过重载ViewGroup.getChildStaticTransformation(View child, Transformation t)方法实现;
注意:这里使用的这种代码设计方式我个人觉得是很不优雅的,有点反模式的味道,他在ViewGroup中声明了一个变量 mChildTransformation,是一个空的Transformation实例,然后在View.draw(…)方法中使用时将该变量传递给上面的需要开发者重载的getChildStaticTransformation方法,开发者需要在方法内对输入参数t进行具体动画参数赋值,并return true,标示采用该静态回调方法来处理子视图的动画;
更符合常规的设计思路不应该是让开发者直接重载getChildStaticTransformation(View child);方法并返回具体的Transformation实例吗?
- 该方法在新的版本(比如4.2.2)中已经被放在View中了,ViewGroup中直接中转调用child.draw(canvas, this, drawingTime);
- View.onDrawScrollBars()的作用是绘制滚动条;
- 滚动条是View的基本元素之一,每个View都可以有滚动条,只是某些视图从用户体验的角度无需显示而已,可以在layout.xml中通过scrollBarXXX相关的属性设置滚动条;
- 滚动条可以是水平的、垂直的、或者两者均有,滚动条的展示封装在ScrollBarDrawable中,包括三个基本尺寸range/offset/extent、以及用来标识滚动条背景和自身的两个图片track/thumb;
range:代表滚动条从头至尾滚动过程中所跨越的范围有多大,比如你想用滚动条来标示一万行代码,那range就可以设置10000;
offset:代表滚动条当前的偏移量,或者是可视的第一行在整个滚动跨度的什么位置,比如当前已经滚动到600行了,那offset就是600;
extent:代表显示滚动条的视图View在屏幕上的可视高度或宽度,比如200dp;
track:代表滚动条显示的背景或轨道,一般其宽高度和extent一致;
thumb:代表滚动条显示的具体前景,其宽高度、位置会根据range/offset/extent三者的具体值进行计算获得;
以上描述的几个变量具体展示参见下图:
- 滚动条一共有三种状态ON/OFF/FADDING,即显示、隐藏、正在隐藏(处于显示状态,但通过设置透明度悄悄改变状态);
一般而言,从用户体验的角度来说滚动条会在滚动完毕后自动隐藏,而滚动时自动显示,开发者可以通过scrollBarFadeDuration参数设置自动隐藏间隔时间,也可以调用 setScrollbarFadingEnabled()设置是否自动隐藏; - 开发者可重载3个 computeVerticalScrollXXX()方法来实现对滚动条具体显示位置和thumb大小的控制,可以调用awakenScrollBars()方法强制显示滚动条;
自定义滚动位置时可调用scrollTo()/scrollBy()来完成滚动到具体位置、或者滚动具体距离;