android viewgroup全面解析

轉自開源中國,目前遇到的寫viewgroup最詳細的文章


一個Viewgroup基本的繼承類格式如下:

 1 import android.content.Context;
 2 import android.view.ViewGroup;
 3 
 4 public class MyViewGroup extends ViewGroup{
 5 
 6     public MyViewGroup(Context context) {
 7         super(context);
 8         // TODO Auto-generated constructor stub
 9     }
10 
11     @Override
12     protected void onLayout(boolean changed, int l, int t, int r, int b) {
13         // TODO Auto-generated method stub
14         
15     }
16 } 

如上所示,onLayout這個方法是必須要求實現的(後面具體講解)

假設現在如下使用這個類:

 1 package com.example.myviewgroup;
 2 
 3 import android.os.Bundle;
 4 import android.widget.ImageView;
 5 import android.app.Activity;
 6 import android.graphics.Color;
 7 
 8 public class MainActivity extends Activity {
 9     MyViewGroup group;
10     ImageView imageView;
11 
12     @Override
13     public void onCreate(Bundle savedInstanceState) {
14         super.onCreate(savedInstanceState);
15         
16         group = new MyViewGroup(MainActivity.this);
17         imageView = new ImageView(this);
18         imageView.setBackgroundResource(R.drawable.ic_launcher);
19         group.addView(imageView);
20         group.setBackgroundColor(Color.GREEN);
21         setContentView(group);
22     } 
23 } 

你會發現界面上什麼都沒有,只是一片綠色,也就是說,子元素根本就沒有被繪製上去。注意到上面有一個要求重載的方法onLayout(),重載如下:

1     @Override
2     protected void onLayout(boolean changed, int l, int t, int r, int b) {
3         // TODO Auto-generated method stub
4         for(int index = 0; index < getChildCount(); index++){
5             View v = getChildAt(index);
6             v.layout(l, t, r, b);
7         } 

這個時候圖像就能顯示出來了。看代碼應該能基本理解原因,我們給每一個child都設定了它的現實範圍,使用的方法是layout,當然這裏只是顯示了一個View,這裏只是基本。上面傳進去的四個參數分別代表着ViewGroup在整個界面上的上下左右邊框,也就是說,它框定了ViewGroup的可視化範圍,我們要做的就是在這個範圍裏面安排我們的子View。再繼續,假設我們這樣使用自定義的ViewGroup:

 1 package com.example.myviewgroup;
 2 
 3 import android.os.Bundle;
 4 import android.widget.ImageView;
 5 import android.widget.LinearLayout;
 6 import android.widget.TextView;
 7 import android.app.Activity;
 8 import android.graphics.Color;
 9 
10 public class MainActivity extends Activity {
11     LinearLayout layout;
12     
13     MyViewGroup group;
14     TextView textView;
15     ImageView imageView;
16     @Override
17     public void onCreate(Bundle savedInstanceState) {
18         super.onCreate(savedInstanceState);
19         
20         layout = new LinearLayout(this);
21         group = new MyViewGroup(this);
22         imageView = new ImageView(this);
23         textView = new TextView(this);
24         
25         imageView.setBackgroundResource(R.drawable.ic_launcher);
26         textView.setText("Hello");
27         
28         layout.setOrientation(LinearLayout.VERTICAL);
29         layout.setBackgroundColor(Color.WHITE);
30         
31         layout.addView(imageView);
32         layout.addView(textView);
33         group.addView(layout, new LinearLayout.LayoutParams(100, 100));
34         group.setBackgroundColor(Color.GREEN);
35         setContentView(group);
36     } 
37 } 

我們會發現,整個界面又和以前一樣,只顯示一片綠色了,組件又不見了,你可以嘗試改變layout的背景顏色,會發現最後顯示的界面顏色也變化了,所以可以判定,我們這樣子寫,只是顯示了最最外層的代碼,並沒有觸發整個佈局去繪製她自己的子View(這裏指的是imageView和textView)。前面說到onLayout方法提供整個組件的可視範圍以便於子View佈局,那麼子View的大小如何確定以及當子View是一個ViewGroup的時候怎麼觸發它去繪製自己的子View呢?這涉及ViewGroup的另外一個方法:

1 @Override
2     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
3         // TODO Auto-generated method stub
4         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
5     } 

這個方法來自View,而不是ViewGroup的,文檔解釋如下:

Measure>int, int) and should be overriden by subclasses to provide accurate and efficient measurement of their contents.

通俗解釋一下:這個方法其實是用來丈量View本身以及它自己的尺寸的!什麼意思呢?我們先看看傳入的參數是什麼。傳入的參數是兩個int,但是實際上這兩個int大有文章,是兩個int的&值,解釋如下:

