自定义View的绘制原理图解

来源:互联网 发布:淘宝店卖肉需要执照吗 编辑:程序博客网 时间:2024/05/17 04:38

每个人对于Android中的自定义View的测量、布局及绘制原理,都有自己独特的分析与看法,但终究的理论都是一样的。


  • 自定义控件的分类
    [1]通过系统提供的原生控件进行组合 来达到自定义的需求
    [2]
    • 定义一个类继承View
    • 定义一个类继承ViewGroup
      [3]View&ViewGroup特点
      • View 是构建页面的基本模块 view类在屏幕上占据的是一个矩形区域 职责:是绘制和事件的处理
      • ViewGroup:可以包裹孩子
        View的绘制过程就是从ViewRoot的performTraversals方法开始的,它经过measure、layout、draw三个过程才能最终将一个View绘制出来:

  1. measure用来测量View的宽和高。
  2. layout用来确定View在父容器中放置的位置。
  3. draw用来将view绘制在屏幕上。

  4. 那么,对于ViewRoot是什么呢?
    初始ViewRoot 与 DecorView
    ViewRoot对应于ViewRootImpl类,它是连接WindowManager和DecorView的纽带,View的三大流程均是通过ViewRoot来完成的,在ActivityThread中,当ActivityThread中,当Activity对象被创建完毕后,会将DecorView添加到Window中,同时会创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView建立关联。

那也许会问DecorView又是什么呢?
其实它就是Activity的顶级View

在Activity中加载布局使用的方法中setContentView,那个contentview就是我们这个DecroView中的子布局,View的点击事件都要先经过DecorView,然后再传递给我们的View。

一、View绘制的流程框架

这里写图片描述

performTraversals会依次调用performMeasure、performLayout和performDraw三个方法,这三个方法分别完成顶级View的measure、layout、draw这个三个流程。也就是说View的绘制是从上往下一层层迭代下来的。

其中:**1.perfromMeasure中会调用measure方法,在measure方法中又会调用onMeasure方法,在onMeasure方法中会对所有的子元素进行measure过程,这个时候measure流程就从父容器传递到子元素中了,这样就完成了一次measure过程。接着子元素会重复父元素的measure过程,如此反复就完成整个View树的遍历。

2.performLayout的传递流程和performMeasure是一样的。

3.performDraw的传递过程是在draw方法中通过dispathDraw来实现的,本质上并没有区别。

二、Measure流程

测量每一个控件的大小。
调用measure()方法,进行一些逻辑处理,然后调用onMeasure()方法,在其中调用setMeasuredDimension()设定View的宽高信息,完成View的测量操作。

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {}

measure()方法中,传入了两个参数 widthMeasureSpec, heightMeasureSpec 表示View的宽高的一些信息。

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

Measure过程决定了View的宽高,Measure完成以后,可以通过getMeasuredWidth和getMeasuredHeight方法来获取到View测量后的宽高,在几乎所有的情况下它都等于View的最终宽高,这仅仅是在代码规范的前提之下。
由上述流程来看Measure流程很简单,关键点是在于widthMeasureSpec, heightMeasureSpec这两个参数信息怎么获得?

如果有了widthMeasureSpec, heightMeasureSpec,通过一定的处理(可以重写,自定义处理步骤),从中获取View的宽/高,调用setMeasuredDimension()方法,指定View的宽高,完成测量工作。

MeasureSpec的理解:
MeasureSpec的中文意思是测量规格的意思,MeasureSpec代表一个32的int值,高2位代表SpecMode测量模式,低30位代表SpecSize测量规格大小。

经常使用的三个函数:
1.public static int makeMeasureSpec(int size,int mode)
构造一个MeasureSpec

2.public static int getMode(int measureSpec)
获取MeasureSpec的测量模式

3.public static int getSize(int measureSpec)
获取MeasureSpec的测量大小

由图可知其中MeasureSpec由两部分组成,一部分是测量模式,另一部分是测量的尺寸大小。
Mode的模式又分为三类

UNSPECIFIED :不对View进行任何限制,要多大给多大,一般用于系统内部

EXACTLY:对应LayoutParams中的match_parent和具体数值这两种模式。检测到View所需要的精确大小,这时候View的最终大小就是SpecSize所指定的值,

AT_MOST :对应LayoutParams中的wrap_content。View的大小不能大于父容器的大小。

