從Android源碼分析View繪製

    在開發過程當中,咱們經常會來自定義View。它是用戶交互組件的基本組成部分,負責展現圖像和處理事件,一般被當作自定義組件的基類繼承。那麼今天就經過源碼來仔細分析一下View是如何被建立以及在繪製過程當中發生了什麼。canvas

建立

    首先,View公有的構造函數的重載形式就有四種:框架

  • View(Context context)    經過代碼建立view時使用此構造函數,經過context參數,能夠獲取到須要的主題,資源等等。
  • View(Context context, AttributeSet attrs)    當經過xml佈局文件建立view時會使用此構造函數,調用了3個參數的構造方法。
  • View(Context context, AttributeSet attrs, int defStyleAttr)     經過xml佈局文件建立view,並採用在屬性中指定的style。這個view的構造函數容許其子類在建立時使用本身的style。調用了下面四參的構造方法。
  • View(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)    該構造函數能夠經過xml佈局文件建立view,能夠採用theme屬性或者style資源文件指定的style。

    參數:函數

    • Context : view運行的上下文信息,從中能夠獲取到當前theme,資源文件等信息。
    • AttributeSet: xml佈局文件中view標籤下指定的屬性集合。
    • defStyleAttr: 當前theme中的一條屬性,它包含一條指向theme資源文件中style的引用。默認值爲0。
    • defStyleRes: 一個style資源文件的標示,表示style的ID,當值爲0或者找不到對應的theme資源時候採用默認值。

 

    綜上所述,單參的構造函數從代碼建立view,其他都調用四參的構造函數根據xml佈局文件建立view。咱們能夠在不一樣的地方指定屬性值,例如:佈局

直接在xml標籤中中指定的attrs值,能夠從AttributeSet中獲取this

  • 經過在標籤屬性「style」中指定的資源文件。
  • 默認的defStyleAttr。
  • 默認的defStyleRes。
  • 當前theme中的默認值。

    構造函數的代碼過長,就不在這裏貼了,主要進行的工做是:獲取各項系統定義的屬性,而後根據屬性值初始化view的各項成員變量和事件。spa

    通常狀況下,咱們自定義view的時候,根據實際狀況重寫構造函數時,若是隻從code建立,則只用實現單參數的便可。若是須要從xml佈局文件中建立,則須要實現單參數和一個多參數的就行了,由於多參數的默認調用了四參數的構造函數;而後再獲取到自定義的屬性進行處理就OK了。設計

    至此,view的建立以及初始化工做完畢,而後開始繪製view的工做。那麼Android系統是如何對view進行繪製的呢?code

繪製

    在activity獲取到焦點後,會請求Android Framework根據它的佈局文件進行繪製,activity須要提供所繪佈局文件的根節點,而後對佈局的樹結構一邊遍歷一邊進行繪製。咱們都知道,ViewGroup是View的子類,它能夠擁有若干子view,它的不少操做和view相同,不一樣的是ViewGroup負責繪製其子節點,而view則負責繪製其自身。整個遍歷過程從上到下,在整個過程當中,須要進行大小測量(measure函數)和定位(layout函數),而後再進行繪製。下面咱們來看這些工做是如何進行的:xml

