對比自定義view和自定義viewgroup

前言

最近在研究view的整個事件過程,以及自定義view的繪制,今天突然發(fā)現(xiàn)還有dispatchDraw(),于是在官方api中找到了draw()方法。這兩個方法分別用于view繪制和viewgroup繪制,通常我們實現(xiàn)更多的是view繪制,而viewgroup繪制 默認是關(guān)閉狀態(tài),需要設(shè)置setWillNotDraw(false),什么意思呢?就是設(shè)置繪制為打開狀態(tài)。下面描述一下view定義以及viewgroup定義的流程,最后對比自定義view以及viewgroup有什么區(qū)別。

自定義view:

通常我們自定義一個view只需要先測量,然后確定要繪制的位置,位置確定了就可以開始繪制了,測量只需要用onMeasure(), 設(shè)置view內(nèi)部元素(后面直接稱之為元素)的位置可以寫在onSizechange()方法里(盡管元素可以在onMeasure()方法中設(shè)置Rect 的left,top,right,bottom來確定繪制的區(qū)域,但是onMeasure()方法主要功能是測量),繪制操作對應onDraw()根據(jù)需要繪制元素來進行繪制,所以自定義一個view一般會涉及到onMeasure(),onDraw(),以及onSizeChange() ( 這個方法是計算過程中Size改變會回調(diào) )這幾個方法,當然自定義view通常也涉及事件處理,只需要重寫onTouchEvent(),下面我介紹下我對這幾個方法的理解:

1. onMeasure()
顧名思義這是一個測量方法,用來測量view的大小,如果不重寫這個方法,該view的父view會在onMeasure()方法中調(diào)用

  setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), 
  widthMeasureSpec)
  getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));

這是系統(tǒng)給的默認測量方式,并且默認方式的測量還和背景圖片有關(guān),如果背景足夠大的話是會影響view的大小。當然我們也可以根據(jù)自己的需要去重寫setMeasuredDimension(widthsize,heightsize),測量方式通常由自己設(shè)置的LayoutParams和外層父ViewGroup的測量模式共同決定,至于為什么和父布局有關(guān),這是因為viewGroup會被內(nèi)部view提供測量模式,后面會在ViewGroup的measure()方法中會提到。

protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth()); }

我們在使用的時候可以根據(jù)自定義的邏輯要求,對其中的bitmap,text等等寬高進行計算,來確定最終要傳給
setMeasuredDimension(widthsize,heightsize)的寬高。

2. onDraw()
打一個比方,自定義view其實就像畫畫,需要畫布,畫筆,確定畫的位置。 onDraw()扮演著畫的動作,通常我們需要重寫這個方法,因為父view的onDraw()方法是個空方法當然它只提供畫布canvas,具體要畫什么圖案,畫多大,以及用什么樣的畫筆,不建議在這個方法做初始化工作,因為調(diào)用invalidate()方法會使這個方法重繪,我們可以在自定義view的構(gòu)造函數(shù)寫一個init()方法,用來實現(xiàn)畫筆及一些繪制工作需要用到的“工具”的初始化工作,在onMeasure()中測量畫布大小,onSizeChange()里確定我們畫的圖案位置(Rect),最后我們只要在canvas.drawbitmap(), canvas.drawcircle()…..等等可以畫出我們想繪制的圖案。

/***Implement this to do your drawing. 
  * * @param canvas the canvas on which the background will be drawn */ 
       protected void onDraw(Canvas canvas) { }
自定義viewGroup:

同樣自定義嘛,還是老規(guī)矩,先測量再確定布局,這里確定布局和view的確定布局不是一個意思,view確定布局是給view要繪制的元素確定繪制“地點”,而viewgroup確定布局的意思是給內(nèi)部子view設(shè)置layout,然后再去繪制,因為view是單獨存在的,不存在子view只存在內(nèi)部需要繪制的元素,所以view和viewgroup的概念是不一樣的。而viewGroup可能存在多個子view,所以需要給每個子view來指定具體位置,這就是onLayout()所做的事了,同樣,測量也是用到onMeasure(),但是它和子view的onMeasure()不同,子view的測量只要測量這個view按照測量法則需要多大的空間,而viewgroup的測量則是根據(jù)子view的測量結(jié)果來進行統(tǒng)計,最終來確定整個viewgroup的widthsize和heightsize。viewgroup的繪制會用到dispatchDraw(),它主要是分發(fā)給子組件進行繪制,調(diào)用子組件的draw()方法,通常viewgroup是不用去繪制的,而且繪制功能默認關(guān)閉。

1. onMeasure()
這里用到了measureChildren()方法,這個方法是對所有的子view進行測量。

 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
 int widthSize = MeasureSpec.getSize(widthMeasureSpec); 
 int heightSize = MeasureSpec.getSize(heightMeasureSpec); 
 int widthMode = MeasureSpec.getMode(widthMeasureSpec);
 int heightMode = MeasureSpec.getMode(heightMeasureSpec); 
 measureChildren(widthMeasureSpec, heightMeasureSpec);
 .....此處省略
 setMeasuredDimension(widthSize,heightSize);
} 