那么MeasureSpec又是如何确定的?

对于DecorView,其确定是通过屏幕的大小,和自身的布局参数LayoutParams。

这部分很简单,根据LayoutParams的布局格式(match_parent,wrap_content或指定大小),将自身大小,和屏幕大小相比,设置一个不超过屏幕大小的宽高,以及对应模式。

对于其他View(包括ViewGroup),其确定是通过父布局的MeasureSpec和自身的布局参数LayoutParams。

MeasureSpec和LayoutParams的关系

在上面系统中,使用MeasureSpec来进行View的测量,但是正常情况下我们使用View指定MeasureSpec,尽管如此,但是我们可以给View设置layoutparams,在View测量的时候,系统会将LayoutParams在父容器的约束下自动转化成对应的MeasureSpec,然后再根据MeasureSpec来确定View最终的宽高。

MeasureSpec不是由LayoutParams唯一决定的,子View的宽高由自身的layoutparams和父容器的MeasureSpec。

对于DecorView,其MeasureSpec由窗口的尺寸和自身的Layoutparams决定;
对于普通的View,其MeasureSpec由父容器的MeasureSpec和自身的Layoutparams共同决定;

遵循的规则:

1.LayoutParams.MATCH_PARENT:精确模式,大小就是窗口的大小
2.LayoutParams.WRAP_CONTENT:最大模式,大小不定,但是不能超过窗口的大小
3.固定大小(比如100dp):精确模式,大小为LayoutParams中指定的大小
**4.当view采用固定的宽高时,不管父容器的MeasureSpec是什么,View的MeasureSpec都是精确模式并且大小遵循LayoutParams的大小
5.当View的宽高时matchparent时,如果父容器是精确模式,那么View也是精确模式,并且View也是精确模式并且大小是父容器的剩余空间。如果父容器是最大模式,那么View也是最大模式但是大小不能超过剩余的空间。
6.当View是wrap_content,那么不管父容器的模式,View一定是最大模式,但是不能超过父容器的剩余空间。**

总结:当子View的LayoutParams的布局格式是wrap_content,可以看到子View的大小是父View的剩余尺寸,和设置成match_parent时,子View的大小没有区别。为了显示区别,一般在自定义View时,需要重写onMeasure方法,处理wrap_content时的情况,进行特别指定。

从这里看出MeasureSpec的指定也是从顶层布局开始一层层往下去,父布局影响子布局。

三、View的工作流程

View的工作流程主要是指measure、layout、draw这三个流程,即测量、布局、绘制,其中measure确定View的测量宽高,layout确定View的最终宽高和上下左右的位置,draw将View绘制到屏幕上。
清晰View测量流程

Measure过程决定了View的宽高,Measure完成以后,可以通过getMeasuredWidth和getMeasuredHeight方法来获取到View测量后的宽高,在几乎所有的情况下它都等于View的最终宽高,这仅仅是在代码规范的前提之下。

layout最终决定了View的四个顶点的坐标和实际View的宽/高,完成以后,可以通过getTop、getBottom、getLeft、getRight来拿到View的四个顶点坐标位置,并可以通过getWidth和getHeight来得到View的最终宽高

draw过程决定了View的显示,只有draw方法完成以后View的内容才会最终显示在屏幕上。

①measure

measure过程有时分两种情况

  • 针对View的measure
  • 针对ViewGroup的measure

1.针对View的measure
View的measure过程由其meaure方法完成,measure方法是一个final类型的方法,子类不可以重写此方法,因为View的measure内部会调用onMeasure方法

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

里面就是将getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec)获取到的宽和高赋给View。
再看一下getDefalutSize的代码实现

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 getSuggestedMinimumWidth() {        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());    }

其中,如果当前的模式为测量模式为UnSpecified,那么View的宽高就是传递过来Size的大小,如果测量模式为AT_MOST或者EXACTLY,那么View的宽高就是SpecSize的大小。也就是说View的内部将matchparent和wrapcontent是一样处理的,没有区别,所以这就是我们自定义View时,要自己处理wrapcontent的原因。

结论:直接继承View的自定义控件需要重写onMeasure方法并设置wrap_content时自身的大小,否则在布局中使用wrap_content就相当于使用match_parent。matchparent使用的是父View剩余的控件。

解决方法:重写View的onMeasure方法

