Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android View的工作原理

Android View的工作原理

編輯:關於Android編程

Android中控件大致被分為兩類ViewGroup,View。ViewGroup作為容器管理View。Android視圖,是類似於Dom樹的架構。父視圖負責測量定位繪制等操作。我們經常在用的findViewById方法代價昂貴的原因,就是因為他負責至上而下遍歷整棵控件樹,來尋找View實例,在重復操作中盡量少用。現在在用的很多控件都是直接或者間接繼承自View的,如下圖。

\

 

Android UI界面架構

每個Activity包含一個PhoneWindow對象,PhoneWindow設置DecorView為應用窗口的根視圖。在裡面就是熟悉的TitleView和ContentView,沒錯,平時使用的setContentView()就是設置的ContentView。

\

 

為什麼調用requestWindowFeature()方法一定要在setContentView()方法調用之前?
當程序在onCreate()方法中調用setContentView()方法後,ActivityManagerService會回調onResume()方法,此時系統才會將整個DecorView添加到PhoneWindow中,並讓其顯示出來,從而完成界面的繪制.

Android是如何繪制View的?

當一個Activity啟動時,會被要求繪制出它的布局。Android框架會處理這個請求,當然前提是Activity提供了合理的布局。繪制從根視圖開始,從上至下遍歷整棵視圖樹,每一個ViewGroup負責讓自己的子View被繪制,每一個View負責繪制自己,通過draw()方法.繪制過程分三步走。

oMeasure

oLayout

oDraw

整個繪制流程是在ViewRoot中的performTraversals()方法展開的。部分源代碼如下。

 

private void performTraversals() {
    ......
    //最外層的根視圖的widthMeasureSpec和heightMeasureSpec由來
    //lp.width和lp.height在創建ViewGroup實例時等於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);
    ......
}

 

在繪制之前當然要知道view的尺寸和繪制。所以先進行measu和layout(測量和定位),如下圖。

\

Measure過程

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {  
    //....  
  
    //回調onMeasure()方法    
    onMeasure(widthMeasureSpec, heightMeasureSpec);  
     
    //more  
}

計算view的實際大小,獲得高寬存入mMeasuredHeight和mMeasureWidth,measure(int, int)傳入的兩個參數。MeasureSpec是一個32位int值,高2位為測量的模式,低30位為測量的大小。測量的模式可以分為以下三種(有問到)。

oEXACTLY
精確值模式,當layout_width或layout_height指定為具體數值,或者為match_parent時,系統使用EXACTLY。

oAT_MOST
最大值模式,指定為wrap_content時,控件的尺寸不能超過父控件允許的最大尺寸。

oUNSPECIFIED
不指定測量模式,View想多大就多大,一般不太使用。

根據上面的源碼可知,measure方法不可被重寫(final方法),自定義時需要重寫的是onMeasure方法

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

查看源碼可知最終的高寬是調用setMeasuredDimension()設定的,如果不重寫,默認是直接調用getDefaultSize獲取尺寸的。

使用View的getMeasuredWidth()和getMeasuredHeight()方法來獲取View測量的寬高,必須保證這兩個方法在onMeasure流程之後被調用才能返回有效值。

Layout過程

Layout方法就是用來確定view布局的位置,就好像你知道了一件東西的大小以後,總要知道位置才能畫上去。

mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());

layout獲取四個參數,左,上,右,下坐標,相對於父視圖而言。這裡可以看到,使用了剛剛測量的寬和高。

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設置坐標。如果坐標改變過了,則重新進行定位。如果是View對象,那麼onLayout是個空方法。因為定位是由ViewGroup確定的。

當layout結束以後getWidth()與getHeight()才會返回正確的值。

說到這裡,我相信很多朋友長久以來都會有一個疑問,getWidth()方法和getMeasureWidth()方法到底有什麼區別呢?它們的值好像永遠都是相同的。其實它們的值之所以會相同基本都是因為布局設計者的編碼習慣非常好,實際上它們之間的差別還是挺大的。

首先getMeasureWidth()方法在measure()過程結束後就可以獲取到了,而getWidth()方法要在layout()過程結束後才能獲取到。另外,getMeasureWidth()方法中的值是通過setMeasuredDimension()方法來進行設置的,而getWidth()方法中的值則是通過視圖右邊的坐標減去左邊的坐標計算出來的。Let me give an example,A widget is asked to measure itself, The widget says that it wants to be 200px by 200px.This is measuredWidth/height.Thethe width/height of the widgetis最終控件中內容的長寬值。

觀察SimpleLayout中onLayout()方法的代碼,這裡給子視圖的layout()方法傳入的四個參數分別是0、0、childView.getMeasuredWidth()和childView.getMeasuredHeight(),因此getWidth()方法得到的值就是childView.getMeasuredWidth() - 0 =childView.getMeasuredWidth() ,所以此時getWidth()方法和getMeasuredWidth() 得到的值就是相同的,但如果你將onLayout()方法中的代碼進行如下修改:

