Android开发进阶—View的工作原理

1.前言

       在Android的体系中View扮演者很重要的角色,虽然说View不属于Android的四大控件,但是它的作用和四大组件一样重要。简单的说,View是Android在视觉上的呈现,系统给我们提供了一套GUI库,里面有很多控件我们可以直接使用,比如:TextView、EditText、Button等,也可以进行自定义后使用。

2.View的绘制流程

     首先我们通过一张图看看Activity的控件架构图

       

       从上面的图可以看出,DecorView作为顶级View,一般情况下它的内部都会包含一个竖直方向的LinearLayout,在这个LinearLayout中又分为上下两部分,上面的TitleView时标题栏,下面的ContentView是内容栏,内容栏的id是content,我们在Activity中通常使用setContentView()方法所设置的布局文件,其实就是将布局文件加入到内容栏中显示出来,这样很好的解释了为什么使用requestWindowFeature(Window.FEATURE_NO_TITLE)方法去除标题栏为什么需要在setContentView()方法之前才会有效果。

       在Activity的onCreate()方法中还可以通过ViewGroup content  = (ViewGroup)findViewById(android.R.id.content)得到ContentView,通过content.getchildAt(0)得到其中的一个View,View层的事件都是先经过DecorView,然后才能传递给View。

       ViewRoot对应于ViewRootImpl类,它是连接WindowManager和DecorView的纽带,View的三大流程均是通过ViewRoot来完成的。View的绘制流程是从ViewRoot的performTraversals方法开始的,它经过measure、layout和draw三个过程才能最终将一个View绘制出来。其中measure是用来测量View的宽和高,layout是用来确定View在父容器中的放置位置,而draw则负责将View绘制在屏幕上。

       Measure过程:这一过程决定了View的宽和高,在测量结束后,可以通过getMeasuredWidth和getMeasuredHeight的方法来获取到View测量后的宽和高(注:这时的宽和高基本上等同于View最终的宽和高)。

       Layout过程:这一过程在Measure过程完成以后,layout过程决定了View的四个顶点的坐标和实际View的宽和高,在layout结束之后,可以通过getTop、getBottom、getLeft和getRight来得到View的四个顶点的位置,并且可以通过getWidth和getHeight来获取到View最终的宽和高(注:measure中得到的宽和高和layout中得到的宽和高几乎是一样的,但是也有可能不一样)。

       draw过程:这一过程在Layout过程完成之后,draw过程决定了View的显示,只有这一过程完成以后View的内容才能显示在屏幕上。

3.Measure中View的宽高和和Layout中View的宽高的区别

       在上面我们说到过,Measure过程中通过getMeasuredWidth/getMeasuredHeight的方法来获取到View的宽和高与Layout过程中通过getWidth和getHeight来获取到View最终的宽和高是有可能不一样的。

       想要知道这里的原因我们就必须更深入的去了解View的测量过程,在这里还必须要了解到MeasureSpec这个类,从名字上看MeasureSpec可以被翻译成为“测量规格”或者“测量说明书”。其实MeasureSpec代表的是一个32位的int类型的值,高2位代表的是SpecMode(测量模式),低30位代表SpecSize(在某种测量模式下的规格大小),MeasureSpec通过将SpecMode和SpecSize打包成一个int值来避免过多的内存对象分配。

       SpecMode有三种类型,每种类型都代表一种特殊的含义:

  •  UNSPECIFIED—父容器对View没有任何限制,要多大给多大
  •  EXACTLY—父容器已经检测出View所需要的精确大小,这时View的最终大小就是SpecSize所指定的值
  •  AT_MOST—父容器指定了一个可用大小,View的大小不能大于这个值
       在Measure的过程中系统会将View的LayoutParams在父容器的约束下转换成对应的MeasureSpec,然后再根据这个measureSpec来测量出View的宽/高,需要注意的是,MeasureSpec不是唯一由LayoutParams决定的,LayoutParams需要和父容器一起才能决定View的MeasureSpec从而进一步决定View的宽/高。
       LayoutParams中的宽/高的参数有三种模式,每种类型都代表一种含义:
  • LayoutParams.MATCH_PARENT—精确模式,大小就是窗口的大小
  • LayoutParams.WRAP_CONTENT—最大模式,大小不定但是不能超过窗口的大小
  • 固定大小—精确模式,大小位LayoutParams中指定的大小
       
//getWidth和getHeight的源码
public final int getWidth()
{
    return mRight-mLeft;
}

