从Android源码分析View绘制流程

《从Android源码分析View绘制流程》 Android View

丰富的View类型是Android手机的一大亮点,我们每天都在跟View打交道,了解View的绘制流程有助于我们更好的布局,以及实现漂亮高效的自定义View。本文将结合Android源码讲解View的绘制流程,不会拘泥于细节,主要是为了提供一个流程上的认识。

关键路径摘要

ViewRootImpl->performTraversals->performMeasure->performLayout->performDraw

performMeasure–>view.measure–>view.onMeasure
performLayout–>view.layout–>view.onLayout
performDraw–>view.draw–>view.onDraw

ViewRootImpl.java

public final class ViewRootImpl implements ... {
    final Rect mWinFrame; // frame given by window manager.
    ...
    void doTraversal() {
        ...
        performTraversals();
        ...
    }  

    private void performTraversals() {
        ...
        WindowManager.LayoutParams lp = mWindowAttributes;
        ...
        Rect frame = mWinFrame; // 来源看handleMessage函数
        ...
        if (mWidth != frame.width() || mHeight != frame.height()) {    
            mWidth = frame.width();    
            mHeight = frame.height();
        }
        ...
        // getRootMeasureSpec定义了如何根据父窗口的宽高和view本身的layout方式来获取view的实际宽高
        int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
        int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);

        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
        ...
        performLayout(lp, desiredWindowWidth, desiredWindowHeight);
        ...
        performDraw();
        ...
    }

    // 定义了如何根据父窗口的宽高和view本身的layout方式来获取view的实际宽高
    // 这里只可能返回AT_MOST或EXACTLY两种类型
    private static int getRootMeasureSpec(int windowSize, int rootDimension) {    
        int measureSpec;    
        switch (rootDimension) {    
            case ViewGroup.LayoutParams.MATCH_PARENT:        
            // 如果view的layout是MATCH_PARENT方式, 就返回父窗口的宽高       
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);        
            break;    
            case ViewGroup.LayoutParams.WRAP_CONTENT:        
            // 如果view的layout是WRAP_CONTENT方式,则宽高随内容而变,上限是父窗口的宽高        
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);        
            break;    
            default:        
            // 其他情况就是view指定固定值的宽高      
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);        
            break;    
        }    
        return measureSpec;
    }

    @Override
    public void handleMessage(Message msg) {    
        switch (msg.what) {
            ...
            case MSG_RESIZED_REPORT:    
                if (mAdded) {
                    SomeArgs args = (SomeArgs) msg.obj;
                    mWinFrame.set((Rect) args.arg1);
                    ...
                    requestLayout();
                } break;
            case MSG_WINDOW_MOVED:    
                if (mAdded) {        
                    final int w = mWinFrame.width();        
                    final int h = mWinFrame.height();        
                    final int l = msg.arg1;        
                    final int t = msg.arg2;        
                    mWinFrame.left = l;        
                   mWinFrame.right = l + w;        
                    mWinFrame.top = t;        
                    mWinFrame.bottom = t + h;        
                    if (mView != null) {            
                        forceLayout(mView);        
                    }        
                    requestLayout();    
                } break;
                ...
            }
            ...
        }

    private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {    
        ...
        try {        
            mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);    
        } finally {        
            ...
        }
    }
    ...
}

makeMeasureSpec返回的是一个MeasureSpec类型的int值,该值是一个包含了view的size和mode的复合数据,通过int值的高位和低位分别存储size和mode。

View.java

public static class MeasureSpec {
      ...
    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);
    }
    ...
}

performMesaure函数调用了View的measure方法,measure函数里面调用onMeasure函数以父窗口分配的宽高限制为基础,计算view自身的宽高,View的子类必须重写onMeasure函数以完成特定类别view的宽高测量。接下来看看onMeasure的实现。

View.java

@UiThread
public class View implements Drawable.Callback, 
        KeyEvent.Callback, AccessibilityEventSource {
    ...
    // 以父窗口分配的宽高限制为基础,计算view自身的宽高。两个参数分别是父窗口的宽高
    public final void measure(int widthMeasureSpec, int heightMeasureSpec){
        ...
        // view宽高的实际计算通过onMeasure函数完成,所以View的子类必须重写onMeasure函数以完成特定类别view的宽高测量
        onMeasure(widthMeasureSpec, heightMeasureSpec);
        ...
    }

    // 计算view的实际宽高,View的子类必须重写onMeasure函数以完成特定类别view的宽高测量
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {    
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), 
                widthMeasureSpec), 
                getDefaultSize(getSuggestedMinimumHeight(), 
                heightMeasureSpec));
    }
    ...
}

