第四章,View的工作原理
本章主要介绍两方面的内容
1. View的工作原理
2. 自定义View的实现方式
需要掌握:View的三大流程;View的常见回调方法;View滑动(上一章中的滑动冲突处理)
大纲
ViewRoot 和 DecorView
MeasureSpec
View工作流程
自定义View
初识ViewRoot 和 DecorView
ViewRoot
的实现类是ViewRootImpl
,是链接WindowManager
和DecorView
的纽带,View
的三大流程都是通过ViewRoot
来完成的
一. ViewRoot
View
的绘制流程是从ViewRoot
的performTraversals
开始的.
//这里面的三大流程中,前面的方法会调用后面的方法,ps:performMeasure会调用measure,measure会调用onMeasure
ViewRoot.performTraversals ->
(performMeasure)measure(onMeasure) ->
(performLayout)layout(onLayout)->
(performDraw)draw(onDraw)
- 其中
measure
用来测量View
的宽和高,measure
后即可获取到View
测量的宽高.layout
用来确定View
在父容器中的放置位置
,- 而
draw
则负责将View绘制
在屏幕上,draw会
调用dispatchDraw
来对子View
进行draw
,只有draw
方法完成后View
才能显示在屏幕上.
二. DecorView
DecorView
是一个FrameLayout
DecorView
是一个顶级View
,一般里面包含一个LinearLayout
,上面的是标题栏
,下面的是内容栏
.setContentView
就是将布局文件设置到内容区(id为android.R.id.content
的FrameLayout
中);
理解MeasureSpec
MeasureSpec
有点像测量规格
或者测量说明书
,View
的尺寸和规格受MeasureSpec
和父容器
影响.
系统会将View
的LayoutParams
根据父容器所施加的规则
转换成对应的MeasureSpec
MeasureSpec
- 一个32位的int值,
SpecMode
(高2位),SpecSize
(低30位).- 有三类
SpecMode
,UNSPECIFIED
(没有限制),EXACTLY
(精确性),AT_MOST
(不能大于这个值)
MeasureSpec与LayoutParams的关系
对于DecorView
,其MeasureSpec
有窗口尺寸
和自身的LayoutParams
共同决定.
DecorView
的MeasureSpec
创建过程,可以查看ViewRootImpl#measureHierarchy,其中会调用ViewRootImpl#getRootMeasureSpec,- EXACTLY模式下,DecorView大小就是窗口大小
- AT_MOST模式下,DecorView大小不定,不超过窗口大小
- 固定大小(EXACTLY),大小为LayoutParams中指定的大小.
对于普通View
,其MeasureSpec
有父容器的MeasureSpec
和自身的LayoutParams
共同决定.
- 查看ViewGroup#measureChildWithMargins,其中会调用ViewGroup#getChildMeasureSpec得到子元素的
MeasureSpec
.- 子元素的
MeasureSpec
与父容器的MeasureSpec
和本身的LayoutParams
及View的Margin与Padding
有关.
View工作流程
一. measure 过程
View的measure过程
//View#onMeasure
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
- 一般情况下
getDefaultSize
得到的就是View
测量后的大小. 在
setMeasuredDimension()
方法调用之后,我们才能使用getMeasuredWidth()
和getMeasuredHeight()
来获取视图测量出的宽高,以此之前调用这两个方法得到的值都会是0.View
的最终大小在onLayout
中确定,而测量大小
在onMeasure
中确定,大多数情况下他们是相等的getDefaultSize
一般情况下,返回的是测量后的大小
,在UNSPECIFIED
模式下才返回getSuggestedMinimumWidth()
和getSuggestedMinimumHeight()
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;
}
getSuggestMinimumWidth
对应如下情况:view
没背景则对应android:minWidth
,如果此属性没指定
,则为0
view
有背景则是minWidth(上面属性对应的值)和 background的minimumWidth
的最大值,- 通过Drawable#getMinimumWidth看出
background的minimumWidth
返回的是Drawable的原始宽高
.
直接继承
View的自定义控件
需要重写onMeasure
方法并设置wrap_content
时的自身大小,否则在布局中使用wrap_content
时就相当于使用match_parent
解决方法: 在wrap_content时,给View设置一个默认的内部宽/高
//解决示例
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthSpecMode == MeasureSpec.AT_MOST
&& heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(200, 200);
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(200, heightSpecSize);
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpecSize, 200);
}
}
ViewGroup的measure过程
ViewGroup
是一个抽象类,没有重写onMeasure
,提供了一个measurechild
的方法(先拿到子View的LayoutParams,根据LayoutParams确定其MeasureSpec
,接着将MeasureSpec
传递给子view
进行测量)
因为不同的ViewGroup的布局特性不一样,导致其测量细节各不相同.
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);
}
在某些极端情况下,系统可能需要多次measure才能确定view的大小.
比较好的习惯是在onLayout
中获取view的宽高
四种方法获取View的宽高
通过如下四种方法
获取View的宽高
Activity/View#onWindowFocusChanged
view.post(runnable)
将runnable
将消息投递到队列的尾部ViewTreeObserver
- 手动调用
View#measure
方法;
第四种方式需要根据
LayoutParams
分情况处理
//1. MATCH_PARENT: 无法测量
//2. 具体值 dp/px
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100,EXACTLY);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100,EXACTLY);
//3. wrap_content
int widthMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,AT_MOST);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,AT_MOST);//理论上能支持的最大值来构造
view.measure(widthMeasureSpec,heightMeasureSpec);
二. layout 过程
layout用于ViewGroup确定子元素的位置.
- layout方法确定view本身的位置,但onLayout确定所有子元素的位置.
- layout中首先通过setFrame来确定l,r,t,b四个位置,接着调用onLayout.
- View 的onLayout是空方法,ViewGroup的onLayout是一个抽象方法,不同ViewGroup的布局特性不一致.
- 自定义ViewGroup中的onLayout中会遍历调用子view的layout过程
- getMeasuredWidth和getWidth只是赋值时机不同(测量宽高的赋值时机要稍微早一些),值一般相等
//在自定义view中将r增大,则导致`最终宽高`和`测量宽高`不一致
@Override public void layout(int l, int t, int r, int b) {
super.layout(l, t, r+100, b);
}
三. draw 过程
background.draw (绘制背景)->
onDraw (绘制自己)->
dispatchDraw(draw child)(绘制children) ->
onDrawScrollBars.(绘制装饰)
dispatchDraw
用于绘制传递onDraw
一般是一个空方法,不同的子View/子ViewGroup
绘制过程是不同的.- View的dispatchDraw是空方法,ViewGroup的dispatchDraw有代码.
setWillNotDraw
:如果一个View不需要绘制任何内容,设置这个标记位true后,系统会进行优化.- 默认情况下View没有开启,ViewGroup开启了setWillNotDraw.
- 当明确知道一个
ViewGroup
需要通过onDraw
来绘制内容时,需要显示地关闭WILL_NOT_DRAW
标记.
自定义View
- 继承
View
,实现onDraw
,需要支持wrap_content和padding
处理- 继承
ViewGroup
,需要处理自己和子元素的测量和布局过程
.- 继承特定的
View(TextView)
,不需要处理wrap_content和padding
- 继承特定
ViewGroup(LinearLayout)
,和2
差不多
需要注意
- 支持
wrap_content
,否则wrap_content
就和match_parent
效果相同.- 处理好
padding
,(需要在draw中处理padding)否则padding属性是无法起作用的- 直接继承自
ViewGroup
的控件需要在onMeasure
和onLayout
中处理padding
和margin
.- 不需要使用
handler
,因为View有post方法- 线程和动画及时停止
View#onDetachedFromWindow
,否则会导致内存泄漏- 嵌套滑动,需要
冲突处理
,参考第3章