public final int getHeight()
{
    return mBottom-mTop;
}

       在Layout过程中,从getWidth和getHeight的源码可以来出来,getWidth的返回值刚好就是View的测量宽度,getHeight的返回值刚好就是View的测量高度,从这两个方法的实现我们可以知道,在View的默认实现中,View的测量宽/高和最终宽/高是相等的,只不过测量宽/高形成于measure过程,而最终宽/高形成于 layout过程,即两者的赋值时机不同,measure过程中的宽/高赋值时机稍微早一些,因此在日常的开发中我们可以认为View的测量宽/高和最终宽/高是相同的,但是的确会存在某些特殊的情况导致两者不一致。比如重写View的layout方法:
public void layout(int l, int t, int r, int b)
{
    super.layout(l, t, r + 100, b + 100);
}
       上述代码会导致View的最终宽/高比测量宽/高均大100px。

4.measure过程

       在进行measure过程时需要分两种情况来看待,如果只是一个原始的View,那么通过measure方法就可以完成测量过程,如果是一个ViewGroup在进行自身的测量过程中还会去遍历调用所有元素的measure方法,其各个子元素再递归去执行这个流程。

1)ViewGroup的measure过程

       ViewGroup在完成自身的测量外还会通过遍历去调用所有子元素的measure方法,各个子元素再递归去执行这个过程。ViewGroup提供了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);
            }
        }
    }
        通过上述代码可以看出,ViewGroup在进行measure时,是通过measureChild方法来对每一个子元素进行measure的,接下来就看看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);
}
       通过上述代码可以看出,measureChild方法中总共做了三件事:

       1、首先,通过child.getLayoutParams()方法取出当前子元素的LayoutParams

       2、其次,通过getChildMeasureSpec()方法分别获取childWidthMeasureSpec和childHeightMeasureSpec

       3、最后,通过child.measure()方法将childWidthMeasureSpec和childHeightMeasureSpec传递给当前子元素的measure进行测量

2)View的measure过程

       当ViewGroup将childWidthMeasureSpec和childHeightMeasureSpec传递给当前子元素的measure进行测量后,系统就开始对当前View进行测量,View的测量过程是由其measure方法来完成的,在measure方法中又会去调用View的onMeasure方法。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
    getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
       在onMeasure方法中是通过setMeasuredDimension方法来设置View宽/高的测量值,因此在这里我们只需要看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;
}
        通过上述代码可以看出这里需要根据测量模式进行相应的赋值操作,在UNSPECIFIED状态时,父容器不对View有任何限制,通常是要多大给多大,在AT_MOST 状态和EXACTLY状态时,这时候View的大小都是specSize指定的值,这两个状态分别对应于LayoutParams中的match_parent和wrap_content。

       从getDefaultSize的方法源码可以看出,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 && heightMeasureSpec == MeasureSpec.AT_MOST)
    {
        //mWidth/mHeight为用户默认指定的宽/高
        setMeasuredDimension(mWidth, mHeight);
    }
    else if(widthSpecMode == MeasureSpec.AT_MOST)
    {
        setMeasuredDimension(mWidth, heightSpecSize);
    }
    else if(heightMeasureSpec == MeasureSpec.AT_MOST)
    {
        setMeasuredDimension(widthSpecMode, mHeight);
    }
}

       在上述代码中我们只需要在wrap_content的情况下给View一个指定的内部宽/高(mWidth/mHeight),对于非wrap_content的情况下,我们沿用系统的测量值即可,至于默认内部宽/高的数值如何指定,没有固定的依据,只要按照当前情况灵活指定即可。

4.layout过程

       ViewGroup是通过Layout来确定子元素的位置,当ViewGroup的位置被确定后,在ViewGroup的onLayout方法中会遍历所有的子元素并调用其layout方法,在子元素的layout方法中又会去调用它的onLayout方法。

    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:
	 * Draw方法必须以适当的顺序遍历执行几个绘图步骤:
         *
         *      1. Draw the background
	 *	1. 绘制背景
         *      2. If necessary, save the canvas' layers to prepare for fading
	 *	2. 如果需要,保存Canvas层并且准备褪色
         *      3. Draw view's content
	 *	3. 绘制View自身
         *      4. Draw children
	 *	4. 绘制children
         *      5. If necessary, draw the fading edges and restore layers
	 *	5. 如果需要,绘制出边缘和恢复层
         *      6. Draw decorations (scrollbars for instance)
	 *	6. 绘制装饰(例如:滚动条)
         */

        // 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
	// 第二步,保存Canvas层
        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
	// 第三步,绘制View自身
        if (!dirtyOpaque) onDraw(canvas);

        // Step 4, draw the children
	// 第四步,绘制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);
    }
       从上述代码中可以看出来,View的绘制过程主要时通过dispatchDraw来实现的,dispatchDraw会遍历并且调用所有子元素的draw方法,这样draw事件就一层层传递下去了。










  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值