1.@Override  
2.protected void onLayout(boolean changed, int l, int t, int r, int b) {  
3.    if (getChildCount() > 0) {  
4.        View childView = getChildAt(0);  
5.        childView.layout(0, 0, 200, 200);  
6.    }  
}

這樣getWidth()方法得到的值就是200 - 0 = 200,不會再和getMeasuredWidth()的值相同了。當然這種做法充分不尊重measure()過程計算出的結果,通常情況下是不推薦這麼寫的。getHeight()與getMeasureHeight()、方法之間的關系同上,就不再重復分析了。

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

重點是第三步調用onDraw方法。其它幾步都是繪制一些邊邊角角的東西比如背景、scrollBar之類的。其中dispatchDraw,是用來遞歸調用子View,如果沒有則不需要。

onDraw方法是需要自己實現的,因為每個控件繪制的內容不同。主要用canvas對象進行繪制,這裡就不說了。

強調一點的就是,在這三個流程中,Google已經幫我們把draw()過程框架已經寫好了,自定義的ViewGroup只需要實現measure()過程和layout()過程即可 。

這三種情況,最終會直接或間接調用到三個函數,分別為invalidate(),requsetLaytout()以及requestFocus() ,接著這三個函數最終會調用到ViewRoot中的schedulTraversale()方法,該函數然後發起一個異步消息,消息處理中調用performTraverser()方法對整個View進行遍歷。

 

Android中Invalidate和postInvalidate和requestLayout的區別

 

requestLayout:當view確定自身已經不再適合現有的區域時,該view本身調用這個方法要求parent view重新調用他的onMeasure onLayout來對重新設置自己位置。特別的當view的layoutparameter發生改變,並且它的值還沒能應用到view上,這時候適合調用這個方法。

invalidate:View本身調用迫使view重畫。是在UI線程自身使用。

postInvalidate:是在非UI線程使用。

Android提供了Invalidate方法實現界面刷新,但是Invalidate不能直接在線程中調用,因為他是違背了單線程模型:Android UI操作並不是線程安全的,並且這些操作必須在UI線程中調用。

Android程序中可以使用的界面刷新方法有兩種,分別是利用Handler和利用postInvalidate()來實現在線程中刷新界面。

下面利用invalidate()刷新界面

實例化一個Handler對象,並重寫handleMessage方法調用invalidate()實現界面刷新;而在線程中通過sendMessage發送界面更新消息。

// 在onCreate()中開啟線程
	new Thread(new GameThread()).start();
	// 實例化一個handler
	Handler myHandler = new Handler() {
		// 接收到消息後處理
		public void handleMessage(Message msg) {
			switch (msg.what) {
			case Activity01.REFRESH:
				mGameView.invalidate(); // 刷新界面
				break;
			}
			super.handleMessage(msg);
		}
	};

	class GameThread implements Runnable {
		public void run() {
			while (!Thread.currentThread().isInterrupted()) {
				Message message = new Message();
				message.what = Activity01.REFRESH;
				// 發送消息
				Activity01.this.myHandler.sendMessage(message);
				try {
					Thread.sleep(100);
				} catch (InterruptedException e) {
					Thread.currentThread().interrupt();
				}
			}
		}
	}

使用postInvalidate()刷新界面

使用postInvalidate則比較簡單,不需要handler,直接在線程中調用postInvalidate即可。

 

class GameThread implements Runnable {  
        public void run(){  
            while(!Thread.currentThread().isInterrupted()) {  
                try{  
                    Thread.sleep(100 );  
                } catch(InterruptedException e) {  
                    Thread.currentThread().interrupt();  
                }  
                // 使用postInvalidate可以直接在線程中更新界面   
                mGameView.postInvalidate();  
            }  
        }  
    }

 

常見問題:

 

http://gold.xitu.io/entry/56f7eb386be3ff005cfcaaed

1.view的繪制流程分幾步,從哪開始?哪個過程結束以後能看到view?

View的繪制流程是從ViewRoot的performTraversals方法開始的,它經過measure、layout和draw三個過程才能最終將一個View繪制出來,其中measure用來測量View的寬和高,layout用來確定View在父容器中的放置位置,而draw則負責將View繪制在屏幕上。

2.view的測量寬高和實際寬高有區別嗎?

答:基本上百分之99的情況下都是可以認為沒有區別的。有兩種情況,有區別。第一種是在某些情況下,View需要多次measure才能確定自己的測量寬/高,在前幾次的測量過程中,其得出的測量寬/高有可能和最終寬/高不一致,但最終來說,測量寬/高還是和最終寬/高相同的。第二種情況,實際寬高是在layout流程裡確定的,我們可以在layout流程裡 將實際寬高寫死 寫成硬編碼,這樣測量的寬高和實際寬高就肯定不一樣了,雖然這麼做沒有意義 而且也不好。

3.view的measureSpec 由誰決定?頂級view呢?

對於DecorView,其MeasureSpec由窗口尺寸和其自身的LayoutParams來共同決定;

對於普通的View,其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams來共同決定,MeasureSpec一旦確定後,onMeasure中就可以確定View的測量寬/高。

4.measure過程

measure過程分情況來看,如果只是一個原始的VIew,那麼通過measure方法就完成了其測量的過程,如果是一個VIewGroup,除了完成自己的測量過程外,還會遍歷去調用所有子元素的measure方法,各個子元素在遞歸去執行這個流程。

(1)View的measure 過程

View的measure過程由其measure方法完成,measure方法是一個final類型的方法,這意味著子類不能重寫此方法,在view的measure方法中會去調用view的onmeasure方法。

(2)ViewGroup的measure 過程

對於VIewGroup來說,除了完成自己的measure過程之外,還會遍歷去調用所有子元素的measure方法,各個子元素在遞歸去執行這個過程。和View不同的是,ViewGroup是一個抽象類,因此它並沒有重寫View的onMeasure方法,但是它提供了一個叫measureChildren的方法。

measureChildren的思想就是取出元素的LayoutParams,然後再通過getChildMeasureSpec來創建子元素的MeasureSpec,接著將MeasureSpec直接傳遞給View的Measure方法來進行測量。

ViewGroup並沒有定義其測量的具體過程,這是因為ViewGroup是一個抽象類,其測量過程的OnMeasure方法需要各個子類去具體實現,比如LinearLayout、RelativeLayout等,為什麼ViewGroup不像VIew一樣直接對其onMeasure方法做統一的實現呢?那是因為不同的VIewGroup子類有不同的布局特性,這導致它們的測量細節哥不相同,ViewGroup無法統一實現。

5.自定義view中 如果onMeasure方法 沒有對wrap_content 做處理 會發生什麼?為什麼?怎麼解決?

答:如果沒有對wrap_content做處理 ,那即使你在xml裡設置為wrap_content.其效果也和match_parent相同。看問題4的分析。我們可以知道view自己的layout為wrap,那mode就是at_most(不管父親view是什麼specmode).

這種模式下寬高就是等於specSize(getDefaultSize函數分析可知),而這裡的specSize顯然就是parentSize的大小。也就是父容器剩余的大小。那不就和我們直接設置成match_parent是一樣的效果了麼?

解決方式就是在onMeasure裡 針對wrap 來做特殊處理 比如指定一個默認的寬高,當發現是wrap_content 就設置這個默認寬高即可。

6.為什麼在activity的生命周期裡無法獲得測量寬高?有什麼方法可以解決這個問題嗎?

答:因為measure的過程和activity的生命周期 沒有任何關系。實際上onCreat、onStart、OnResume中均無法無法正確的得到某個View的寬/高信息,你無法確定在哪個生命周期執行完畢以後 view的measure過程一定走完。如果VIew還沒有測量完畢,那麼獲得的寬/高就是0。

可以嘗試如下幾種方法 獲取view的測量寬高。

(1)Activity/View # onWindowsFocusChanged

onWindowsFocusChanged這個方法的含義就是:View已經初始化完畢了,寬/高已經准備好了,這個時候去獲取寬/高是沒有問題的。

\

(2)View.post(runnable)

通過post可以將一個runnable投遞到消息隊列的尾部,然後等待Looper調用此runnable的時候,View已經初始化好了。

\

7.layout和onLayout方法有什麼區別?

答:layout是確定本身view的位置 而onLayout是確定所有子元素的位置。layout裡面 就是通過serFrame方法設設定本身view的 四個頂點的位置。這4個位置以確定 自己view的位置就固定了

然後就調用onLayout來確定子元素的位置。view和viewgroup的onlayout方法都沒有寫。都留給我們自己給子元素布局

8.setWillNotDraw方法有什麼用?

答:這個方法在view裡。

用於設置標志位的 也就是說 如果你的自定義view 不需要draw的話,就可以設置這個方法為true。這樣系統知道你這個view 不需要draw 可以優化執行速度。viewgroup 一般都默認設置這個為true,因為viewgroup多數都是只負責布局

不負責draw的。而view 這個標志位 默認一般都是關閉的.

9.自定義view 有哪些需要注意的點?

答:主要是要處理wrap_content 和padding。否則xml 那邊設置這2個屬性就根本沒用了。還有不要在view中使用handler 因為人家已經提供了post方法。如果是繼承自viewGroup,那在onMeasure和onLayout裡面 也要考慮padding和layout的影響。也就是說specSize 要算一下 。最後就是如果view的動畫或者線程需要停止,可以考慮在onDetachedFromWindow裡面來做。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved