《Android群英传》笔记4——View绘制流程分析及重写

来源:互联网 发布:网络看江苏教育频道 编辑:程序博客网 时间:2024/05/17 03:45

 

View的绘制流程分析与重写

本文是读了《Android 群英传》第三章--Android体控件架构与自定义空间详解--之后的读书笔记,感谢作者,在此特别推荐此书。

 

  从上篇博客,我们了解到Android里View是所有UI组件的基类,Android里所有控件和布局都是直接或者间接自View实现的。View的绘制就如我们日常画画一样,首先需知道画画内容的大小,然后知道它的位置,最后才开始画,于是再代码里,我们将这三个过程分为了三个阶段,如图所示:


  这三个阶段的意义就是:

  1. measure: 判断是否需要重新计算View的大小,需要的话则计算;
  2. layout: 判断是否需要重新计算View的位置,需要的话则计算;
  3. draw: 判断是否需要重新绘制View,需要的话则重绘制。
阶段一:Measure测量阶段


  测量阶段主要是测量控件树里各个控件为实现控件里所有内容,所需的宽和高。
  测量里我们可以用MeasureSpec类来进行View的测量。MeasureSpec是一个32位int,由SpecMode和SpecSize两部分组成,其中,高2位为SpecMode,低30位为SpecSize。SpecMode为测量模式,SpecSize为相应测量模式下的测量尺寸。
  测量的模式有三种:

  • EXACTLY: 精确值模式。当我们将控件的layout_width或layout_height属性设置为具体数值,比如android:layout_width=“50dp”时候;或者指定为match_parent属性(此时的值为父View的大小)时候,用的是该模式。此时表明对该控件提出了一个确切的建议尺寸(SpecSize);
  • AT_MOST:最大值模式。当我们把控件的layout_width或layout_height属性设置为wrap_content时候,该控件的大小会随着子控件的变化而变化,但大小不能超过其父控件的大小。此时表明对该控件的大小不得超过SpecSize;
  • UNSPECIFIED: 对该控件的尺寸不作限制,通常用于系统内部。  

  View类里默认的onMeasure()方法里支持EXACTLY模式,所以当我们自定义采用其他模式时候,就必须重写onMeasure()方法。

  首先我们看看Android里measure方法的源码:

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {    boolean optical = isLayoutModeOptical(this);    if (optical != isLayoutModeOptical(mParent)) {        Insets insets = getOpticalInsets();        int oWidth  = insets.left + insets.right;        int oHeight = insets.top  + insets.bottom;        widthMeasureSpec  = MeasureSpec.adjust(widthMeasureSpec,  optical ? -oWidth  : oWidth);        heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);    }    // Suppress sign extension for the low bytes    long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;    if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);    if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||            widthMeasureSpec != mOldWidthMeasureSpec ||            heightMeasureSpec != mOldHeightMeasureSpec) {        // first clears the measured dimension flag        mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;        resolveRtlPropertiesIfNeeded();        int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :                mMeasureCache.indexOfKey(key);        if (cacheIndex < 0 || sIgnoreMeasureCache) {            // measure ourselves, this should set the measured dimension flag back            onMeasure(widthMeasureSpec, heightMeasureSpec);            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;        } else {            long value = mMeasureCache.valueAt(cacheIndex);            // Casting a long to int drops the high 32 bits, no mask needed            setMeasuredDimensionRaw((int) (value >> 32), (int) value);            mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;        }        // flag not set, setMeasuredDimension() was not invoked, we raise        // an exception to warn the developer        if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {            throw new IllegalStateException("View with id " + getId() + ": "                    + getClass().getName() + "#onMeasure() did not set the"                    + " measured dimension by calling"                    + " setMeasuredDimension()");        }        mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;    }    mOldWidthMeasureSpec = widthMeasureSpec;    mOldHeightMeasureSpec = heightMeasureSpec;    mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |            (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension}
  可以看出,当父控件的大小发生变化时候,就会调用onMeasure()方法。其源码如下:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));}
  setMeasureDinmension里主要是进行了一种特殊情况的判断,然后将长宽赋值,所以核心还是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;}
  由源码这样我们也就可以得到为啥默认的是EXACTLY了。
  所以如果我们要重写获取其他两种模式的话就可以通过继承View并重写onMeasure()方法:

