android View 绘制解析

624 阅读6分钟

在android开发过程中相信很多人对View树并不陌生,本文着重介绍android中view树是如何绘制。

什么是View树

下面通过一个图示简单描述View树
这里写图片描述
通过上述这个View树的绘制,我们大致能够猜想出整个View树的绘制过程其实是一个View的递归绘制。

View绘制主要是通过 measure、layout、draw这三个方法进行绘制的,而我们通过源码知道view绘制的实现主要是在ViewRootImpl该类中实现。在改类中View的绘制流程是从ViewRootImpl的performTraversals()方法开始的。
该方法太长,这里我只截取了其中关于View绘制流程的代码;

 private void performTraversals() {
        ....
        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
        ...
         performLayout(lp, desiredWindowWidth, desiredWindowHeight);
        ...
        performDraw();
        ...

}

下面我们先通过一个图是简单描述View绘制的大致流程

这里写图片描述

View 绘制流程第一步 measure

measure在View绘制中主要作用是用于测量,通过上述图示,我们知道measure是在perfromMeasure方法内进行调用的。那么我们先来看ViewRootImpl中 performMeasure中主要做了什么。

 private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
        try {
            mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
    }

在这个方法内其实是很简单,内部直接调用了 mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
可以看到具体的测量过程是在mView的measure方法内实现,该方法的具体实现可以看到,

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

        if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);

        if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
                widthMeasureSpec != mOldWidthMeasureSpec ||
                heightMeasureSpec != mOldHeightMeasureSpec) {

         .....

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

          ....
    }

通过上述这端代码我们可以看出,内部 cacheIndex < 0 || sIgnoreMeasureCache 条件成立的情况下会直接调用

onMeasure(widthMeasureSpec, heightMeasureSpec);

否则 调用

 setMeasuredDimensionRaw((int) (value >> 32), (int) value);

而对于View的测量我们知道实质上是调用了 onMeasure 这个方法;

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

在这个方法内部我们可以看到器内部的参数分别表示 measuredWidth, measuredHeight
由此我们可以看出View的测量是通过 getDefaultSize 方法实现的。下面我们来详细看一下 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;
    }

对于我们来将,我们只需要看AT_MOST、EXACTLY 这两种模式即可,通过上述的代码我们可以看出 getDefaultSize 的返回值其实就是measureSpec中的Specsize,而specsize 就是View测量之后的大小。
以上是View的绘制过程,对于ViewGroup 我们知道除了绘制自身之外,还会去变量其内部子view然后调用子view的measure方法进行绘制。

 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(child, widthMeasureSpec, heightMeasureSpec); 该方法,而该方法内部

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

我们可以看出其是直接调用了ViewGroup内部子view的measure方法。

至此View的测量(measure)方法我们大致了解。

下面我们以一个图示说明View的测量过程

这里写图片描述

View 绘制流程第二步 layout

layout的作用主要是ViewGroup用来确定子元素的位置,当ViewGroup位置确定之后它在onLayout方法中会遍历所有子元素并调用其layoug方法来确定View本身的位置。

下面我们通过api中的源码对layout过程进行详解。

我们知道当measure执行完之后会执行相应的layout过程。
在viewRootImpl类中我们在performLayout方法中可以看到

 private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
            int desiredWindowHeight) {
        ...
        final View host = mView;
         host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
         ...
}

内部直接执行来layout方法,在该方法内部,

public void layout(int l, int t, int r, int b) {
       ...
        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);
           ...
                }
            }
        }
        ..
    }

我们可以看到 changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED 当该判断成立时,内部其是是直接执行了 onLayout(changed, l, t, r, b);

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    }

该方法我们在View类中可以看到是一个空实现方法

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    }

同样在ViewGroup中

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

也是一个空实现方法,由此可见layout方法的具体实现是根据具体不同的布局实现的。
所以到此我们可以很清晰的了解,layout的执行过程与measure相似,但是也存在不同。

下面我们同样通过一个图示来理解layout的执行过程。

这里写图片描述

View 绘制第三步 draw

draw的作用主要是用来将view绘制到屏幕上,这个方法相对于measure以及layout 比较简单,下面我们来看一下android api中是如何实现draw方法的。
在ViewRootImpl类中

private void performDraw() {
    ...
      draw(fullRedrawNeeded);
      ...
}

之后在draw方法内

private void draw(boolean fullRedrawNeeded) {
....
 if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
                    return;
                }
        ....
   }

通过drawSoftware 方法内部我们可以看出内部直接调用了mView.draw(..)方法

private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
            boolean scalingRequired, Rect dirty) {
        ...
         mView.draw(canvas);
         ...
}

接下来我们只需要看mView中draw方法内部的具体作用

通过 draw方法,我们可以看出绘制view主要分这么几步,
1.绘制背景
2.绘制自己
3.绘制子元素
4.绘制装饰

public void draw(Canvas canvas) {

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

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

        ...
        if (!verticalEdges && !horizontalEdges) {
            // Step 2, draw the content
            if (!dirtyOpaque) onDraw(canvas);

            // Step 3, 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 4, draw decorations (foreground, scrollbars)
            onDrawForeground(canvas);

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


}

下面我们主要看第三步 是如何绘制 子元素。

protected void dispatchDraw(Canvas canvas) {

    }

我们可以看到在view类中dispatchDraw 方法实质是一个空实现,这里我们可以想到作为一个具体的view如果不是容器类型,那么它本身是没有子元素的,所以我们该方法的实现只会在容器类型的控件中,下面我们看容器类型元素的父类ViewGroup
在该类中我们找到了这个方法的具体实现。

    protected void dispatchDraw(Canvas canvas) {
    ....
      for (int i = 0; i < childrenCount; i++) {
            while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
                final View transientChild = mTransientViews.get(transientIndex);
                if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
                        transientChild.getAnimation() != null) {
                    more |= drawChild(canvas, transientChild, drawingTime);
                }
                transientIndex++;
                if (transientIndex >= transientCount) {
                    transientIndex = -1;
                }
            }
            int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i;
            final View child = (preorderedList == null)
                    ? children[childIndex] : preorderedList.get(childIndex);
            if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
                more |= drawChild(canvas, child, drawingTime);
            }
        }

}

通过上述代码我们其事可以看到,内部对容器的子元素进行遍历,同时调用了drawChild(…)该方法。

而在drawChild方法中;

protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        return child.draw(canvas, this, drawingTime);
    }

直接调用了 draw方法。

下面我们同样通过一个图示对draw方法对过程进行描绘,

这里写图片描述

至此本文对View的绘制流程已讲解完成,还望各位看官不吝赐教。