牛骨文教育服务平台(让学习变的简单)

在开发过程中我们经常要进行view的自定义。如果熟练掌握自定义技巧的话就能做出很多控件出来。这篇博客来讲讲view绘制背后发生的那些事。

一, view的基础知识

view的绘制概括

首先先说说view绘制的整体过程。 
View绘制的源码分析 ,它的三大流程都是在ViewRootImpl中完成的,从ViewRootImpl中的performTraversals开始,有三个方法performMeasure,performLayout,prformDraw分别对measure,layout,draw三个方法。在onMeasure对所有子元素进行measure过程 ,这时measure就从父容器传递到子元素。子元素重复父元素的过程。layout与draw类似,只是draw通过diapatchDraw来实现 。 
measure完成后可以通过getMeasureWidth,getMeasureHeight分别获取View测量后的宽高。在实际情况下几乎所有情况它都等于最终宽高。layout过程决定view的四个顶点的坐标和实际view的宽高,完成之后可以通过getTop,getBottom,getLeft,getRight来拿 到view的四个顶点位置。并通过getWidth()和getHeight()来拿到最终宽高。draw决定了view的显示,只有完成才能显示在屏幕上。

MeasureSpec

在测量过程中系统会将View的LayoutParams根据容器所施加的规则转换成对应的MeasureSpec,然后再根据这个测量出view。 
Measure是一个32位的int,高2位代表SpecMode,低30位代表SpecSize。SpecMode表示测量模式,SpecSize指在某种测量模式下规格的大小。其代码如下:

public static class MeasureSpec {
    private static final int MODE_SHIFT = 30;
    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

    public static final int UNSPECIFIED = 0 << MODE_SHIFT;

  public static final int EXACTLY     = 1 << MODE_SHIFT;
 public static final int AT_MOST     = 2 << MODE_SHIFT;

public static int makeMeasureSpec(int size, int mode) {
    if (sUseBrokenMakeMeasureSpec) {
        return size + mode;
    } else {
        return (size & ~MODE_MASK) | (mode & MODE_MASK);
    }
}
public static int getMode(int measureSpec) {
    return (measureSpec & MODE_MASK) ;
}
public static int getSize(int measureSpec) {
    return (measureSpec & ~MODE_MASK) ;
}

其实MeasureSpec中源码很值得我们学习。他用一个32位的int来表示模式和大小,节省了空间,也更直观。MeasureSpec通过将specMode和specSize打包成一个int来避免过多的对象内存分配。以上是MeasureSpec的打包和解包过程。 
specMode有三种状态:UNSPECIFIED,EXACTLY(相当于match_parent和精确值这两种模式),AT_MOST(wrap_content)。

LayoutParams

对于一般容器,它的MeasureSpec是由父容器的MeasureSpec和自身的LayoutParams共同决定的。上篇博客LayoutInflater源码解析 我们己经介绍了android view的结构,PhoneWindow包了一层DecorView,DecorView里才是title和我们的content view。所以行分析DecorView。 
先来看下DecorView的产生源码:

 childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width);
                childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
                performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

再看下getRootMeasureSpec方法:

private static int getRootMeasureSpec(int windowSize, int rootDimension) {
        int measureSpec;
        switch (rootDimension) {

        case ViewGroup.LayoutParams.MATCH_PARENT:
            // Window can"t resize. Force root view to be windowSize.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            // Window can resize. Set max size for root view.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            // Window wants to be an exact size. Force root view to be that size.//自定义
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
        }
        return measureSpec;
    }

这里很清楚,分别分MatchPraent和wrap_content和自定义来计算宽高。再来看下普通的view,在ViewGroup的measureChildWIthMargins中:

protected void measureChildWithMargins (View child,
        int parentWidthMeasureSpec , int widthUsed,
        int parentHeightMeasureSpec , int heightUsed) {
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams() ;

    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec ,
            mPaddingLeft + mPaddingRight + lp. leftMargin + lp.rightMargin
                    + widthUsed, lp.width) ;
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec ,
            mPaddingTop + mPaddingBottom + lp. topMargin + lp.bottomMargin
                    + heightUsed, lp.height) ;

    child.measure(childWidthMeasureSpec , childHeightMeasureSpec);
}

再看下getChildMeasureSpec:

