View的绘制流程是从ViewRoot的performTraversals方法开始的。
MeasureSpace
代表一个32位的int值,高2位代表SpaceMode,低30位代表SpaceSize。
SpaceMode分三类:
- ==UNSPECIFLED==:父容器不对View的大小做限制,要多大给多大
- ==EXACTLY==:父容器已经检测出了View的精确大小,这个模式下View的最终大小就是SpaceSize所给定的值,_相当于LayoutParams 中的match-parent和具体的数值两种。
- ==AT_MOST==:父容器制定了一个SpaceSize的可用大小,View的大小不能超过这个值,对应warp_content
获取SpaceMode和SpaceSize的方法是:
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
View 的 measure
view的测量只需要通过onMeasure方法就可以完成
==注意==:直接继承View的自定义控件需要重写onMeasure方法并设置warp_content时的自身大小,否则在布局中使用warp_content就相当于使用match_parent。具体实现代码:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
/**
* 给view制定一个默认的内部宽高,如果SpaceMode为MeasureSpec.AT_MOST然后布局文件中又设置为wrap_content,
* 则使用这个默认宽高,不然view的效果就会是match_parent的效果
*/
int mWidth = 50;
int mHeight = 50;
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST){
setMeasuredDimension(mWidth,mHeight);
}else if(widthMode == MeasureSpec.AT_MOST ){
setMeasuredDimension(mWidth,heightSize);
}else if (heightMode == MeasureSpec.AT_MOST){
setMeasuredDimension(widthSize,mHeight);
}
}
根据项目的实际情况而定,可以参考TextView的源码
ViewGroup的measure
ViewGroup除了要完成自己的measure过程,还会遍历去调用所有子view的measure方法。所以ViewGroup是通过measureChildren方法完成测量的。它的代码如下:
/**
* Ask all of the children of this view to measure themselves, taking into
* account both the MeasureSpec requirements for this view and its padding.
* We skip children that are in the GONE state The heavy lifting is done in
* getChildMeasureSpec.
*
* @param widthMeasureSpec The width requirements for this view
* @param heightMeasureSpec The height requirements for this view
*/
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);
}
}
}
然后是measureChild去测量具体的每一个view:
/**
* Ask one of the children of this view to measure itself, taking into
* account both the MeasureSpec requirements for this view and its padding.
* The heavy lifting is done in getChildMeasureSpec.
*
* @param child The child to measure
* @param parentWidthMeasureSpec The width requirements for this view
* @param parentHeightMeasureSpec The height requirements for this view
*/
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);
}
最后又回到了view的measure方法中。
一个常见的问题,经常需要子Activity启动的时候去获取一些控件的宽高,但是在onCreate、onStart、onResume中都无法正确获取到。因为view的measure方法和Activity的生命周期不是同步的。所以可以通过下面这四种方法来获取:
- onWindowFocusChanged
public void onWindowFocusChanged(boolean hasFocus){
super.onWindowFocusChanged(hasFocus);
if(hasFocus){
int width = view.getMeasuredWidth();
}
}
2.view.post(runnable)
void start(){
super.start();
view.post(new Runnable(){
@Override
public void run(){
int width = view.getMeasuredWidth();
}
}
});
3.ViewTreeObserver
void start(){
super.start();
ViewTreeObserver observer = view.getViewTreeOberver();
observer.addOnGlobalLyoutListener(new OnGlobalLayoutListener(){
@Override
public void onGlobalLayout(){
view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
int width = view.getMeasuredWidth();
}
})
}
4.手动计算view.measure(int widthMeasureSpec,int heightMeasureSpec)
layout
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
View view = getChildAt(0);
//左边与父容器的距离
int mLeft = 0;
//上边与父容器的距离
int mTop = 0;
//右边就是view的宽
int mRight = mLeft+view.getMeasuredWidth();
//下边就是view的高
int mBottom = mTop+view.getMeasuredHeight();
//因为上边和左边和父容器的边距都是0 ,所以这个view就是在父容器的左上角
view.layout(mLeft,mTop,mRight,mBottom);
}
其实layout也可以控制view的宽高,比如将上面的代码修改如下:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
View view = getChildAt(0);
//左边与父容器的距离
int mLeft = 0;
//上边与父容器的距离
int mTop = 0;
//右边就是view的宽
int mRight = mLeft+view.getMeasuredWidth()*4;
//下边就是view的高
int mBottom = mTop+view.getMeasuredHeight()*4;
//因为上边和左边和父容器的边距都是0 ,所以这个view就是在父容器的左上角
view.layout(mLeft,mTop,mRight,mBottom);
}
这个view的宽高就变为原来的4倍。
draw
draw的过程相对来说就比较简单,因为全是自己的逻辑。自己想实现什么效果就画什么,主要是能理解canvas的各种draw方法。
view有一个特殊的方法setWillNotDraw,如果一个view不需要绘制任何内容,就设置为true,系统就会进行相应的优化。默认情况下view的这个方法是false的,但是viewgroup是设置为true,所以当一个viewgroup要调用onDraw来绘制内容时需要设置setWillNotDraw为false。这需要在构造方法中设置,代码如下:
public class MyViewgroup extends ViewGroup {
public MyViewgroup(Context context) {
super(context);
}
public MyViewgroup(Context context, AttributeSet attrs) {
super(context, attrs);
//启用viewgroup的onDraw方法
setWillNotDraw(false);
}
对于直接继承view的自定义控件来说,如果不在onDraw方法中处理padding,那么padding属性就无法生效,代码处理如下:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//计算控件的宽高时要把padding值考虑进去
paddingLeft = getPaddingLeft();
paddingTop = getPaddingTop();
paddingRight = getPaddingRight();
paddingBottom = getPaddingBottom();
Rect rect = new Rect();
rect.left = 0 + paddingLeft;
rect.top = 0 + paddingTop;
rect.right = width - paddingRight;
rect.bottom = height - paddingBottom;
canvas.drawRect(rect,mPaint);
}