Android制霸控件View總結(jié)
關(guān)于Android View控件
Android中控件大致被分為兩類ViewGroup,View。ViewGroup作為容器管理View。Android視圖,是類似于Dom樹的架構(gòu)。父視圖負(fù)責(zé)測量定位繪制等操作。我們經(jīng)常在用的findViewById方法代價(jià)昂貴的原因,就是因?yàn)樗?fù)責(zé)至上而下遍歷整棵控件樹,來尋找View實(shí)例,在重復(fù)操作中盡量少用。現(xiàn)在在用的很多控件都是直接或者間接繼承自View的,如下圖。
view 繼承樹
Android UI界面架構(gòu)
每個(gè)Activity包含一個(gè)PhoneWindow對(duì)象,PhoneWindow設(shè)置DecorView為應(yīng)用窗口的根視圖。在里面就是熟悉的TitleView和ContentView,沒錯(cuò),平時(shí)使用的setContentView()就是設(shè)置的ContentView。
UI 架構(gòu)
Android是如何繪制View的?
當(dāng)一個(gè)Activity啟動(dòng)時(shí),會(huì)被要求繪制出它的布局。Android框架會(huì)處理這個(gè)請(qǐng)求,當(dāng)然前提是Activity提供了合理的布局。繪制從根視圖開始,從上至下遍歷整棵視圖樹,每一個(gè)ViewGroup負(fù)責(zé)讓自己的子View被繪制,每一個(gè)View負(fù)責(zé)繪制自己,通過draw()方法,繪制過程分三步走。
- Measure
- Layout
- Draw
整個(gè)繪制流程是在ViewRoot中的performTraversals()方法展開的。部分源代碼如下。
- private void performTraversals() {
- ......
- //最外層的根視圖的widthMeasureSpec和heightMeasureSpec由來
- //lp.width和lp.height在創(chuàng)建ViewGroup實(shí)例時(shí)等于MATCH_PARENT
- int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
- int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
- ......
- mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
- ......
- mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
- ......
- mView.draw(canvas);
- ......
- }
在繪制之前當(dāng)然要知道view的尺寸和繪制。所以先進(jìn)行measu和layout(測量和定位),如下圖。
繪制流程
Measure過程
- public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
- //....
- //回調(diào)onMeasure()方法
- onMeasure(widthMeasureSpec, heightMeasureSpec);
- //more
- }
計(jì)算view的實(shí)際大小,獲得高寬存入mMeasuredHeight和mMeasureWidth,measure(int, int)傳入的兩個(gè)參數(shù)。MeasureSpec是一個(gè)32位int值,高2位為測量的模式,低30位為測量的大小。測量的模式可以分為以下三種。
-
EXACTLY
精確值模式,當(dāng)layout_width或layout_height指定為具體數(shù)值,或者為match_parent時(shí),系統(tǒng)使用EXACTLY。 -
AT_MOST
***值模式,指定為wrap_content時(shí),控件的尺寸不能超過父控件允許的***尺寸。 -
UNSPECIFIED
不指定測量模式,View想多大就多大,一般不太使用。
根據(jù)上面的源碼可知,measure方法不可被重寫,自定義時(shí)需要重寫的是onMeasure方法。
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
- getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
- }
查看源碼可知,最終的高寬是調(diào)用setMeasuredDimension()設(shè)定的,如果不重寫,默認(rèn)是直接調(diào)用getDefaultSize獲取尺寸的。
使用View的getMeasuredWidth()和getMeasuredHeight()方法來獲取View測量的寬高,必須保證這兩個(gè)方法在onMeasure流程之后被調(diào)用才能返回有效值。
Layout過程
Layout方法就是用來確定view布局的位置,就好像你知道了一件東西的大小以后,總要知道位置才能畫上去。
- mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
layout獲取四個(gè)參數(shù),左,上,右,下坐標(biāo),相對(duì)于父視圖而言。這里可以看到,使用了剛剛測量的寬和高。
- public void layout(int l, int t, int r, int b) {
- int oldL = mLeft;
- int oldT = mTop;
- int oldB = mBottom;
- int oldR = mRight;
- boolean changed = setFrame(l, t, r, b);
- if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) {
- .....
- onLayout(changed, l, t, r, b);
- .....
- }
通過setFrame設(shè)置坐標(biāo)。如果坐標(biāo)改變過了,則重新進(jìn)行定位。如果是View對(duì)象,那么onLayout是個(gè)空方法。因?yàn)槎ㄎ皇怯蒝iewGroup確定的。
當(dāng)layout結(jié)束以后getWidth()與getHeight()才會(huì)返回正確的值。
這里出現(xiàn)一個(gè)問題,getWidth/Height() 和 getMeasuredWidth/Height()有什么區(qū)別?
- getWidth():View在設(shè)定好布局后View的寬度。
- getMeasuredWidth():對(duì)View上的內(nèi)容進(jìn)行測量后得到的View內(nèi)容占據(jù)的寬度。
getwidth
Draw過程
- public void draw(Canvas canvas) {
- ......
- /*
- * 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
- ......
- if (!dirtyOpaque) {
- drawBackground(canvas);
- }
- // skip step 2 & 5 if possible (common case)
- ......
- // Step 2, save the canvas' layers
- ......
- if (drawTop) {
- canvas.saveLayer(left, top, right, top + length, null, flags);
- }
- ......
- // Step 3, draw the content
- if (!dirtyOpaque) onDraw(canvas);
- // Step 4, draw the children
- dispatchDraw(canvas);
- // Step 5, draw the fade effect and restore layers
- ......
- if (drawTop) {
- matrix.setScale(1, fadeHeight * topFadeStrength);
- matrix.postTranslate(left, top);
- fade.setLocalMatrix(matrix);
- p.setShader(fade);
- canvas.drawRect(left, top, right, top + length, p);
- }
- ......
- // Step 6, draw decorations (scrollbars)
- onDrawScrollBars(canvas);
- ......
- }
重點(diǎn)是第三步調(diào)用onDraw方法。其它幾步都是繪制一些邊邊角角的東西比如背景、scrollBar之類的。其中dispatchDraw,是用來遞歸調(diào)用子View,如果沒有則不需要。
onDraw方法是需要自己實(shí)現(xiàn)的,因?yàn)槊總€(gè)控件繪制的內(nèi)容不同。主要用canvas對(duì)象進(jìn)行繪制,這里就不說了。
參考資料