public static int getChildMeasureSpec(int spec, int padding , int childDimension) {
    int specMode = MeasureSpec.getMode(spec);
    int specSize = MeasureSpec. getSize(spec) ;

    int size = Math. max( 0, specSize - padding) ;

    int resultSize = 0;
    int resultMode = 0;

    switch (specMode) {
    // Parent has imposed an exact size on us
    case MeasureSpec.EXACTLY:
        if (childDimension >= 0) {
            resultSize = childDimension;
            resultMode = MeasureSpec. EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // Child wants to be our size. So be it.
            resultSize = size ;
            resultMode = MeasureSpec. EXACTLY;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            // Child wants to determine its own size. It can"t be
            // bigger than us.
            resultSize = size ;
            resultMode = MeasureSpec. AT_MOST;
        }
        break;

    // Parent has imposed a maximum size on us
    case MeasureSpec.AT_MOST:
        if (childDimension >= 0) {
            // Child wants a specific size... so be it
            resultSize = childDimension ;
            resultMode = MeasureSpec. EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // Child wants to be our size, but our size is not fixed.
            // Constrain child to not be bigger than us.
            resultSize = size ;
            resultMode = MeasureSpec. AT_MOST;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            // Child wants to determine its own size. It can"t be
            // bigger than us.
            resultSize = size ;
            resultMode = MeasureSpec. AT_MOST;
        }
        break;

    // Parent asked to see how big we want to be
    case MeasureSpec.UNSPECIFIED:
        if (childDimension >= 0) {
            // Child wants a specific size... let him have it
            resultSize = childDimension ;
            resultMode = MeasureSpec. EXACTLY;
        } else if (childDimension == LayoutParams.MATCH_PARENT) {
            // Child wants to be our size... find out how big it should
            // be
            resultSize = 0;
            resultMode = MeasureSpec. UNSPECIFIED;
        } else if (childDimension == LayoutParams.WRAP_CONTENT) {
            // Child wants to determine its own size.... find out how
            // big it should be
            resultSize = 0;
            resultMode = MeasureSpec. UNSPECIFIED;
        }
        break;
    }
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode) ;
}

以上表明,如果父是EXACTLY,parentSize,那么子如果是EXACTLY, 
1)具体的值size:那子的MeasureSpec就是EXACTLY,size; 
2)MATCH_PARENT:那子的MeasureSpec就是EXACTLY,parentSize; 
3)WRAP_CONTENT:那子的MeasureSpec就是AT_MOST,parentSize;

如果父是ATMOST,parentSize,那么子如果是EXACTLY, 
1)具体的值size:那子的MeasureSpec就是EXACTLY,size; 
2)MATCH_PARENT:那子的MeasureSpec就是AT_MOST,parentSize; 
3)WRAP_CONTENT:那子的MeasureSpec就是AT_MOST,parentSize; 
总结:对于普通View的MeasureSpec由父容器的MeasureSpec和自身的LayoutParams来共同决定。

二,三大过程源码分析

OnMeasure

  • measure。如果是view,measure绘制其自身。如果是VIewGroup,measure绘制自身外,还要绘制其子元素。先看View的measure方法,measure是一个final方法,不能重写:
if (cacheIndex < 0 |if (cacheIndex < 0 || sIgnoreMeasureCache ) {
    // measure ourselves, this should set the measured dimension flag back
    onMeasure(widthMeasureSpec , heightMeasureSpec);
    mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}| sIgnoreMeasureCache ) {
    // measure ourselves, this should set the measured dimension flag back
    onMeasure(widthMeasureSpec , heightMeasureSpec);
    mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}

调用了onMeasure(),来看下它的源码:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth() , widthMeasureSpec) ,
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)) ;
}

看下getSuggestedMinimumWidth(),它就是获取背景大小和mMinWidth的较大值:

protected int getSuggestedMinimumWidth () {
    return (mBackground == null) ? mMinWidth : max(mMinWidth , mBackground .getMinimumWidth());
}

那么mMinWidth是什么呢,mMinWidth就是设置的android:minWidth的属性,没设置就等于0。不信,看如下代码:

case R.styleable.View_minWidth:
    mMinWidth = a.getDimensionPixelSize(attr , 0) ;
    break;

getMinimumWidth()表示的是获取背景图大小,它位于Drawable下:

public int getMinimumHeight() {
    final int intrinsicHeight = getIntrinsicHeight() ;
    return intrinsicHeight > 0 ? intrinsicHeight : 0 ;
}

看下getDefaultSize方法:

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;
}

