前言
最近在研究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()方法主要功能是測量)煮落,繪制操作對應(yīng)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);
}
}
這是效果圖
**3. dispatchDraw() **
對于view樹的繪制來說,最先調(diào)用的還是draw()方法挨队,這個方法必須在layout完成后調(diào)用谷暮,而這個方法有6步:
- Draw the background // 畫背景
- If necessary, save the canvas’ layers to prepare for fading // 不是必需的,為后面第5步繪制做準備工作
- Draw view’s content // 畫內(nèi)容部分
- Draw children //繪制子組件部分
- If necessary, draw the fading edges and restore layers //繪制fade效果
- Draw decorations (scrollbars for instance) //對應(yīng)方法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)更多的邏輯的話,可以重載鼓黔。