兩個參數分別代表寬度和高度的MeasureSpec,android2.2文檔中對於MeasureSpec中的說明是: 一個MeasureSpec封裝了從父容器傳遞給子容器的佈局需求.每一個MeasureSpec代表了一個寬度,或者高度的說明.一個MeasureSpec是一個大小跟模式的組合值.一共有三種模式.

(1)UPSPECIFIED:父容器對於子容器沒有任何限制,子容器想要多大就多大.

(2) EXACTLY:父容器已經爲子容器設置了尺寸,子容器應當服從這些邊界,不論子容器想要多大的空間.

(3) AT_MOST:子容器可以是聲明大小內的任意大小.

暫時先這樣解釋着,後面再去細說。總之,這兩個參數傳進來的是本View(ViewGroup)顯示的長和寬的值和某個模式的&值,具體取出模式或者值的方法如下:

1         int widthMode = MeasureSpec.getMode(widthMeasureSpec); 
2         int heightMode = MeasureSpec.getMode(heightMeasureSpec); 
3            
4         int widthSize = MeasureSpec.getSize(widthMeasureSpec); 
5         int heightSize = MeasureSpec.getSize(heightMeasureSpec);  

而合成則可以使用下面的方法:

1 MeasureSpec.makeMeasureSpec(size, MeasureSpec.AT_MOST)

OK,上面是一些介紹,到這裏可能比較混亂,整理一下:

如果讓你在一個界面上繪製一個矩形,爲了準確的畫出這個矩形,你必須知道兩件事情:1)矩形的位置(暫定爲左上角的座標);2)尺寸(長和寬),Android繪製圖形的時候也要知道這兩件事情,前面已經介紹了幾個方法了,現在把它們聯繫起來(你可以想象,你用一個layoutA作爲contentView,然後在layoutA裏面要加一個button),Android會怎麼去做呢?最正規的解釋當然源自Android官方文檔:http://developer.android.com/guide/topics/ui/how-android-draws.html

首先看一下View樹的樣子:

我們的界面基本上就是以這樣子的方式組織展現的。

When an Activity receives focus, it will be requested to draw its layout. The Android framework will handle the procedure for drawing, but the Activity must provide the root node of its layout hierarchy.

(當一個Activity獲取焦點的時候,它就會被要求去畫出它的佈局。Android框架會處理繪畫過程,但是Activity必須提供佈局的根節點,在上面的圖上,我們可以理解爲最上面的ViewGroup,而實際上還有一個更深的root)

Drawing begins with the root node of the layout. It is requested to measure and draw the layout tree. Drawing is handled by walking the tree and rendering each View that intersects the invalid region. In turn, each View group is responsible for requesting each of its children to be drawn (with the draw() method) and each View is responsible for drawing itself. Because the tree is traversed in-order, this means that parents will be drawn before (i.e., behind) their children, with siblings drawn in the order they appear in the tree.

(繪畫開始於佈局的根節點,要求測量並且畫出整個佈局樹。繪畫通過遍歷整個樹來完成,不可見的區域的View被放棄。每個ViewGroup負責要求它的子View去繪畫,每個子View則負責去繪畫自己。因爲佈局樹是順序遍歷的,這意味着父View在子View之前被畫出來(這個符合常理,後面解釋))。

註解:假設一個TextView設置爲(FILL_PAREMT, FILL_PARENT),則很明顯必須先畫出父View的尺寸,才能去畫出這個TextView,而且從上至下也就是先畫父View再畫子View,顯示的時候才正常,否則父View會擋住子View的顯示。

Drawing the layout is a two pass process: a measure pass and a layout pass. The measuring pass is implemented in measure(int, int) and>layout(int, int, int, int) and is also top-down. During this pass each parent is responsible for positioning all of its children using the sizes computed in the measure pass.

(佈局繪畫涉及兩個過程:測量過程和佈局過程。測量過程通過measure方法實現,是View樹自頂向下的遍歷,每個View在循環過程中將尺寸細節往下傳遞,當測量過程完成之後,所有的View都存儲了自己的尺寸。第二個過程則是通過方法layout來實現的,也是自頂向下的。在這個過程中,每個父View負責通過計算好的尺寸放置它的子View。)

註解:這和前面說的一樣,一個過程是用來丈量尺寸的,一個過程是用來擺放位置的。

When a View's measure() method returns, its getMeasuredWidth() and getMeasuredHeight() values must be set, along with those for all of that View's descendants. A View's measured width and measured height values must respect the constraints imposed by the View's parents. This guarantees that at the end of the measure pass, all parents accept all of their children's measurements. A parent View may call measure() more than once on its children. For example, the parent may measure each child once with unspecified dimensions to find out how big they want to be, then call measure() on them again with actual numbers if the sum of all the children's unconstrained sizes is too big or too small (i.e., if the children don't agree among themselves as to how much space they each get, the parent will intervene and set the rules on the second pass).

(當一個View的measure()方法返回的時候,它的getMeasuredWidth和getMeasuredHeight方法的值一定是被設置好的。它所有的子節點同樣被設置好。一個View的測量寬和測量高一定要遵循父View的約束,這保證了在測量過程結束的時候,所有的父View可以接受子View的測量值。一個父View或許會多次調用子View的measure()方法。舉個例子,父View會使用不明確的尺寸去丈量看看子View到底需要多大,當子View總的尺寸太大或者太小的時候會再次使用實際的尺寸去調用onmeasure().)

The measure pass uses two classes to communicate dimensions. The ViewGroup.LayoutParams class is used by Views to tell their parents how they want to be measured and positioned. The base LayoutParams class just describes how big the View wants to be for both width and height. For each dimension, it can specify one of:

  • an exact number
  • FILL_PARENT, which means the View wants to be as big as its parent (minus padding)
  • WRAP_CONTENT, which means that the View wants to be just big enough to enclose its content (plus padding).

不解釋。

There are subclasses of LayoutParams for different subclasses of ViewGroup. For example, RelativeLayout has its own subclass of LayoutParams, which includes the ability to center child Views horizontally and vertically.

MeasureSpecs are used to push requirements down the tree from parent to child. A MeasureSpec can be in one of three modes:

  • UNSPECIFIED: This>For example, a LinearLayout may call measure() on its child with the height set to UNSPECIFIED and a width of EXACTLY240 to find out how tall the child View wants to be given a width of 240 pixels.
  • EXACTLY: This is used by the parent to impose an exact size on the child. The child must use this size, and guarantee that all of its descendants will fit within this size.
  • AT_MOST: This is used by the parent to impose a maximum size on the child. The child must guarantee that it and all of its descendants will fit within this size.

這裏前面已經提到過,也不多說,注意紅色部分,也就是說可以通過設置高爲一個確定值(通過EXACTLY)來看看子View在這個寬度下會怎麼確定自己的高度。

OKOK,再休息一下。上面的問題可以得到解決了,往重載的ViewGroup裏面添加Layout子View的時候,我們需要重載如下:

1     @Override
2     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
3             caculateWidthAndPadding(MeasureSpec.getSize(widthMeasureSpec));
4         for(int index = 0; index < getChildCount(); index++){
5 
6                 child.measure(MeasureSpec.makeMeasureSpec(childSize, MeasureSpec.AT_MOST), MeasureSpec.makeMeasureSpec(childSize, MeasureSpec.AT_MOST));
7         }
8         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
9     }

   
   
   
   

當然,具體的measure裏面傳入的參數你可以自己決定,我在這裏根據widthMeasureSpec計算出一個子View的寬度(childSize),然後告訴所有的childView,你使用的最大尺寸就是childSize,不能超過(通過childSize),這個方法則會觸發子View的onMeasure()方法,去設置子View的佈局,由此我們可以可以看到onMeasure這個方法的作用:

1)在這個方法裏面會循環調用子View的measure方法,不停的往下觸發子View去丈量自己的尺寸;

2)ViewGroup繼承於View,onMeasure方法在View類中的源碼如下:

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

關於getDefaultSize方法就不多說了,看看setMeasureDimension,源碼如下:

 1     /**
 2      * <p>This mehod must be called by {@link #onMeasure(int, int)} to store the
 3      * measured width and measured height. Failing to do so will trigger an
 4      * exception at measurement time.</p>
 5      *
 6      * @param measuredWidth the measured width of this view
 7      * @param measuredHeight the measured height of this view
 8      */
 9     protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
10         mMeasuredWidth = measuredWidth;
11         mMeasuredHeight = measuredHeight;
12 
13         mPrivateFlags |= MEASURED_DIMENSION_SET;
14     } 

看到木有,它設置了自己的寬和高!我們在重載View的時候,如果重載了onMeasure方法,就一定要調用setMeasureDimension方法,否則會拋出異常,而重載View'Group的時候,則只需要調用super.OnMeasure即可。

 

最後整理一下:

1)測量過程------>onMeasure(),傳入的參數是本View的可見長和寬,通過這個方法循環測量所有View的尺寸並且存儲在View裏面;

2)佈局過程------>onLayout(),傳入的參數是View可見區域的上下左右四邊的位置,在這個方法裏面可以通過layout來放置子View;

 

補充:getWidth()和getMeasuredWidth()的區別

getWidth(): View在設定好佈局後,整個View的寬度

getMeasuredWidth():對View上的內容進行測量後得到的View內容佔據的寬度。

很簡單,getWidth()就是View顯示之後的width,而getMeasuredWidth,從前面的源代碼就可以看出來其實是在measure裏面傳入的參數,具體是否一樣完全要看程序最後的計算。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章