測定尺寸

    在Android中,全部view被組織成樹狀結構,最頂層measure的主要工做就是負責遞歸測量出整個view樹結構的尺寸大小,每一個View的控件的實際寬高都是由父視圖和自己視圖決定的。對象

    在研究源碼以前,我先從總體上概況一下整個遞歸調用過程。從根view開始,使用measure方法中計算整個view樹的大小,在該方法中調用子view的onMeasure方法。在onMeasure中主要進行兩個工做:

  1. 調用setMeasuredDimension設置view自身的尺寸(mMeasureWidth和mMeasuredHeight),具體會在下面看到。
  2. 若是該view是ViewGroup,則須要繼續遞歸調用其onMeasure方法來計算ViewGroup的子view大小。

    根view一般就是一個ViewGroup,須要計算子view尺寸。首先獲取到全部子view,而後調用measureChildWithMargins方法來計算子view的尺寸。在這個方法中調用了子view的measure方法。下面咱們來看具體源碼。

 

    首先在measure方法中肯定view的大小。這個方法被定義爲final類型,不可被子類重寫。在View中有一個靜態內部類MeasureSpec封裝了父view要傳遞給子View的佈局參數,由size 和 mode共同組成。size便是大小,mode表示模式。(其實就是一個int值高2位表示mode,低30位表示size). mode總共有三種模式:

  • UNSPECIFIED:父view並未指定子view的大小,可隨意根據開發人員需求指定view大小。
  • EXACTLY: 父view嚴格指定了子view的大小
  • AT_MOST: 子view的大小不超過該值
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        boolean optical = isLayoutModeOptical(this);//是否使用視覺邊界佈局 
        if (optical != isLayoutModeOptical(mParent)) {// 當view和它的父viewGroup就是否採用視覺邊界佈局不一致時
            Insets insets = getOpticalInsets();
            int oWidth  = insets.left + insets.right;
            int oHeight = insets.top  + insets.bottom;
            widthMeasureSpec  = MeasureSpec.adjust(widthMeasureSpec,  optical ? -oWidth  : oWidth);
            heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
        }

        
        long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
        if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);

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

            // first clears the measured dimension flag
            mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;

            resolveRtlPropertiesIfNeeded();

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

            // flag not set, setMeasuredDimension() was not invoked, we raise
            // an exception to warn the developer
            if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
                throw new IllegalStateException("onMeasure() did not set the"
                        + " measured dimension by calling"
                        + " setMeasuredDimension()");
            }

            mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
        }

        mOldWidthMeasureSpec = widthMeasureSpec;
        mOldHeightMeasureSpec = heightMeasureSpec;

        mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
                (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
    }

    方法接收的兩個參數widthMeasureSpec和heightMeasureSpec表示view的寬高,由上一層父view計算後傳遞過來。view大小的測量工做在標紅的onMeasure方法中進行。咱們在自定義view時每每須要重寫該方法,根據傳入的view大小以及其內容來設定view最終顯示的尺寸。

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

    重寫該方法時,咱們須要調用setMeasuredDimension這個方法來存儲已經測量好的尺寸(這裏默認使用getDefalutSize),只有在調用過此方法後,才能經過getMeasuredWidth方法和getMeasuredHeight方法獲取到尺寸。同時,咱們要保證最後獲得的尺寸不小於view的最小尺寸。咱們須要注意的是,setMeasuredDimension方法必須在OnMeasure方法中調用,不然會拋出異常。

    OK,measure方法至此完畢。然而,咱們能夠發現真正測量view大小的工做並不在此方法中進行,這裏僅僅是一個測量框架,根據各類不一樣的狀況進行判斷,完成一些必要的步驟。這些步驟是必須的也是沒法被開發者更改的,須要根據狀況自定義的工做放在了onMeasure中由開發者完成。這樣既保證了繪製流程的執行,又靈活的知足了各類需求,是典型的模板方法模式。

    因爲一個父view下可能有多個子view,因此measure方法不只僅執行一次,而是在父view(viewGroup)中獲取到全部子view,而後遍歷調用子view的measure方法。

 

定位

    當view的大小已經設定完畢,則須要肯定view在其父view中的位置,也就是把子view放在合理的位置上。由於只有ViewGroup才包含子view,因此通常咱們提及父view,確定是在說ViewGroup。完成佈局工做主要分爲兩部分,也是遞歸實現的:

  1. 在layout方法中調用setFrame設置該View視圖位於父視圖的座標。
  2. 若是view是ViewGroup類型,則調用其onLayout方法完成子view佈局工做。

    下面來看具體源碼,父view調用了子view的layout方法:

public void layout(int l, int t, int r, int b) {
        if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
            onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }

        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); // 肯定view在佈局中的位置
            mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;

            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnLayoutChangeListeners != null) {
                ArrayList<OnLayoutChangeListener> listenersCopy =
                        (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
                int numListeners = listenersCopy.size();
                for (int i = 0; i < numListeners; ++i) {
                    listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
                }
            }
        }

        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
        mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
    }

    該方法接收四個參數是子view相對於父view而言的上下左右位置。然而咱們發現其中調用到的onLayout方法默認的實現是空的。這是由於肯定view在佈局的位置這個操做應該由Layout根據自身特色來完成。任何佈局的定義都要重寫其onLayout方法,並在其中設定子view的位置。

