最近看到一篇对view绘制讲的很好的博客,特此也总结一下我对view绘制的理解。
先对上图做一波简单的解释:Activity内部持有一个PhoneWindow,PhoneWindow才是真正展示用户界面的大boss。
PhoneWindow这个类是Framework为我们提供的Android窗口的具体实现,我们熟悉的Dialog也继承自它。当调用setContentView()方法设置Activity的用户界面时,实际上就完成了对所关联的PhoneWindow的ViewTree(也就是ContentView)的设置。
那么DecorView是啥?DecorView是Window(抽象类,PhoneWindow实现自Window)的最顶层view,它内部是一个LinearLayout包括TitleView和ContentView两部分。
ContentView实际上是一个FrameLayout,平时我在Activity中setContentView()也就是想布局add到ContentView上。这里走进setContentView()源码发现了平时我们常用的LayoutInflater.inflate(),它就是真正填充布局的方法。这里不多解释,感兴趣的童鞋可以看看源码。
View的绘制:
1.View的绘制实际是由ViewRootImpl负责,那么ViewRootImpl是啥?
它是的官方注释是实现视图和窗口管理器之间所需的协议。
2.那是完成啥协议呢?
这里最主要完成的就是视图的绘制以及各种点击事件。
3.那么View的绘制什么时候开始绘制呢?
是setContentView()的时候吗?答案是NO,setContentView()方法只是将ViewTree添加到DecorView中,此时并没有开始绘制。DecorView真正的绘制是在activity.handleResumeActivity方法中DecorView被添加到WindowManager时候,也就是调用到windowManager.addView(decorView)。而在windowManager.addView方法中调用到windowManagerGlobal.addView,开始创建初始化ViewRootImpl,再调用到viewRootImpl.setView,最后是调用到viewRootImpl的performTraversals来进行view的绘制(measure,layout,draw),这个时候View才真正被绘制出来。
经过上面的灵魂三问,我们来看看源码学习View绘制流程,刚刚提到viewRootImpl.setView()方法,那么我们就从它看起吧:
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
// Schedule the first layout -before- adding to the window
// manager, to make sure we do the relayout before receiving
// any other events from the system.
requestLayout();
}
}
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
}
}
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
void doTraversal() {
performTraversals();
}
private void performTraversals() {
...
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
performLayout(lp, desiredWindowWidth, desiredWindowHeight);
...
performDraw();
...
}
//这几个方法最终都会调用
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
mView.draw(canvas);
从setView()方法我们发现了requestLayout(),走入这个方法可以看出主要判断是否是当前线程,判断结束最终会走到performTraversals()方法,这是就出现了关键的三个方法measure,layout,draw。
measure(int widthMeasureSpec, int heightMeasureSpec)
measure顾名思义就是来测量view的大小,对于一个页面的绘制流程会从ViewRoot的performTraversals()方法中开始递归依次去测量子视图,viewGroup中的measureChildren()方法就是来去测量子视图的大小,之后再返回给viewRoot。我们看下面这张图:
它是一种树的遍历过程,它是根据一些父容器对子容器测量规格以及参数去测量子容器的大小,测量完成之后再返回给父容器。
measure方法中重要参数:
一.ViewGroup.LayoutParams
这个参数大家应该都不陌生,使用最多的场景就是在类中动态设置控件的摆放位置。
可以设置三种参数:
1.固定数值,单位px
2.ViewGroup.LayoutParams.MATCH_PARENT ,意思为宽度和父view相同
3.ViewGroup.LayoutParams.WRAP_CONTENT,意思为自适应
二.MeasureSpec
MeasureSpec中文解释是测量规格,它是一个32位的int值,由前两位的specSize(记录大小)和后30位的specMode(记录规格)共同组成。
specMode有三种模式:
final int specMode = MeasureSpec.getMode(measureSpec);
final int specSize = MeasureSpec.getSize(measureSpec);
final int result;
switch (specMode) {
case MeasureSpec.AT_MOST:
if (specSize < size) {
result = specSize | MEASURED_STATE_TOO_SMALL;
} else {
result = size;
}
break;
case MeasureSpec.EXACTLY:
result = specSize;
break;
case MeasureSpec.UNSPECIFIED:
default:
result = size;
}
EXACTLY:大小是由specSize的值来决定的,系统默认会按照这个规则来设置子视图的大小
AT_MOST:子视图最多只能是specSize中指定范围内的大小,开发人员应该尽可能小得去设置这个视图,并且保证不会超过specSize。
UNSPECIFIED:开发人员可以设置任意的大小,没有任何限制。这种情况比较少见,不太会用到。
在measure()方法中会调用onMeasure()方法最终会调用到setMeasuredDimension()方法,这时候才能使用getMeasuredWidth()和getMeasuredHeight()来获取视图测量出的宽高,以此之前调用这两个方法得到的值都会是0。
layout(0, 0, host.mMeasuredWidth, host.mMeasuredHeight);
layout()是确定view的摆放位置的方法,它同measure()也是自上而下递归遍历子视图的摆放位置的。值得注意的是ViewGroup中的onLayout()方法是一个抽象方法,这就意味着所有ViewGroup的子类都必须重写这个方法。像LinearLayout、RelativeLayout等布局,都是重写了这个方法,然后在内部按照各自的规则对子视图进行布局的。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mOrientation == VERTICAL) {
layoutVertical(l, t, r, b);
} else {
layoutHorizontal(l, t, r, b);
}
}
例如LinearLayout,它重写onLayout()方法实现自己内部子控件的摆放规则。
onDraw(Canvas canvas)
当measure和layout结束后就真正的进入了视图绘制的draw方法。这里主要说一下两个容易混淆的方法:
一.invalidate()和postInvalidate()
invalidate()它是用于进行刷新重写绘制重新调用draw(),但是不会调用layout()和measure()。
invalidate方法和postInvalidate方法都是用于进行View的刷新,invalidate方法应用在UI线程中,而postInvalidate方法应用在非UI线程中,用于将线程切换到UI线程,postInvalidate方法最后调用的也是invalidate方法。
invalidate()触发时机:
1、直接调用invalidate()方法,请求重新draw(),但只会绘制调用者本身。
2、setSelection()方法 :请求重新draw(),但只会绘制调用者本身。
3、setVisibility()方法 : 当View可视状态在INVISIBLE转换VISIBLE时,会间接调用invalidate()方法,继而绘制该View。
4 、setEnabled()方法 : 请求重新draw(),但不会重新绘制任何视图包括该调用者本身。
二.requstLayout()
requestLayout会直接递归调用父窗口的requestLayout,直到ViewRootImpl,然后触发peformTraversals,由于mLayoutRequested为true,会导致onMeasure和onLayout被调用。不一定会触发OnDraw。
requestLayout触发onDraw可能是因为在在layout过程中发现l,t,r,b和以前不一样,那就会触发一次invalidate,所以触发了onDraw,也可能是因为别的原因导致mDirty非空(比如在跑动画)
到这里view的绘制就大致小结完了,希望对大家有帮助,也欢迎提问互相学习。
参考资料: