前言
最近在研究view的整個事件過程,以及自定義view的繪制,今天突然發(fā)現還有dispatchDraw(),于是在官方api中找到了draw()方法。這兩個方法分別用于view繪制和viewgroup繪制,通常我們實現更多的是view繪制,而viewgroup繪制 默認是關閉狀態(tài),需要設置setWillNotDraw(false),什么意思呢?就是設置繪制為打開狀態(tài)。下面描述一下view定義以及viewgroup定義的流程,最后對比自定義view以及viewgroup有什么區(qū)別。
自定義view:
通常我們自定義一個view只需要先測量,然后確定要繪制的位置,位置確定了就可以開始繪制了,測量只需要用onMeasure(), 設置view內部元素(后面直接稱之為元素)的位置可以寫在onSizechange()方法里(盡管元素可以在onMeasure()方法中設置Rect 的left,top,right,bottom來確定繪制的區(qū)域,但是onMeasure()方法主要功能是測量),繪制操作對應onDraw()根據需要繪制元素來進行繪制,所以自定義一個view一般會涉及到onMeasure(),onDraw(),以及onSizeChange() ( 這個方法是計算過程中Size改變會回調 )這幾個方法,當然自定義view通常也涉及事件處理,只需要重寫onTouchEvent(),下面我介紹下我對這幾個方法的理解:
1. onMeasure()
顧名思義這是一個測量方法,用來測量view的大小,如果不重寫這個方法,該view的父view會在onMeasure()方法中調用
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(),
widthMeasureSpec)
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
這是系統(tǒng)給的默認測量方式,并且默認方式的測量還和背景圖片有關,如果背景足夠大的話是會影響view的大小。當然我們也可以根據自己的需要去重寫setMeasuredDimension(widthsize,heightsize),測量方式通常由自己設置的LayoutParams和外層父ViewGroup的測量模式共同決定,至于為什么和父布局有關,這是因為viewGroup會被內部view提供測量模式,后面會在ViewGroup的measure()方法中會提到。
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth()); }
我們在使用的時候可以根據自定義的邏輯要求,對其中的bitmap,text等等寬高進行計算,來確定最終要傳給
setMeasuredDimension(widthsize,heightsize)的寬高。
2. onDraw()
打一個比方,自定義view其實就像畫畫,需要畫布,畫筆,確定畫的位置。 onDraw()扮演著畫的動作,通常我們需要重寫這個方法,因為父view的onDraw()方法是個空方法當然它只提供畫布canvas,具體要畫什么圖案,畫多大,以及用什么樣的畫筆,不建議在這個方法做初始化工作,因為調用invalidate()方法會使這個方法重繪,我們可以在自定義view的構造函數寫一個init()方法,用來實現畫筆及一些繪制工作需要用到的“工具”的初始化工作,在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確定布局的意思是給內部子view設置layout,然后再去繪制,因為view是單獨存在的,不存在子view只存在內部需要繪制的元素,所以view和viewgroup的概念是不一樣的。而viewGroup可能存在多個子view,所以需要給每個子view來指定具體位置,這就是onLayout()所做的事了,同樣,測量也是用到onMeasure(),但是它和子view的onMeasure()不同,子view的測量只要測量這個view按照測量法則需要多大的空間,而viewgroup的測量則是根據子view的測量結果來進行統(tǒng)計,最終來確定整個viewgroup的widthsize和heightsize。viewgroup的繪制會用到dispatchDraw(),它主要是分發(fā)給子組件進行繪制,調用子組件的draw()方法,通常viewgroup是不用去繪制的,而且繪制功能默認關閉。
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);
// 調用子組件的measure()方法
child.measure(childWidthMeasureSpec, childHeightMeasureSpec); }
對于getChildMeasureSpec()這個方法,就是根據父類的測量法則和 子組件的LayoutParams 來確定子組件的測量法則,然后傳給子組件,其實就是上面介紹 onMeasure(int widthMeasureSpec, int heightMeasureSpec)的兩個參數,因為child.measure()這個方法最終是會調用view.onMeasure()的。
2. onLayout()
這是一個抽象方法,如果繼承了viewGroup就需要去實現,上面也簡單的介紹了,其實這個方法就是確定子組件在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根據其寬和高,以及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);
}
}
這是效果圖
**3. dispatchDraw() **
對于view樹的繪制來說,最先調用的還是draw()方法,這個方法必須在layout完成后調用,而這個方法有6步:
- Draw the background // 畫背景
- If necessary, save the canvas’ layers to prepare for fading // 不是必需的,為后面第5步繪制做準備工作
- Draw view’s content // 畫內容部分
- Draw children //繪制子組件部分
- If necessary, draw the fading edges and restore layers //繪制fade效果
- Draw decorations (scrollbars for instance) //對應方法onDrawForeground(canvas); 和scrollbar有關,譬如listview的scrollbar,該層是在最外面的一層,也是最后繪制的。
第一步 drawBackground(canvas); 這個方法主要是設置背景的邊框 setBackgroundBounds();
第二步和第五步不是必須的,沒有仔細去分析,就不誤人了
第三步 onDraw(canvas) 就是view的onDraw()方法,這肯定是內容部分
第四步 // Step 4, draw the children dispatchDraw(canvas); 繪制子組件看到沒有,就是這個方法
整個view樹外層是decorview,繪制過程是調用draw(),有背景就先繪制背景,對于viewGroup來說不需要實現onDraw()方法,所以會跳過這一步,來到了dispatchDraw(canvas);這個方法會調用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沒有背景就會調用dispatchDraw(canvas),有背景就調用draw(canvas); 所以這也是為什么在view這個類中 dispatchDraw(canvas)是個空函數,而在viewGroup中卻實現了這個函數,這個方法不建議重寫,有需要實現更多的邏輯的話,可以重載。