View繪製流程以及自定義控件

一、前言概述

在Android的知識體系中,View扮演着很重要的角色,簡單來理解,View是Android在視覺上的呈現。在界面上Android提供了一套GUI庫,裏面有很多控件,但是很多時候我們並不滿足於系統提供的控件,因爲這樣就意味着應用界面的同類化比較嚴重。那麼怎麼才能做出與衆不同的效果呢?答案是自定義View,也可以叫自定義控件,通過自定義View我們可以實現各種五花八門的效果。但是自定義View也是有一定難度的,尤其是複雜的自定義View,大部分時候我們僅僅瞭解基本控件的使用方法是無法做出複雜的自定義控件的。爲了更好地自定義View,還需要掌握View的底層工作原理,比如View的測量流程、佈局流程以及繪製流程,掌握這幾個基本流程後,我們就對View的底層更加了解,這樣我們就可以做出一個比較完善的自定義View。

二、初識ViewRoot和DecorView

ViewRoot對應於ViewRootImpl類,它是連接WindowManager和DecorView的紐帶,View的三大流程均是通過ViewRoot來完成的。在ActivityThread中,當Activity對象被創建完畢後,會將DecorView添加到Window中,同時會創建ViewRootImpl對象,並將ViewRootImpl對象和DecorView建立關聯。

View的繪製流程是從ViewRoot的performTraversals方法開始的,它經過measure、layout和draw三個過程才能最終將一個View繪製出來,其中measure用來測量View的寬和高,layout用來確定View在父容器中的放置位置,而draw則負責將View繪製在屏幕上。針對performTraversals的大致流程,可見下列代碼以及後面的流程圖:

/frameworks/base/core/java/android/view/ViewRootImpl.java

private void performTraversals() {
        ...
        if (!mStopped || mReportNextDraw) {
                boolean focusChangedDueToTouchMode = ensureTouchModeLocally(
                        (relayoutResult&WindowManagerGlobal.RELAYOUT_RES_IN_TOUCH_MODE) != 0);
                if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()
                        || mHeight != host.getMeasuredHeight() || contentInsetsChanged) {
                    int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
                    int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
                    ...
                     // Ask host how big it wants to be
                    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
                    ...
                }
            }   
        ...

        if (didLayout) {
            performLayout(lp, desiredWindowWidth, desiredWindowHeight);
            ... 
        }
        ...
        if (!cancelDraw && !newSurface) {
            if (!skipDraw || mReportNextDraw) {
                if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
                    for (int i = 0; i < mPendingTransitions.size(); ++i) {
                        mPendingTransitions.get(i).startChangingAnimations();
                    }
                    mPendingTransitions.clear();
                }
                performDraw();
            }
        }
        ...
}

performtraversals流程圖
如圖所示:performTraversals會依次調用performMeasure、performLayout和performDraw三個方法,這三個方法分別完成頂級View的measure、layout和draw這三大流程,其中在performMeasure中會調用measure方法,在measure方法中又會調用OnMeasure方法,在OnMeasure方法中則會對所有的子元素進行measure過程,這個時候measure流程就從父容器傳遞到子元素中了,這樣就完成了一次measure過程。接着子元素會重複父容器的measure過程,如此反覆就完成了整個View樹的遍歷。同理,performLayout和performDraw的傳遞流程和performMeasure是類似的,唯一不同的是,performDraw的傳遞過程是在draw方法中通過dispatchDraw來實現的,不過這並沒有什麼本質區別。
DecorView作爲頂級View,一般情況下它內部會包含一個豎直方向的LinearLayout,在這個LinearLayout裏面有上下兩個部分,上面是標題欄,下面是內容欄。在Activity中我們通過setContentView所設置的佈局文件其實就是被加到內容欄之中的,而內容欄的id是content,因此可以理解爲Activity指定佈局的方法不叫setView而叫setContentView,因爲我們的佈局的確加到了id爲content的FrameLayout中。通過源碼我們可以知道,DecorView其實是一個Framelayout
,View層的事件都先經過DecorView,然後才傳遞給我們的View。
DecorView圖形展示

三、MeasureSpec的理解

MeasureSpec代表一個32位int值,高2位代表SpecMode,低30位代表SpecSize,SpecMode是指測量模式,而SpecSize是指在某種測量模式下的規格大小。SpecMode的源碼如下:
    public static class MeasureSpec {
        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

        /**
         * Measure specification mode: The parent has not imposed any constraint
         * on the child. It can be whatever size it wants.
         */
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;

        /**
         * Measure specification mode: The parent has determined an exact size
         * for the child. The child is going to be given those bounds regardless
         * of how big it wants to be.
         */
        public static final int EXACTLY     = 1 << MODE_SHIFT;

        /**
         * Measure specification mode: The child can be as large as it wants up
         * to the specified size.
         */
        public static final int AT_MOST     = 2 << MODE_SHIFT;

        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);
        }
        ...
    }
MeasureSpec通過將SpecMode和SpecSize打包成一個int值來避免過多的對象內存分配,爲了方便操作,其提供了打包和解包的方法。
從源碼中可以看出SpecMode有三種類型,如下:
  • UNSPECIFIED:父容器不對View作任何限制,它可以是任意大小,這種情況一般用於系統內部,表示一種測量的狀態。
  • EXACTLY:父容器已經檢測出View所需要的精確大小,這個時候View的最終大小就是SpecSize所指定的值。它對應於LayoutParams中的match_parent和具體的數值這兩種模式。
  • AT_MOST:父容器指定了一個可用大小即SpecSize,View的大小不能大於這個值,具體是什麼值要看不同View的具體實現。它對應於LayoutParams中的wrap_content。

四、MeasureSpec和LayoutParams的對應關係

對於DecorView,其MeasureSpec由窗口的尺寸和其自身的LayoutParams來共同確定;對於普通View,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams來共同決定,MeasureSpec一旦確定後,onMeasure中就可以確定View的測量寬/高。
在ViewRootImpl中的measureHierarchy方法中,展示了DecorView的MeasureSpec的創建過程,其中desiredWindowWidth和desiredWindowHeight是屏幕的尺寸。代碼如下:
    private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
            final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {
        ...
        if (!goodMeasure) {
            childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
            childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
            performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
            if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) {
                windowSizeMayChange = true;
            }
        }
        ...
    }
接着我們來看一下getRootMeasureSpec方法的實現:
    private static int getRootMeasureSpec(int windowSize, int rootDimension) {
        int measureSpec;
        switch (rootDimension) {
        case ViewGroup.LayoutParams.MATCH_PARENT:
            // Window can't resize. Force root view to be windowSize.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            // Window can resize. Set max size for root view.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            // Window wants to be an exact size. Force root view to be that size.
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
        }
        return measureSpec;
    }
通過以上的代碼可以得知,DecorView的MeasureSpec的產生過程遵守以下規則,根據它的LayoutParams中的寬/高參數來劃分:
  • LayoutParams.MATCH_PARENT:精準模式,大小就是窗口的大小;
  • LayoutParams.WRAP_CONTENT:最大模式,大小不定,但是不能超過窗口的大小;
  • 固定大小:精準模式,大小即爲LayoutParams中指定的大小。
以上是DecorView,接下來說一下普通的View。在佈局中,View的measure過程由ViewGroup傳遞而來,先看一下ViewGroup的measureChildWithMargins方法:
    protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
上述方法會對子元素進行measure,在調用子元素的measure方法之前會先通過getChildMeasureSpec方法來得到子元素的MeasureSpec。從代碼來看,很顯然,子元素的MeasureSpec的創建與父容器的MeasureSpec和子元素本身的LayoutParams有關,此外還和VIew的margin及padding有關,具體情況可以看一下ViewGroup的getChildMeasureSpec方法,如下:
    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);

        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent has imposed a maximum size on us
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size... find out how big it should
                // be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }
根據上面的方法,它的主要作用是根據父容器的MeasureSpec同時結合View本身的layoutParams來確定子元素的MeasureSpec,參數中的padding是指父容器中已佔用的空間大小,因此子元素可用的大小爲父容器的尺寸減去padding。
這裏簡單的說一下,當View採用固定寬/高的時候,不管父容器的MeasureSpec是什麼,View的MeasureSpec都是精準模式並且其大小遵循Layoutparams中的大小。當View的寬/高是match_parent時,如果父容器是精準模式,那麼View也是精準模式並且其大小是父容器的剩餘空間;如果父容器是最大模式,那麼View也是最大模式並且其大小不會超過父容器的剩餘空間。當View的寬/高是wrap_content時,不管父容器的模式是精準還是最大化,View的模式總是最大化並且大小不能超過父容器的剩餘空間。

五、View的工作流程

View的工作流程主要是指measure、layout、draw這三大流程,即測試、佈局和繪製,其中measure確定View測量寬/高,layout確定View的最終寬/高和四個頂點的位置,而draw則會將View繪製到屏幕上。

5.1 measure過程

5.1.1 View的measure過程

View的measure過程由其measure方法來完成,measure方法是一個final類型的方法,這意味着子類不能重寫此方法,在View的measure方法中會去調用View的onMeasure方法,因此只需要看onMeasure的實現即可,View的onMeasure方法如下所示:
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
setMeasureDimension方法會設置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;
    }
