1. 程式人生 > >自定義View基礎之——初識View

自定義View基礎之——初識View

介面永遠離不開各種各樣的控制元件,而這些控制元件,無論是TextView,Button,ImageView,甚至ListView等等,他們都有一個共同的基類,那就是View。但是,哪怕有了如此多的控制元件,有時候依舊滿足不了我們設計師的胃口,時不時會冒出各種各樣酷炫吊炸天的介面,這時候就需要我們自己去自定義View了。例如說,繪製一個圓形頭像,繪製圖片的載入進度條,或者實現上拉重新整理下拉載入的操作等等,這些都是通過自定義View的實現。想要自定義View,那麼首先就要先了解View:

一、位置,尺寸:

對於Android系統中的每一個View都會在介面中佔據一塊矩形的區域,自然也就包括left,top,right,bottom四個屬性,我們可以使用相應的get方法進行獲取,具體幾個方法如下:

getLeft():獲取view的left邊相對於父view的距離,左上角的橫座標。

getTop():獲取view的top邊相對於父View的距離,左上角的縱座標。

getRight():獲取view的right邊相對於父View的距離,右下角的橫座標。

getBottom():獲取view的bottom邊相對於父View的距離,右下角的縱座標。

而view的尺寸是以寬度和高度來表達的,事實上一個view擁有兩組寬和高的值。一組是measured width和measured height,可以使用getMeasuredWidth()和getMeasuredHeight()來獲取,這組尺寸指的是view想要在父佈局內是多大。第二組尺寸是width和height,這組尺寸定義了view在螢幕上繪製時候的實際尺寸,可以使用getWidth()和getHeight()方法獲取,兩組尺寸大多數情況下一樣。兩組尺寸大多數情況下一樣,那麼時候不一樣呢?等到接下來再說。為了測量尺寸,view通常需要將padding也要考慮進去,如果有必要的話,其實在自定義view的onDraw()方法裡也應該處理padding,不然padding是無法起到任何作用的。而margin則是隻有我們自定義ViewGroup的時候才會去考慮。

二、view的繪製過程:

view的繪製流程依次是measure過程,layout過程和draw過程。其實我們稍微一想也就知道這個大概思路了,得首先進行measure測量過程,知道了view的寬度和高度;之後layout佈局過程,由父佈局安排view的位置;最後進行draw過程,將view繪製到螢幕上。

1、measure過程

view的measure是通過呼叫measure()這個方法來實現的,當測量過程結束,measure()方法返回之後,就可以獲取measuredWidth和measuredHeight了,通過getMeasuredWidth()和getMeasuredHeight()來獲取。

public final void measure(int widthMeasureSpec, int heightMeasureSpec)

而measure()方法,很明顯是個final方法,這代表子類不能重寫這個方法,而在measure()方法內部,則會去呼叫onMeasure()方法,確切的測量工作也都是在onMeasure()這個方法裡執行的。而我們通常自定義View的時候,需要重寫的也就是onMeasure()方法。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
我們在onMeasure()方法中可以看到widthMeasureSpec和heightMeasureSpec這兩個引數,也是measure()方法傳遞進來的。這裡就不得不提MeasureSpec,雖然已經有很多部落格仔細研究過,可能你們都厭煩了,可是它確實不可或缺,我還是要在這裡好好說一遍。MeasureSpec是一個32的int值,高2位代表的SpecMode,低30位代表的是SpecSize。SpecMode有以下三種:

EXACTLY:父容器已經知道view需要的確切大小,就是SpecSize。通常我們將layout_width或layout_height指定為具體數值,或者指定為match_parent的時候,對應的就是這種模式。

AT_MOST:父容器給定了最大值SpecSize,view的大小不能超過這個值。通常我將layout_width活layout_height指定為wrap_content的時候,對應這種模式。

UNSPECIFIED:把它放在最後不是因為它最重要,而是因為它用的比較少。這是指父容器不對view進行任何限制,view想多大就多大。

通常我們在自定義view重寫onMeasure()方法的時候,通過widthMeasuSpec和heightMeasureSpec就可以獲得相應的SpecMode和SpecSize:

int widthSize=MeasureSpec.getSize(widthMeasureSpec);
int widthMode=MeasureSpec.getMode(widthMeasureSpec);
int heightSize=MeasureSpec.getSize(heightMeasureSpec);
int heightMode=MeasureSpec.getMode(heightMeasureSpec);
當然,在重寫onMeasure()方法的時候,最後一定要呼叫setMeasuredDimension(widthMeasureSpec,heightMeasureSpec)。

有人可能要問了,為什麼要重寫onMeasure()方法?那是因為view類預設的onMeasure()方法只支援EXACTLY模式,具體原因接下來說。想象一下,如果你自定義了一個view,然後在xml中設定android:layout_width="wrap_content",執行起來卻發現你的自定義view鋪滿了全屏,很明顯這不是你想要的結果,那是多糟糕的體驗啊,這時候你想要支援wrap_content就必須重寫onMeasure()方法了。