/** *這是一個遍歷去測量子組件的方法,主要子組件不是Gone類型都會被測量 */

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); 
    }
  }
}

接下來我們看下measureChild()這個方法,顯然是去測量子組件的大小的。官方文檔的代碼如下:

protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) {
 //獲取子組件的LayoutParams 
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight, lp.width); 
final int childHeightMeasureSpec =   getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom, lp.height); 
// 調(diào)用子組件的measure()方法 
child.measure(childWidthMeasureSpec, childHeightMeasureSpec); }

對于getChildMeasureSpec()這個方法,就是根據(jù)父類的測量法則和 子組件的LayoutParams 來確定子組件的測量法則,然后傳給子組件,其實就是上面介紹 onMeasure(int widthMeasureSpec, int heightMeasureSpec)的兩個參數(shù),因為child.measure()這個方法最終是會調(diào)用view.onMeasure()的。

2. onLayout()

這是一個抽象方法,如果繼承了viewGroup就需要去實現(xiàn),上面也簡單的介紹了,其實這個方法就是確定子組件在viewGroup的位置,并且是通過遍歷所有的childview,然后通過childview.layout()這個方法來給子組件確定position具體是通過setFrame(l, t, r, b)來確定左上右下.下面是一個onLayout()方法的重寫。

//這個方法是我在看了鴻洋大神的博客寫的一個例子
 @Override protected void onLayout(boolean changed, int l, int t, int r, int b) {
 int cCount = getChildCount(); 
 int cHeight, cWidth; MarginLayoutParams cParams; 
 /** * 遍歷所有childView根據(jù)其寬和高,以及margin進行布局 */
 for (int i = 0; i < cCount; i++) 
{ 
 View childView = getChildAt(i);
 cWidth = childView.getMeasuredWidth();
 cHeight = childView.getMeasuredHeight();
 cParams = (MarginLayoutParams) childView.getLayoutParams(); 
 int cl = 0, ct = 0, cr = 0, cb = 0; 
 switch (i){ 
 case 0: 
     cl = cParams.leftMargin + getPaddingLeft();
     ct = cParams.topMargin +getPaddingTop(); 
     break; 
case 1:
     ct = cParams.topMargin+getPaddingTop();
     cl = getWidth() - cParams.rightMargin - cWidth -getPaddingRight();
     break; 
case 2: 
     cl = cParams.leftMargin + getPaddingLeft(); 
     ct = getHeight() - cParams.bottomMargin - cHeight -  getPaddingBottom();
     break; 
case 3: 
     cl = getWidth()- cParams.rightMargin - cWidth - getPaddingRight(); 
     ct = getHeight() - cParams.bottomMargin - cHeight -       getPaddingBottom();
     break; 
    }  
     cr = cl + cWidth; cb = ct + cHeight; childView.layout(cl,ct,cr,cb); 
  } 
}

這是效果圖


QQ截圖20160426171159.png

**3. dispatchDraw() **
對于view樹的繪制來說,最先調(diào)用的還是draw()方法,這個方法必須在layout完成后調(diào)用,而這個方法有6步:

  1. Draw the background // 畫背景
  2. If necessary, save the canvas’ layers to prepare for fading // 不是必需的,為后面第5步繪制做準備工作
  3. Draw view’s content // 畫內(nèi)容部分
  4. Draw children //繪制子組件部分
  5. If necessary, draw the fading edges and restore layers //繪制fade效果
  6. Draw decorations (scrollbars for instance) //對應方法onDrawForeground(canvas); 和scrollbar有關(guān),譬如listview的scrollbar,該層是在最外面的一層,也是最后繪制的。

第一步 drawBackground(canvas); 這個方法主要是設(shè)置背景的邊框 setBackgroundBounds();
第二步和第五步不是必須的,沒有仔細去分析,就不誤人了
第三步 onDraw(canvas) 就是view的onDraw()方法,這肯定是內(nèi)容部分
第四步 // Step 4, draw the children dispatchDraw(canvas); 繪制子組件看到?jīng)]有,就是這個方法
整個view樹外層是decorview,繪制過程是調(diào)用draw(),有背景就先繪制背景,對于viewGroup來說不需要實現(xiàn)onDraw()方法,所以會跳過這一步,來到了dispatchDraw(canvas);這個方法會調(diào)用drawChild(canvas, transientChild, drawingTime);來為每個子view進行繪制,而在子viewdrawchild()方法中有這一段代碼
// Fast path for layouts with no backgrounds if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW){ mPrivateFlags &= ~PFLAG_DIRTY_MASK; dispatchDraw(canvas); } else { draw(canvas); }
如果子view沒有背景就會調(diào)用dispatchDraw(canvas),有背景就調(diào)用draw(canvas); 所以這也是為什么在view這個類中 dispatchDraw(canvas)是個空函數(shù),而在viewGroup中卻實現(xiàn)了這個函數(shù),這個方法不建議重寫,有需要實現(xiàn)更多的邏輯的話,可以重載。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內(nèi)容