由上面代碼可知,正常情況下(SpecMode爲AT_MOST或EXACTLY),getDefaultSize獲取的尺寸大小即爲SpecSize。同時可知,直接繼承View的自定義控件需要重寫onMeasure方法並設置wrap_content時的自身大小,否則在佈局中使用wrap_content就相當於使用match_parent的效果。
如果View在佈局中使用wrap_content,那麼它的SpecMode是AT_MOST模式,在這種模式下,它的寬/高等於specSize;這種情況下View的specSize是parentSize,而parentSize是父容器中目前可以使用的大小,也就是父容器當前剩餘的空間大小。很顯然,View的寬/高就等於父容器當前剩餘的空間大小,這種效果和在佈局中使用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  && heightSpecMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(mWidth, mHeight);
    } else if (widthSpecMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(mWidth, heightSpecSize);
    } else if (heightSpecMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(widthSpecSize, mHeight);
    }
}
在上面的代碼中,我們只需要給View指定一個默認的內部寬/高(mWidth和mHeight),並在wrap_content時設置此寬/高即可。對於非wrap_content情形,我們沿用系統的測量值即可。

5.1.2 ViewGroup的measure過程

對於ViewGroup來說,除了完成自己的measure過程以外,還會遍歷去調用所有子元素的measure方法,各個子元素再遞歸去執行這個過程。和View不同的是,ViewGroup是一個抽象類,因此它沒有重寫View的onMeasure方法,但是它提供了一個叫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時,會對每一個子元素進行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的思想就是取出子元素的LayoutParams,然後再通過getChildMeasureSpec來創建子元素的MeasureSpec,接着將MeasureSpec直接傳遞給View的measure方法進行測量。

5.2 Layout過程

Layout的作用是ViewGroup用來確定子元素的位置,當ViewGroup的位置被確定後,它在onLayout中會遍歷所有的子元素並調用其layout方法,在layout方法中onLayout方法又被調用。Layout過程和measure過程相比就簡單多了,layout方法確定View本身的位置,而onLayout方法則會確定所有子元素的位置,先看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);
            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;
    }
layout方法的大致流程如下:首先會通過setFrame方法來設定View的四個頂點的位置,即初始化mLeft、mRight、mTop和mBottom這四個值,View的四個頂點一旦確定,那麼View在父容器中的位置也就確定了;接着會調用onLayout方法,這個方法的用途是父容器確定子元素的位置,和onMeasure方法類似,onLayout的具體實現同樣和具體的佈局有關,所以View和ViewGroup均沒有真正實現onLayout方法。

5.3 draw過程

Draw過程就比較簡單,它的作用是將View繪製到屏幕上面。View的繪製過程遵循如下幾步:
(1)繪製背景 background.draw(canvas)
(2)繪製自己(onDraw)
(3)繪製children(dispatchDraw)
(4)繪製裝飾(onDrawScrollBars)
可以來看一下draw方法的源碼:
    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:
         *
         *      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)
         */

        // 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;
        }
        ...
    }
View繪製過程的傳遞是通過dispatchDraw來實現的,dispatchDraw會遍歷調用所有子元素的draw方法,如此draw事件一層層地傳遞了下去。

六、自定義View

6.1 自定義View的分類

1.繼承View重寫onDraw方法
這種方法主要用於實現一些不規則的效果,即這種效果不方便通過佈局的組合方式來達到,往往需要靜態或者動態地顯示一些不規則的圖形。很顯然這需要通過繪製的方式來實現,即重寫onDraw方法。採用這種方式需要自己支持wrap_content,並且padding也需要自己處理。

2.繼承ViewGroup派生特殊的Layout
這種方式主要用於實現自定義的佈局,即除了LinearLayout、RelativeLayout、FrameLayout這幾種系統的佈局之外,我們重新定義一種新佈局,當某種效果看起來很像幾種View組合在一起的時候,可以採用這種方法來實現。採用這種方式稍微複雜一些,需要合適地處理ViewGroup的測量、佈局這兩個過程,並同時處理子元素的測量和佈局過程。

3.繼承特定的View(比如TextView
這種方法比較常見,一般是用於擴展某種已有的View的功能,比如TextView,這種方法比較容易實現。這種方法不需要自己支持wrap_content和padding等。

4.繼承特定的ViewGroup(比如LinearLayout)
這種方法也比較常見,當某種效果看起來很像幾種View組合在一起的時候,可以採用這種方法來實現。採用這種方法不需要自己處理ViewGroup的測量和佈局這兩個過程。需要注意這種方法和方法2的區別,一般來說方法2能實現的效果方法4也都能實現,兩者的主要差別在於方法2更接近View的底層。

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