Android View總結

weixin_33860722發表於2016-03-10

作者:threezj
原文地址:Android View總結

關於Android View控制元件

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

675733-c283dddc618cd45a.jpg
view 繼承樹

Android UI介面架構

每個Activity包含一個PhoneWindow物件,PhoneWindow設定DecorView為應用視窗的根檢視。在裡面就是熟悉的TitleViewContentView,沒錯,平時使用的setContentView()就是設定的ContentView

675733-0878cce3f2f0cc38.png
UI 架構

Android是如何繪製View的?

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

  • Measure
  • Layout
  • Draw

整個繪製流程是在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的尺寸和繪製。所以先進行measulayout(測量和定位),如下圖。

675733-8a68311ed761c769.gif
繪製流程

Measure過程

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

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

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

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

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

根據上面的原始碼可知,measure方法不可被重寫,自定義時需要重寫的是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/Height()getMeasuredWidth/Height()有什麼區別?

  • getWidth():View在設定好佈局後View的寬度。
  • getMeasuredWidth():對View上的內容進行測量後得到的View內容佔據的寬度。
675733-121617c3bca3357d
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);
        ......
    }

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

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

參考資料

  1. Android檢視繪製流程完全解析,帶你一步步深入瞭解View(二)
  2. Android應用層View繪製流程與原始碼分析
  3. How Android Draws Views
  4. 《Android群英傳》
  5. What is the difference between getWidth/Height() and getMeasuredWidth/Height() in Android SDK?

相關文章