自定義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 . |
瞭解了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度鞠躬!