編輯:關於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就確定了嗎?我們先看下官方注釋:
View.MeasureSpec.
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) { ArrayListlistenersCopy = (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、為什麼要有AIDL? 無論學什麼東西,最先得弄明白為什麼要有這個東西,不要說存在即是合理,存在肯定合理,但是你還是沒有明白。對於AIDL有一些人的淺顯概念就是,AID
繪制曲線類:public class BaseFundChartView extends View { Paint linePaint; Paint text
最近經常看到各種介紹MVP模式的博客的,之前寫過不少的Android應用,在做那些應用的時候,都是要求快速完成,所以從開始設計到寫代碼就一直考慮著重用。以前寫的項目基本都
Android 列表組件 ListView列表組件是開發中經常用到組件,使用該組件在使用時需要為它提供適配器,由適配器提供來確定顯示樣式和顯示數據。下面看一個例子:新建一