繪製

    在進行完測定尺寸和定位以後,終於能夠開始繪製了。這裏的工做還是經過遞歸來完成的。view調用draw方法來進行繪製,裏面調用onDraw來繪製自身,若是還有子view則須要調用dispatchDraw來繪製子view。

    繪製須要調用draw方法,總共分爲六個步驟:

  1. 繪製背景
  2. 若是須要,保存canvas的層次準備邊緣淡化。
  3. 繪製view的內容
  4. 繪製子view
  5. 若是須要,繪製淡化的邊緣並存儲圖層。
  6. 繪製裝飾部分,例如滾動條等。
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;
        // Step 1, 繪製背景
        int saveCount;

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

        // 若是不須要,跳過步驟2和5
        final int viewFlags = mViewFlags;
        boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
        boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
        if (!verticalEdges && !horizontalEdges) {
            // Step 3, 繪製內容
            if (!dirtyOpaque) onDraw(canvas);

            // Step 4, 繪製子view
            dispatchDraw(canvas);

            // Step 6, 繪製裝飾部分
            onDrawScrollBars(canvas);

            if (mOverlay != null && !mOverlay.isEmpty()) {
                mOverlay.getOverlayView().dispatchDraw(canvas);
            }

            // 完成
            return;
        }

    }

    咱們選擇常規的繪製過程,不介紹2,5步驟。

    第一步,調用drawBackground繪製背景圖案:

private void drawBackground(Canvas canvas) {
        final Drawable background = mBackground;
         // 獲取到當前view的背景,是一個drawable對象 if (background == null) {
            return;
        }

        if (mBackgroundSizeChanged) {// 判斷背景大小是否變化,是則設置背景邊界
            background.setBounds(0, 0,  mRight - mLeft, mBottom - mTop);
            mBackgroundSizeChanged = false;
            mPrivateFlags3 |= PFLAG3_OUTLINE_INVALID;
        }

        // Attempt to use a display list if requested.
        if (canvas.isHardwareAccelerated() && mAttachInfo != null
                && mAttachInfo.mHardwareRenderer != null) {
            mBackgroundRenderNode = getDrawableRenderNode(background, mBackgroundRenderNode);

            final RenderNode displayList = mBackgroundRenderNode;
            if (displayList != null && displayList.isValid()) {
                setBackgroundDisplayListProperties(displayList);
                ((HardwareCanvas) canvas).drawRenderNode(displayList);
                return;
            }
        }
 // 調用drawable對象的繪製方法完成繪製
        final int scrollX = mScrollX;
        final int scrollY = mScrollY;
        if ((scrollX | scrollY) == 0) {
            background.draw(canvas);
        } else {
            canvas.translate(scrollX, scrollY);
            background.draw(canvas);
            canvas.translate(-scrollX, -scrollY);
        }
    }

    第三步,調用onDraw方法繪製view的內容,因爲不一樣的view內容不一樣,因此須要子類進行重寫。

    第四步,繪製子view,這裏仍然須要當前layout的dispatchDraw方法來完成對各子view的繪製。

    第六步,繪製滾動條。

    一般狀況下,咱們自定義view,複寫onDraw方法來繪製咱們定義的view的內容便可。

 

總結

    經過研究view類的源碼,咱們能夠發現,在整個view的繪製流程中咱們須要完成測定尺寸,佈局定位,繪製這三個步驟。Android在設計過程當中,將固定不變的流程設計爲不可更改的模板方法,然而須要根據不一樣狀況而定的內容則交給開發者來完成重寫,在模板方法中調用便可。這樣設計即保證了整個流程的完整,又給開發工做帶來了靈活。同時,在類中又根據不一樣狀況定義了不一樣的flag,來知足不一樣狀況的繪製需求,之後有機會再具體研究這些flag的具體意義。

相關文章
相關標籤/搜索