它返回了specSize,它是测量后的大小。由上面的分析可知,view的宽高由specSize决定,而如果直接继承View的控件需要重写onMeasure方法并设置wrap_content的自身大小。否则wrap_content就相当 于Match_parent了。一般一重写方法如下:

 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);
        }
    }

上面的200是指定的一个默认宽高。

2.ViewGroup的measure过程,它没有重写onMeasure,它会调用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);
        }
    }
}

分别绘制child,进入measureChild:

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);
}

获取LayoutParams,通过getChildMeasureSpec来创建子无素的MeasureSpec,调用child.measure,因为ViewGroup有不同的特性,所以无法实现统一的onMeasure。

Layout的过程

viewGroup会遍历所有子元素并调用 其layout方法,layout方法来确定子元素的位置。viewgroup如下:

protected abstract void onLayout(boolean changed,
        int l , int t, int r, int b) ;

需要子类自己实现。看下view的layout:

public void layout(int l, int t , int r, int b) {
    if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT ) != 0) {
        onMeasure(mOldWidthMeasureSpec , mOldHeightMeasureSpec) ;
        mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
    }

    int oldL = mLeft;
    int oldT = mTop;
    int oldB = mBottom;
    int oldR = mRight;

    boolean changed = isLayoutModeOptical( mParent) ?
            setOpticalFrame(l, t , r, b) : setFrame(l, t , r, b);

    if (changed || ( mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED ) {
        onLayout(changed, l, t , r, b);
        mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;

        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnLayoutChangeListeners != null) {
            ArrayList<OnLayoutChangeListener> listenersCopy =
                    (ArrayList<OnLayoutChangeListener>)li. mOnLayoutChangeListeners .clone();
            int numListeners = listenersCopy.size() ;
            for ( int i = 0 ; i < numListeners; ++i) {
                listenersCopy.get(i).onLayoutChange( this, l, t, r , b, oldL, oldT , oldR, oldB);
            }
        }
    }

    mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
    mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}

在setFrame中确定了view的四个顶点坐标。mleft等。onLayout view也没有具体实现,要看具体的。以LinearLayout为例:

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);
    }
}

以layoutVertical为例:

void layoutVertical(int left, int top , int right, int bottom) {
    final int paddingLeft = mPaddingLeft ;

    int childTop ;
    int childLeft ;

    // Where right end of child should go
    final int width = right - left;
    int childRight = width - mPaddingRight ;

    // Space available for child
    int childSpace = width - paddingLeft - mPaddingRight ;

    final int count = getVirtualChildCount() ;

    final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
    final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;

    switch (majorGravity) {
       case Gravity.BOTTOM:
           // mTotalLength contains the padding already
           childTop = mPaddingTop + bottom - top - mTotalLength;
           break;

           // mTotalLength contains the padding already
       case Gravity.CENTER_VERTICAL:
           childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
           break;

       case Gravity. TOP:
       default :
           childTop = mPaddingTop;
           break;
    }

    for (int i = 0; i < count ; i++) {
        final View child = getVirtualChildAt(i) ;
        if (child == null) {
            childTop += measureNullChild(i);
        } else if (child.getVisibility() != GONE) {
            final int childWidth = child.getMeasuredWidth() ;
            final int childHeight = child.getMeasuredHeight();

            final LinearLayout.LayoutParams lp =
                    (LinearLayout.LayoutParams) child.getLayoutParams() ;

            int gravity = lp. gravity;
            if (gravity < 0) {
                gravity = minorGravity;
            }
            final int layoutDirection = getLayoutDirection() ;
            final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection) ;
            switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                case Gravity. CENTER_HORIZONTAL :
                    childLeft = paddingLeft + ((childSpace - childWidth) / 2)
                            + lp. leftMargin - lp.rightMargin ;
                    break;

                case Gravity.RIGHT:
                    childLeft = childRight - childWidth - lp. rightMargin;
                    break;

                case Gravity.LEFT:
                default:
                    childLeft = paddingLeft + lp. leftMargin;
                    break;
            }

            if (hasDividerBeforeChildAt(i)) {
                childTop += mDividerHeight;
            }

            childTop += lp.topMargin;
            setChildFrame(child , childLeft, childTop + getLocationOffset(child) ,
                    childWidth, childHeight);
            childTop += childHeight + lp. bottomMargin + getNextLocationOffset(child);

            i += getChildrenSkipCount(child , i);
        }
    }
}

主要看以下代码:

