Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 自定義View基礎之——初識View

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

編輯:關於Android編程

界面永遠離不開各種各樣的控件,而這些控件,無論是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 withView.MeasureSpec. heightMeasureSpec vertical space requirements as imposed by the parent. The requirements are encoded withView.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 listenersCopy =
                    (ArrayList)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時需要重寫的方法,敬請關注!

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved