Android-視圖繪製

來源:互聯網
上載者:User

標籤:

http://blog.csdn.net/guolin_blog/article/details/16330267

 

任何一個視圖都不可能憑空突然出現在螢幕上,它們都是要經過非常科學的繪製流程後才能顯示出來的。每一個視圖的繪製過程都必須經曆三個最主要的階段,即onMeasure()、onLayout()和onDraw(),下面我們逐個對這三個階段展開進行探討。

 

onMeasure()用於測量視圖大小

onLayout()用於確定視圖位置

onDraw()用於繪製視圖

 

一. onMeasure()

measure是測量的意思,那麼onMeasure()方法顧名思義就是用於測量視圖的大小的

View系統的繪製流程會從ViewRoot的performTraversals()方法中開始,在其內部調用View的measure()方法。measure()方法接收兩個參數,widthMeasureSpec和heightMeasureSpec,這兩個值分別用於確定視圖的寬度和高度的規格和大小。

MeasureSpec的值由specSize和specMode共同組成的,其中specSize記錄的是大小,specMode記錄的是規格。specMode一共有三種類型,如下所示:

1. EXACTLY

表示父視圖希望子視圖的大小應該是由specSize的值來決定的,系統預設會按照這個規則來設定子視圖的大小,開發人員當然也可以按照自己的意願設定成任意的大小。

2. AT_MOST

表示子視圖最多隻能是specSize中指定的大小,開發人員應該儘可能小得去設定這個視圖,並且保證不會超過specSize。系統預設會按照這個規則來設定子視圖的大小,開發人員當然也可以按照自己的意願設定成任意的大小。

3. UNSPECIFIED

表示開發人員可以將視圖按照自己的意願設定成任意的大小,沒有任何限制。這種情況比較少見,不太會用到。

那麼你可能會有疑問了,widthMeasureSpec和heightMeasureSpec這兩個值又是從哪裡得到的呢?通常情況下,這兩個值都是由父視圖經過計算後傳遞給子視圖的,說明父視圖會在一定程度上決定子視圖的大小。但是最外層的根視圖,它的widthMeasureSpec和heightMeasureSpec又是從哪裡得到的呢?這就需要去分析ViewRoot中的源碼了,觀察performTraversals()方法可以發現如下代碼:

childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);  childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);  

可以看到,這裡調用了getRootMeasureSpec()方法去擷取widthMeasureSpec和heightMeasureSpec的值,注意方法中傳入的參數,其中lp.width和lp.height在建立ViewGroup執行個體的時候就被賦值了,它們都等於MATCH_PARENT。然後看下getRootMeasureSpec()方法中的代碼,如下所示:

private int getRootMeasureSpec(int windowSize, int rootDimension) {      int measureSpec;      switch (rootDimension) {      case ViewGroup.LayoutParams.MATCH_PARENT:          measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);          break;      case ViewGroup.LayoutParams.WRAP_CONTENT:          measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);          break;      default:          measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);          break;      }      return measureSpec;  }  

可以看到,這裡使用了MeasureSpec.makeMeasureSpec()方法來組裝一個MeasureSpec,當rootDimension參數等於MATCH_PARENT的時候,MeasureSpec的specMode就等於EXACTLY,當rootDimension等於WRAP_CONTENT的時候,MeasureSpec的specMode就等於AT_MOST。並且MATCH_PARENT和WRAP_CONTENT時的specSize都是等於windowSize的,也就意味著根視圖總是會充滿全屏的。

 

接下來看View裡的measure方法:

注意觀察,measure()這個方法是final的,因此我們無法在子類中去重寫這個方法,說明Android是不允許我們改變View的measure架構的。其中調用了onMeasure()方法,這裡才是真正去測量並設定View大小的地方,預設會調用getDefaultSize()方法來擷取視圖的大小,如下所示:

public static int getDefaultSize(int size, int measureSpec) {      int result = size;      int specMode = MeasureSpec.getMode(measureSpec);      int specSize = MeasureSpec.getSize(measureSpec);      switch (specMode) {      case MeasureSpec.UNSPECIFIED:          result = size;          break;      case MeasureSpec.AT_MOST:      case MeasureSpec.EXACTLY:          result = specSize;          break;      }      return result;  }  

這裡傳入的measureSpec是一直從measure()方法中傳遞過來的。然後調用MeasureSpec.getMode()方法可以解析出specMode,調用MeasureSpec.getSize()方法可以解析出specSize。接下來進行判斷,如果specMode等於AT_MOST或EXACTLY就返回specSize,這也是系統預設的行為。之後會在onMeasure()方法中調用setMeasuredDimension()方法來設定測量出的大小,這樣一次measure過程就結束了。

 

由上面我們可以知道:

 

MeasureSpec是父視圖傳遞給子視圖的布局要求。每個MeasureSpec代表了一組寬度和高度的要求。一個MeasureSpec由大小和模式組成。

一個View的measure:

 performTraversals() -> 

private int getRootMeasureSpec(int windowSize, int rootDimension) ->      //得到根視圖的MeasureSpec

public final void measure(int widthMeasureSpec, int heightMeasureSpec)->   //其中參數為父視圖傳遞下來的

onMeasure() -> public static int getDefaultSize(int size, int measureSpec)     //設定視圖大小

 

當然,一個介面的展示可能會涉及到很多次的measure,因為一個布局中一般都會包含多個子視圖,每個視圖都需要經曆一次measure過程。ViewGroup中定義了一個measureChildren()方法來去測量子視圖的大小,如下所示:

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) {      final LayoutParams lp = child.getLayoutParams();      final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,              mPaddingLeft + mPaddingRight, lp.width);      final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,              mPaddingTop + mPaddingBottom, lp.height);      child.measure(childWidthMeasureSpec, childHeightMeasureSpec);  }  

可以看到,在第4行和第6行分別調用了getChildMeasureSpec()方法來去計運算元視圖的MeasureSpec,計算的依據就是布局檔案中定義的MATCH_PARENT、WRAP_CONTENT等值,這個方法的內部細節就不再貼出。然後在第8行調用子視圖的measure()方法,並把計算出的MeasureSpec傳遞進去,之後的流程就和前面所介紹的一樣了。

 

當然,onMeasure()方法是可以重寫的,也就是說,如果你不想使用系統預設的測量方式,可以按照自己的意願進行定製,比如:

public class MyView extends View {        ......            @Override      protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {          setMeasuredDimension(200, 200);      }    }  

這樣的話就把View預設的測量流程覆蓋掉了,不管在布局檔案中定義MyView這個視圖的大小是多少,最終在介面上顯示的大小都將會是200*200。

需要注意的是,在setMeasuredDimension()方法調用之後,我們才能使用getMeasuredWidth()和getMeasuredHeight()來擷取視圖測量出的寬高,以此之前調用這兩個方法得到的值都會是0。

由此可見,視圖大小的控制是由父視圖、布局檔案、以及視圖本身共同完成的,父視圖會提供給子視圖參考的大小,而開發人員可以在XML檔案中指定視圖的大小,然後視圖本身會對最終的大小進行拍板。

到此為止,我們就把視圖繪製流程的第一階段分析完了。

 

二. onLayout()

measure過程結束後,視圖的大小就已經測量好了,接下來就是layout的過程了。正如其名字所描述的一樣,這個方法是用於給視圖進行布局的,也就是確定視圖的位置。ViewRoot的performTraversals()方法會在measure結束後繼續執行,並調用View的layout()方法來執行此過程,如下所示:

host.layout(0, 0, host.mMeasuredWidth, host.mMeasuredHeight);  

layout()方法接收四個參數,分別代表著左、上、右、下的座標,當然這個座標是相對於當前視圖的父視圖而言的。可以看到,這裡還把剛才測量出的寬度和高度傳到了layout()方法中。那麼我們來看下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) {          if (ViewDebug.TRACE_HIERARCHY) {              ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_LAYOUT);          }          onLayout(changed, l, t, r, b);          mPrivateFlags &= ~LAYOUT_REQUIRED;          if (mOnLayoutChangeListeners != null) {              ArrayList<OnLayoutChangeListener> listenersCopy =                      (ArrayList<OnLayoutChangeListener>) mOnLayoutChangeListeners.clone();              int numListeners = listenersCopy.size();              for (int i = 0; i < numListeners; ++i) {                  listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);              }          }      }      mPrivateFlags &= ~FORCE_LAYOUT;  }  

在layout()方法中,首先會調用setFrame()方法來判斷視圖的大小是否發生過變化,以確定有沒有必要對當前的視圖進行重繪,同時還會在這裡把傳遞過來的四個參數分別賦值給mLeft、mTop、mRight和mBottom這幾個變數。接下來會在第11行調用onLayout()方法,正如onMeasure()方法中的預設行為一樣,也許你已經迫不及待地想知道onLayout()方法中的預設行為是什麼樣的了。進入onLayout()方法,咦?怎麼這是個空方法,一行代碼都沒有?!

沒錯,View中的onLayout()方法就是一個空方法,因為onLayout()過程是為了確定視圖在布局中所在的位置,而這個操作應該是由布局來完成的,即父視圖決定子視圖的顯示位置。既然如此,我們來看下ViewGroup中的onLayout()方法是怎麼寫的吧,代碼如下:

@Override  protected abstract void onLayout(boolean changed, int l, int t, int r, int b);  

可以看到,ViewGroup中的onLayout()方法竟然是一個抽象方法,這就意味著所有ViewGroup的子類都必須重寫這個方法。沒錯,像LinearLayout、RelativeLayout等布局,都是重寫了這個方法,然後在內部按照各自的規則對子視圖進行布局的

 

Android-視圖繪製

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.