@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {    super.onMeasure(widthMeasureSpec, heightMeasureSpec);    int widthSize = getWidthSize(widthMeasureSpec);    int heightSize = getHeightSize(heightMeasureSpec);    setMeasuredDimension(widthSize, heightSize);}

  获取长宽的方法如下:

private int getHeightSize(int heightMeasureSpec){    int mode = MeasureSpec.getMode(heightMeasureSpec);    int height = 0;    switch (mode) {        case MeasureSpec.AT_MOST:            height = textSize + scaleSpaceText + scaleHeight;            break;        case MeasureSpec.EXACTLY:{            height = MeasureSpec.getSize(heightMeasureSpec);            break;        }        case MeasureSpec.UNSPECIFIED:{            height = Math.max(textSize + scaleSpaceText + scaleHeight, MeasureSpec.getSize(heightMeasureSpec));            break;        }    }    return height;}
      getWidth的方法与之类似,这里我使用的参数textSize、scalSpaceText和scaleHeight分别是我需要设置的三个高度,int格式。这个可以根据自己情况控制下。


阶段二:Layout布局阶段


 布局阶段主要是根据子视图的大小以及布局参数将View树放到合适的位置上。
  layout方法会设置该View视图位于父视图的坐标轴,即mLeft,mTop,mLeft,mBottom(调用setFrame()函数去实现),接下来回调onLayout()方法(如果该View是ViewGroup对象,需要实现该方法,对每个子视图进行布局) ;
  如果该View是个ViewGroup类型,需要遍历每个子视图chiildView,调用该子视图的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;}
  这个没太多说的,值的注意的是layout里的参数l、t、r、b指的分别是本View控件左侧和父控件左侧的距离、本View控件顶部和父控件顶部的距离、本View控件右侧和父控件左侧的距离和本View控件底部和父控件顶部的距离。