繼續回到我們之前的話題,onMeasure()方法中的兩個引數widthMeasureSpec和heightMeasureSpec,我們已經知道了它們是MeasureSpec型別,也知道了如何獲取它們的SpecMode和SpecSize,那麼它們是怎麼來的呢?每次重寫onMeasure()方法的時候,可能大家都在疑惑,這兩個引數是靠什麼決定的呢,它們只是通過我們在xml中設定layout_width或者layout_height就確定了嗎?我們先看下官方註釋:

widthMeasureSpec horizontal space requirements as imposed by the parent. The requirements are encoded with View.MeasureSpec.
heightMeasureSpec vertical space requirements as imposed by the parent. The requirements are encoded with View.MeasureSpec.
簡單翻譯下就是,這兩個引數是由父view強加給子view的水平或者垂直空間要求。也就是說並不只是通過xml中的layout_width或layout_height來決定咯?其實對於普通的view,它的MeasureSpec都是由父容器自身的MeasureSpec和view自身的LayoutParams(也就是layout_width和layout_height屬性)共同決定的。而父容器的MeasureSpec則由它的父容器的MeasureSpec和它自身的LayoutParams共同決定,繼續向上追溯到頂級View(DecorView),則是由視窗的尺寸和其自身的LayoutParams來共同決定的。

瞭解了onMeasure()方法中的兩個引數之後,我們繼續來看方法內的具體內容,setMeasuredDimension()我們前面已經說過,是為了設定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;
}

程式碼很簡單,就是根據提供的widthMeasureSpec或heightMeasureSpec來確定測量所得的width或height。程式碼中我們可以看到,無論是AT_MOST還是EXACTLY,最終的所得到的測量的大小都是specSize,也就是我們提供的引數measureSpec中得來的。也就是說哪怕我們設定了wrap_content,最終我們得到的寬或高並不是wrap_content,而是match_parent的父容器允許的最大值。這樣也就解答了“view類預設的onMeasure()方法只支援EXACTLY模式”這個問題,我們想要支援wrap_content,只能重寫onMeasure()方法。

以上說的都是單獨view的測量過程,而對於ViewGroup來說,measure過程是一個自頂向下的樹的遍歷,除了執行自己的測量過程外,還會去執行所有子元素的measure()方法,各個子元素再遞迴去執行這個流程。

2、layout過程

layout過程,作為整個繪製流程的第二階段,父容器會根據在measure過程中獲得的寬度和高度來安排所有子元素的位置,也是自頂向下的樹的遍歷。layout過程通過呼叫layout()方法來實現的,與measure()方法一樣,我們在自定義ViewGroup的時候並不需要重寫這個方法,而是重寫onLayout()方法來確定子元素的位置。

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()方法是用來確定view本身的位置,其中會呼叫onLayout()方法,onLayout()方法則是用來確定所有子元素的位置的。

大致的流程就是,父元素在layout()方法中完成自己的定位,然後呼叫onLayout()方法,其中onLayout()方法中會繼續呼叫子元素的layout()方法,子元素就可以確定自己的位置。這樣一層層傳遞下去,就完成了整個view樹的layout過程。

當我們自定義ViewGroup重寫onLayout()這個方法的時候,需要注意的就是呼叫子view的layout()的時候,需要將margin考慮進去,自定義view並不需要重寫onLayout()方法。

3、draw過程

費了這多事,我們終於來到了draw過程。作為整個繪製流程的最後一個階段,當然也是最重要的部分,它的作用,就是將view繪製到螢幕上面。我們自定義view的時候,只需要重寫onDraw()方法即可,之後使用canvas和paint在手,我們還不是想幹什麼就幹什麼。

draw過程也是呼叫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的繪製過程,大概以下幾步:

(1)繪製背景  drawBackground(canvas)

(2)繪製view的內容  onDraw(canvas)

(3)繪製子元素  dispatchDraw(canvas)

(4)繪製裝飾  onDrawForeground(canvas)  

view繪製過程的傳遞是通過dispatchDraw()方法來實現的。ViewGroup通常情況下不需要繪製,但是ViewGroup會呼叫diapatchDraw()方法來繪製其子View。dispatchDraw()方法會遍歷呼叫所有子元素的draw()方法,這樣繪製流程就一層層的傳遞下去了。所以我們通常自定義view的時候才重寫onDraw()方法,自定義ViewGroup的時候大多不重寫onDraw()方法。

這樣,我們整個View的繪製流程都說完了,大家對於View應該也有一定的瞭解了,是不是覺得view也就這麼回事,是不是信心滿滿啦,對於自定義View也躍躍欲試了?可是隻瞭解這些還是不夠的,我們下一篇部落格介紹自定義View時使用的主要工具canvas和paint,以及自定義View時需要重寫的方法,敬請關注!

PS:我靠,這篇部落格寫了我4個小時啊,4個小時啊,4個小時啊啊啊啊啊啊!!!!我發現了寫文件太費勁了,一個字一個字的憋,比擠牙膏累多了,分明是便祕啊。還是直接寫專案部落格開心,只要把程式碼一貼,隨便說幾句話,哪怕滿屏寫上哈哈哈哈,也迅速結束戰鬥呀。寫文件不是人乾的活啊,我都寫了四小時了,還不夠您看個5分鐘麼,求點選啊!!!

PS:真的佩服那些寫小說的,幾百萬幾百萬字的就碼出來了,為我那些年看的盜版小說道歉,90度鞠躬!