final int childWidth = child.getMeasuredWidth() ;
final int childHeight = child.getMeasuredHeight() ;
setChildFrame(child , childLeft, childTop + getLocationOffset(child) ,
        childWidth , childHeight);
childTop += childHeight + lp. bottomMargin + getNextLocationOffset(child);

top会逐渐增大,所以会往下排。setChildFrame仅仅是调用子元素的layout方法。

private void setChildFrame(View child, int left, int top , int width, int height) {       
    child.layout(left, top, left + width , top + height);
}

通过子元素的layout来确定自身。

draw过程

它有以下几步:

  • 绘制背景,(canvas)
  • 绘制自己。(onDraw)
  • 绘制children(dispatchDraw)
  • 绘制装饰(onDrawScrollBars) 
    看下view的draw源码:
public void draw(Canvas canvas) {
    final int privateFlags = mPrivateFlags;
    final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK ) == PFLAG_DIRTY_OPAQUE &&
            (mAttachInfo == null || !mAttachInfo .mIgnoreDirtyState );
    mPrivateFlags = (privateFlags & ~ PFLAG_DIRTY_MASK ) | PFLAG_DRAWN;

    /*
     * Draw traversal performs several drawing steps which must be executed
     * in the appropriate order:
     *
     *      1. Draw the background
     *      2. If necessary, save the canvas" layers to prepare for fading
     *      3. Draw view"s content
     *      4. Draw children
     *      5. If necessary, draw the fading edges and restore layers
     *      6. Draw decorations (scrollbars for instance)
     */

    // Step 1, draw the background, if needed
    int saveCount;

    if (!dirtyOpaque) {
        drawBackground(canvas);
    }

    // skip step 2 & 5 if possible (common case)
    final int viewFlags = mViewFlags;
    boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL ) != 0;
    boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL ) != 0;
    if (!verticalEdges && !horizontalEdges) {
        // Step 3, draw the content
        if (!dirtyOpaque) onDraw(canvas) ;

        // Step 4, draw the children
        dispatchDraw(canvas) ;

        // Step 6, draw decorations (scrollbars)
        onDrawScrollBars(canvas) ;

        if ( mOverlay != null && !mOverlay.isEmpty()) {
            mOverlay .getOverlayView().dispatchDraw(canvas) ;
        }

        // we"re done...
        return;
    }

viewgroup中的dispatchDraw用于遍历子view并调用子view的draw方法。这样就一层层的传下去。 
到此源码分析就结束了。在绘制view的时候经常会在activity中获得view的宽高,因为activity的生命周期和view不同步,在oncreate中无法获取到view的宽高,接下来讲讲activity中如何获取view。

三,view宽高确定

  • onWindowFocusChanged:view己经初始化完毕,宽高己经准备好。当Activity得到焦点和失去焦点均会被调用,所以它会调用多次。
  • 通过view.post,将一个runnable投弟到消息队列尾部,等待looper调用时,view己经初始化好。
protected void onStart(){
super.onStart();
view.post(new Runnable(){
public void run(){
int width = view.getMeasuredWidth();
int height = new .getMeasuredHeight();
}
})
}
  • ViewTreeObserver: 
    使用ViewTreeObserver众多回调接口来完成,如OnGlobalLayoutListener,当view树状态发生改变时或内部view可见性发生改变时会回调。
ViewObserver obserber = view.getViewObserver ();
obserber.addOnGlobalLayoutListener(new OnGlobalLayoutListener(){
public void onGlobalLayout(){
obserber.removeOnGlobalLayoutListener(this);
int width = view.getMeasuredWidth();
int height = new .getMeasuredHeight();
}
})
  • 通过view进行measure来得到view的宽高。
int width = MeasureSpec.makeMeasureSpec(100,Measure.EXACTLY);//确定值
int height= MeasureSpec.makeMeasureSpec(100,Measure.EXACTLY);//确定值
view.measure(width,height);
对于wrap_content:
int width = MeasureSpec.makeMeasureSpec((1<<30)-1,Measure.AT_MOST);//wrap_content
int height= MeasureSpec.makeMeasureSpec((1<<30)-1,Measure.AT_MOST);//wrap_content
view.measure(width,height);

四,自定义view中注意事项

自定义View需要注意的事项:

  • 如果是继承view或者viewGroup,让view支持wrap_content。

  • 如果有必要,让view支持padding。

  • view中如果有动画或者线程,要在onDetachedFromWindow中及时停止。当view的Activity退出或者当前view被remove时,调用它。