阶段三:Draw绘制阶段  


  绘制阶段会调用draw()方法绘制View树里需要重新绘制的View控件,这些控件用一个标志位DRAWN进行判断,当该View需要重新绘制时候,会添加该标志位。
  draw方法的流程源码里也给出,如下所示:

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)*/

  具体源码如下:

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;    // 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);        // Overlay is part of the content and draws beneath Foreground        if (mOverlay != null && !mOverlay.isEmpty()) {            mOverlay.getOverlayView().dispatchDraw(canvas);        }        // Step 6, draw decorations (foreground, scrollbars)        onDrawForeground(canvas);        // we're done...        return;    }    /*     * Here we do the full fledged routine...     * (this is an uncommon case where speed matters less,     * this is why we repeat some of the tests that have been     * done above)     */    boolean drawTop = false;    boolean drawBottom = false;    boolean drawLeft = false;    boolean drawRight = false;    float topFadeStrength = 0.0f;    float bottomFadeStrength = 0.0f;    float leftFadeStrength = 0.0f;    float rightFadeStrength = 0.0f;    // Step 2, save the canvas' layers    int paddingLeft = mPaddingLeft;    final boolean offsetRequired = isPaddingOffsetRequired();    if (offsetRequired) {        paddingLeft += getLeftPaddingOffset();    }    int left = mScrollX + paddingLeft;    int right = left + mRight - mLeft - mPaddingRight - paddingLeft;    int top = mScrollY + getFadeTop(offsetRequired);    int bottom = top + getFadeHeight(offsetRequired);    if (offsetRequired) {        right += getRightPaddingOffset();        bottom += getBottomPaddingOffset();    }    final ScrollabilityCache scrollabilityCache = mScrollCache;    final float fadeHeight = scrollabilityCache.fadingEdgeLength;    int length = (int) fadeHeight;    // clip the fade length if top and bottom fades overlap    // overlapping fades produce odd-looking artifacts    if (verticalEdges && (top + length > bottom - length)) {        length = (bottom - top) / 2;    }    // also clip horizontal fades if necessary    if (horizontalEdges && (left + length > right - length)) {        length = (right - left) / 2;    }    if (verticalEdges) {        topFadeStrength = Math.max(0.0f, Math.min(1.0f, getTopFadingEdgeStrength()));        drawTop = topFadeStrength * fadeHeight > 1.0f;        bottomFadeStrength = Math.max(0.0f, Math.min(1.0f, getBottomFadingEdgeStrength()));        drawBottom = bottomFadeStrength * fadeHeight > 1.0f;    }    if (horizontalEdges) {        leftFadeStrength = Math.max(0.0f, Math.min(1.0f, getLeftFadingEdgeStrength()));        drawLeft = leftFadeStrength * fadeHeight > 1.0f;        rightFadeStrength = Math.max(0.0f, Math.min(1.0f, getRightFadingEdgeStrength()));        drawRight = rightFadeStrength * fadeHeight > 1.0f;    }    saveCount = canvas.getSaveCount();    int solidColor = getSolidColor();    if (solidColor == 0) {        final int flags = Canvas.HAS_ALPHA_LAYER_SAVE_FLAG;        if (drawTop) {            canvas.saveLayer(left, top, right, top + length, null, flags);        }        if (drawBottom) {            canvas.saveLayer(left, bottom - length, right, bottom, null, flags);        }        if (drawLeft) {            canvas.saveLayer(left, top, left + length, bottom, null, flags);        }        if (drawRight) {            canvas.saveLayer(right - length, top, right, bottom, null, flags);        }    } else {        scrollabilityCache.setFadeColor(solidColor);    }    // Step 3, draw the content    if (!dirtyOpaque) onDraw(canvas);    // Step 4, draw the children    dispatchDraw(canvas);    // Step 5, draw the fade effect and restore layers    final Paint p = scrollabilityCache.paint;    final Matrix matrix = scrollabilityCache.matrix;    final Shader fade = scrollabilityCache.shader;    if (drawTop) {        matrix.setScale(1, fadeHeight * topFadeStrength);        matrix.postTranslate(left, top);        fade.setLocalMatrix(matrix);        p.setShader(fade);        canvas.drawRect(left, top, right, top + length, p);    }    if (drawBottom) {        matrix.setScale(1, fadeHeight * bottomFadeStrength);        matrix.postRotate(180);        matrix.postTranslate(left, bottom);        fade.setLocalMatrix(matrix);        p.setShader(fade);        canvas.drawRect(left, bottom - length, right, bottom, p);    }    if (drawLeft) {        matrix.setScale(1, fadeHeight * leftFadeStrength);        matrix.postRotate(-90);        matrix.postTranslate(left, top);        fade.setLocalMatrix(matrix);        p.setShader(fade);        canvas.drawRect(left, top, left + length, bottom, p);    }    if (drawRight) {        matrix.setScale(1, fadeHeight * rightFadeStrength);        matrix.postRotate(90);        matrix.postTranslate(right, top);        fade.setLocalMatrix(matrix);        p.setShader(fade);        canvas.drawRect(right - length, top, right, bottom, p);    }    canvas.restoreToCount(saveCount);    // Overlay is part of the content and draws beneath Foreground    if (mOverlay != null && !mOverlay.isEmpty()) {        mOverlay.getOverlayView().dispatchDraw(canvas);    }    // Step 6, draw decorations (foreground, scrollbars)    onDrawForeground(canvas);}
  由源码可见,要想在Android的界面中绘制相应的图像,就必须在Canvas上进行绘制,Canvas就像是一个画板,使用画笔Paint就可以是在上面做花了。通常需要通过继承View并重写它的onDraw()方法,如下所示:

@Overrideprotected void onDraw(Canvas canvas) {    super.onDraw(canvas);    mPaint = new Paint();//画笔    int width = scaleWidth ;    mPaint.setColor(textColor);    mPaint.setAntiAlias(true);    mPaint.setTextSize(textSize);    mPaint.setTypeface(Typeface.DEFAULT_BOLD);    float textWidth = mPaint.measureText(text);    canvas.drawText(text, (width - textWidth) / 2, textSize, mPaint);    Rect scaleRect = new Rect(0, textSize + scaleSpaceText, width, textSize + scaleSpaceText + scaleHeight);    drawNinepath(canvas, R.drawable.icon_scale, scaleRect);}
private void drawNinepath(Canvas canvas, int resId, Rect rect){    Bitmap bmp= BitmapFactory.decodeResource(getResources(), resId);    NinePatch patch = new NinePatch(bmp, bmp.getNinePatchChunk(), null);    patch.draw(canvas, rect);}
  如此,便完成了一个View的绘制流程




1 0