onMeasure首先调用getDefaultSize函数两次,分别获取measuredWidth和measuredHeight的默认值,然后调用setMeasuredDimension函数设置这两个值。measuredWidth和measuredHeight返回的默认值就是父窗口的宽高。

public static int getDefaultSize(int size, int measureSpec) {    
    int result = size;    
    int specMode = MeasureSpec.getMode(measureSpec);    
    int specSize = MeasureSpec.getSize(measureSpec);    
    // 由于前面getRootMeasureSpec返回的只可能是AT_MOST或EXACTLY,
    // 所以getDefaultSize返回的只可能是specSize,也就是父窗口的宽高
    switch (specMode) {    
        case MeasureSpec.UNSPECIFIED:        
            result = size;        
            break;    
        case MeasureSpec.AT_MOST:    
        case MeasureSpec.EXACTLY:        
            result = specSize;        
            break;    
    }    
    return result;
}

自定义view在onMeasure函数里必须调用setMeasureDimension来设置measuredWidth和measuredHeight,否则会抛IllegalStateException异常。

protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {    
    boolean optical = isLayoutModeOptical(this);    
    // isLayoutModeOptical判断android:layoutMode=“clipBounds”或是“opticalBounds”
    // 前者展示上控件间有少许留白,后者没有。默认是clipBounds,一般不会使用opticalBound
    // if的条件基本不会成立,所以基本不会执行
    if (optical != isLayoutModeOptical(mParent)) {        
        Insets insets = getOpticalInsets();        
        int opticalWidth  = insets.left + insets.right;        
        int opticalHeight = insets.top  + insets.bottom;        

        measuredWidth  += optical ? opticalWidth  : -opticalWidth;        
        measuredHeight += optical ? opticalHeight : -opticalHeight;    
    }    
    setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}

private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {    
    mMeasuredWidth = measuredWidth;    
    mMeasuredHeight = measuredHeight;    

    mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}

performMeasure分析完毕,接下来看performLayout。

ViewRootImpl.java

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

performLayout调用了View的layout函数指定每个view的大小和位置,具体的实现通过调用onLayout函数进行。基类View里的onLayout实现为空,子View需要重写onLayout函数实现layout逻辑。

View.java

public void layout(int l, int t, int r, int b) {
    ...
    onLayout(changed, l, t, r, b);
    ...
}

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

最后来到第三步,看看performDraw的实现。

ViewRootImpl.java

private void performDraw() {
    ...
    try {    
        draw(fullRedrawNeeded);
    } finally {    
        mIsDrawing = false;    
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
    ...
}

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

private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,        
    boolean scalingRequired, Rect dirty) {    
    // Draw with software renderer.    
    final Canvas canvas;
    ...
    canvas = mSurface.lockCanvas(dirty);
    ...
    try {    
        canvas.translate(-xoff, -yoff);    
        ...  
        canvas.setScreenDensity(scalingRequired ? mNoncompatDensity : 0);    
        ...   
        mView.draw(canvas);    
        ...
    } finally {
        ...
    }
    ...
    surface.unlockCanvasAndPost(canvas);
    ...
}
        

可以看到调用的是view里的draw函数进行绘制,draw的过程分六步,必须顺序执行,分别是:
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)

我们只关注其中的关键四步,绘制背景,绘制view本身,绘制子view,绘制滚动条等附属物,看看这个函数实现:

public void draw(Canvas canvas) {
    ...
    // 绘制背景
    drawBackground(canvas);
    ...
    // 绘制view本身
    onDraw(canvas);
    ...
    // 绘制view children
    dispatchDraw(canvas);
    ...
    // 绘制scrollbar等
    onDrawForeground(canvas);
}

// onDraw空实现,需要各个子View重写该函数完成自己的绘制
protected void onDraw(Canvas canvas) {
}

可以看到onDraw是个空函数,需要各个子View重写该函数完成自己的绘制。这样,就完成了measure,layout,draw的流程。

getWidth/getMeasuredWidth, getHeight/getMeasuredHeight的区别是什么

getMeasuredWidth/Height 获取的是子view本身指定的宽度,高度。getWidth/Height获取的是子view在父view是实际占据的宽度,高度。比如子view设定自己的宽高分别是200px,200px,这就是MeasuredWidth, MeasuredHeight。在父view的onLayout函数中,调用了childview.layout(0, 0, 100, 100), 此时子view的实际宽高分别是100px,100px,这就是Width,Height。因此,不要在onLayout函数之外的地方使用MeaturedHeight,MeasuredWidth。

    原文作者:JackMeGo
    原文地址: https://www.jianshu.com/p/50e439b3c0af
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