Android 开发中常常须要用一些自定义 View 去知足产品和设计的脑洞,因此 View 的绘制流程相当重要。网上目前有很是多这方面的资料,但最好的方式仍是直接跟着源码进行解读,每日一问系列一直追求短平快,因此本文笔者尽可能精简。java
想必大多数 Android 开发都知道自定义 View 须要关注的几个方法:onMeasure()
、onLayout()
和 onDraw()
,这其实也是每一个 View 相当重要的绘制流程。git
基本绘制都是会从根视图 ViewRoot
的 performTraversals()
方法开始,从上到下遍历整个视图树,每一个View控件负责绘制本身,而 ViewGroup 还须要负责通知本身的子 View 进行绘制操做。performTraversals()
的核心代码以下:github
private void performTraversals() {
...
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
...
//执行测量流程
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
//执行布局流程
performLayout(lp, desiredWindowWidth, desiredWindowHeight);
...
//执行绘制流程
performDraw();
}
复制代码
public final void measure(int widthMeasureSpec, int heightMeasureSpec) 复制代码
每一个 View 都有本身的大小,因此基本自定义 View 的时候都须要重写 onMeasure()
这个方法,以定制化咱们的 View 的宽高。**若是不重写这个方法,咱们一般会出现 wrap_content
和 match_parent
是同样的显示效果。**至于缘由,其实一探源码便知。json
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
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;
}
protected int getSuggestedMinimumHeight() {
return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}
复制代码
能够看到,View
默认是会使用 getDefaultSize()
方法进行设置宽高的,在 AT_MOST
和 EXACTLY
两种状况下都会直接使用测量规格里面的尺寸。在 UNSPECIFIED
模式下会直接取getSuggestedMinimumWidth()
的返回值。canvas
getSuggestedMinimumWidth()
会直接根据是否设置backgroud
来进行计算,须要注意的是,直接设置 color 做为backgroud
也会直接采用minXXX
的值。布局
在 ViewGroup
中,并无去重写 View
的 onMeasure()
方法,而这都须要它的子类根据本身的逻辑去实现,好比 LinearLayout
和 RelativeLayout
明显测量逻辑是不同的。不过,ViewGroup
却是提供了一个 measureChildren()
方法来依次遍历每一个子 View 对其进行测量。post
在通过 onMeasure()
操做后,getMeasureWidth()
和 getMeasureHeight()
方法就能够拿到正确的返回值了。spa
因为 View 的 measure 过程和 Activity 的生命周期方法不是同步执行的,若是 View 尚未测量完毕,那么得到的宽/高就是 0。因此在
onCreate()
、onStart()
、onResume()
中均没法正确获得某个 View 的宽高信息。能够经过在onWindowFocusChanged()
判断获取到焦点后进行获取,或者使用view.post()
方式。.net
public void layout(int l, int t, int r, int b) 复制代码
咱们能够重写的 onLayout()
方法主要做用是肯定子 View 的显示位置,因为 View 已是最小的层级,因此咱们在自定义 View 的时候一般不须要管这个方法,而在自定义 ViewGroup 的时候就不得不注意这个方法了。设计
通过 onLayout()
流程后,咱们的 left
、right
、top
、bottom
得以赋值,因此这时候能够经过 getWidth()
和 getHeight()
方法来获取 View 的实际宽高了。
注意:在 View 的默认实现中,View 的测量宽/高和最终宽/高是相等的,只不过测量宽/高造成于 View 的
measure
过程,而最终宽/高造成于 View 的layout
过程,即二者的赋值时机不一样,测量宽/高的赋值时机稍微早一些。在一些特殊的状况下则二者不相等:
public void draw(Canvas canvas) 复制代码
绘制的流程也就是经过调用 View 的 draw()
方法实现的。draw()
方法里的逻辑看起来更清晰,我就不贴源码了。通常是遵循下面几个步骤:
drawBackground()
onDraw()
dispatchDraw()
onDrawScrollbars()
因为不一样的控件都有本身不一样的绘制实现,因此V iew 的 onDraw()
方法确定是空方法。而 ViewGroup 因为须要照顾子 View 的绘制,因此确定在 dispatchDraw()
方法里遍历调用了child的 draw()
方法。
参考: