從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。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章