@Override    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {        super.onMeasure(widthMeasureSpec, heightMeasureSpec);        int measuredWidth = 你自己计算出来的宽;        int measureHeight = 你自己计算出来的高;        int widthSpaceSize = MeasureSpec.getSize(widthMeasureSpec);        int widthSpaceMode = MeasureSpec.getMode(widthMeasureSpec);        int heightSpaceSize = MeasureSpec.getSize(heightMeasureSpec);        int heightSpaceMode = MeasureSpec.getMode(heightMeasureSpec);       if (heightSpaceMode == MeasureSpec.AT_MOST && widthSpaceMode == MeasureSpec.AT_MOST) {            setMeasuredDimension(measuredWidth, measureHeight);        } else if (heightSpaceMode == MeasureSpec.AT_MOST) {            setMeasuredDimension(widthSpaceSize, measureHeight);        } else if (widthSpaceMode == MeasureSpec.AT_MOST) {            setMeasuredDimension(measuredWidth, heightSpaceSize);        }    }

所以只需要判断measureSpec的mode是不是AT_MOST,如果是就我们自己计算好的值赋给View。

2.针对ViewGroup的measure过程
和View不同的是,ViewGroup是一个抽象类,因此它没有重写View的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);            }        }    }

可以看出measureChild的思想就是取出子元素的LayoutParams,然后再通过getChildMeasureSpec来创建子元素的MeasureSpec,接着将MeasureSpec传递给View的measure方法进行测量。

总结:
1.View的onmeasure在measure中,onmeasure中处理wrap_content和padding。
2.ViewGroup的onmeasure在measure中,viewgroup除了要测量自身的宽高,还要通过measureChild测量子View的宽高,只有里面子View的宽高确定了,才好计算Viewgroup的宽高。
3.注意MeasureSpec和layoutParams的对应关系。

②Layout
layout的作用是ViewGroup用来确定子元素的位置,当ViewGroup的位置被确定后,它在onLayout中会遍历所有的子元素并调用Layout方法,在layout方法中onLayout方法又会被调用。这样一层一层的完成所有子View的布局过程。
layout里面通过setFrame来确定View的四个顶点的位置,然后再layout中调用onLayout,这个方法用来确定子View的位置,起到一层层传递的作用。

public void layout(int l, int t, int r, int b) {      // 当前视图的四个顶点    int oldL = mLeft;      int oldT = mTop;      int oldB = mBottom;      int oldR = mRight;      // setFrame() / setOpticalFrame():确定View自身的位置    // 即初始化四个顶点的值,然后判断当前View大小和位置是否发生了变化并返回   boolean changed = isLayoutModeOptical(mParent) ?            setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);    //如果视图的大小和位置发生变化,会调用onLayout()    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {          // onLayout():确定该View所有的子View在父容器的位置             onLayout(changed, l, t, r, b);        ...}

③draw操作
draw的过程也比较简单,它的作用是将View绘制到屏幕上面。View的绘制过程遵循下面:

1.绘制背景 (drawBackground)
2.绘制自己 (onDraw(canvas))
3.绘制children ( dispatchDraw(canvas))
4.绘制装饰 onDrawForeground(canvas);

@CallSuper    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);            // 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);    }

无论是ViewGroup还是单一的View,都需要实现这套流程,不同的是,在ViewGroup中,实现了 dispatchDraw()方法,而在单一子View中不需要实现该方法。自定义View一般要重写onDraw()方法,在其中绘制不同的样式。

四、总结

从View的测量、布局和绘制原理来看,要实现自定义View,根据自定义View的种类不同,可能分别要自定义实现不同的方法。但是这些方法不外乎:onMeasure()方法onLayout()方法onDraw()方法

onMeasure()方法:单一View,一般重写此方法,针对wrap_content情况,规定View默认的大小值,避免于match_parent情况一致。ViewGroup,若不重写,就会执行和单子View中相同逻辑,不会测量子View。一般会重写onMeasure()方法,循环测量子View。

onLayout()方法:单一View,不需要实现该方法。ViewGroup必须实现,该方法是个抽象方法,实现该方法,来对子View进行布局。

onDraw()方法:无论单一View,或者ViewGroup都需要实现该方法,因其是个空方法

嗯,对于自定义View的绘制原理都透彻明了了,相信对大家回头看的时候还有些看得见的理论可去弥补。

可参考博客
借鉴博客
资料提供