View的绘制流程
View的绘制流程是从ViewRootImpl#performTraversals方法开始的,在performTraversals中会分别通过performMeasure,performLayout,performDraw这三个方法来绘制一个View
具体可以参考setContentView那些事
- measure: 用来测量当前View的宽度和高度
- layout: 用来确定View在父容器的存放位置
- draw: 将View绘制到屏幕上
如下图:
measure过程决定了当前View的宽度和高度,measure完成之后,就可以通过getMeasuredWidth和getMeasuredHeight来获取其测量后的宽度和高度,layout决定了View的四个顶点的位置,
draw方法决定了View的显示。
MeasureSpec测量模式
MeasureSpec是View的一个内部类,有三种分类:
- UNSPECIFIED
父容器不对当前View做任何限制,一般用于系统内部
- EXACTLY
父容器检测出当前View所需要的精确大小,此时View的最终大小就是SpecSize所指定的值,对应于LayoutParams中给定确定的数值和match_parent这两种模式
- AT_MOST
父容器指定了一个大小可用的SpecSize,View的大小不能大于这个值,对应于LayoutParams中wrap_content
View的工作过程
View的工作流程,主要指measure,layout和draw这三个流程
measure过程
measure过程分两种情况,如果是一个简单的View,那么measure方法就完成了其测量过程,如果是ViewGroup,那么除了自己的测量过程外,还需要遍历调用所有
子元素的measure方法,各个子元素再递归去执行这个流程
View的measure过程
View的measure过程从自身的measure方法开始,measure是一个final方法,所以在子View中不能复写该方法。在View的measure方法中会调用View的onMeasure方法。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
如果在子类中复写了onMeasure方法,那么子View将负责在自己的onMeasure方法中计算自己的宽度和高度,上述通过setMeasuredDimension设置当前View宽度和高度的测量值
ViewGroup的measure过程
对于ViewGroup除了自己的测量过程外,还需要遍历调用所有子元素的measure方法,各个子元素再递归去执行这个流程,ViewGroup是一个抽象类,因此其没有重写View的onMeasure方法,
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);
}
}
}
可以看到,ViewGroup在measure时候,会遍历每一个子元素进行通过measureChild测量
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
// 取出子元素的LayoutParams
final LayoutParams lp = child.getLayoutParams();
// 创建子元素的MeasureSpec
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
// 调用子元素的measure方法
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
View在measure完成之后,可以通过getMeasuredWidth和getMeasuredHeight获取其测量的宽度和高度,不过建议最好在onLayout方法中获取View测量的宽高。
Activity中获取View的宽度和高度
如果我们需要在Activity的onCreate方法中获取当前View的宽度和高度,会发现获取到的都是0,为什么会这样?这是因为View的measure过程和Activity的生命周期方法不是同步的,因此无法保证在Activity执行了onCreate,onStart,onResume时刻,View已经测量完毕,如果View还没有测量完毕,那么或得到的宽度和高度就是0.我们可以通过下面方法解决这个问题:
在Activity中复写onWindowFocusChanged
onWindowFocusChanged表示View已经初始化完毕了,此时获取宽度和高度是没有问题的,不过onWindowFocusChanged可能会多次执行,当activity获取或者失去焦点的时候,onResume和onPause方法多次被调用,那么onWindowFocusChanged也会被频繁的调用
public void onWindowFocusChanged(boolean hasFocus) {
if(hasFocus) {
int width = mTextView.getWidth();
int height = mTextView.getHeight()
}
}
View.post(runnable)
通过post可以将一个runnable放入到消息队列的尾部,然后等待Lopper调用此runnable时候,View也已经初始化好了
mTextView.post(new Runnable() {
@Override
public void run() {
int width = mTextView.getMeasuredWidth();
int height = mTextView.getMeasuredHeight();
}
});
ViewTreeObserver
可以在ViewTreeObserver中设置很多回调,比如可以使用OnGlobalLayoutListener,当View树的状态发生改变的时候,会回调onGlobalLayout,可以在这个方法里获取当前View的宽度和高度
ViewTreeObserver viewTreeObserver = mTextView.getViewTreeObserver();
viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
mTextView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
int width = mTextView.getMeasuredWidth();
int height = mTextView.getMeasuredHeight();
}
});
自定义View
自定义View主要分为下面四类:
- 继承自View,重写onDraw方法
- 继承自ViewGroup
- 继承自特定的View(比如TextView)
- 继承自特定的ViewGroup(比如LinearLayout)
自定义View需要注意的:
- 让View支持wrap_content
如果我们的控件直接继承自View或者ViewGroup,如果不在onMeasure中对wrap_content做特殊处理,那么当在布局中使用wrap_content时候,就无法达到预期效果 - 让View支持padding
原因同上 - 及时停止View中的线程和动画
如果当前View变为不可见,或者包含此View的activity退出时候,或者当前View被remove时,需要及时停止其线程和动画,否则可能造成内存泄漏 - 当有嵌套View时候,处理好滑动冲突