android view原理

 分三個階段來看

第一步需要 知道view的繪製過程

View繪製流程以及invalidate()等相關方法分析

整個View樹的繪圖流程是在ViewRoot.java類的performTraversals()函數展開的,該函數做的執行過程可簡單概況爲

 根之前狀態,判斷是否需要重新計算視圖大小(measure)、是否重新需要安置視圖的位置(layout)、以及是否需要重繪

 (draw),其框架過程如下:

                                                                                                   步驟其實爲host.layout() 

           

 

 

      接下來溫習一下整個View樹的結構,對每個具體View對象的操作,其實就是個遞歸的實現。

 

                  

 

流程一:      mesarue()過程


        主要作用:爲整個View樹計算實際的大小,即設置實際的高(對應屬性:mMeasuredHeight)和寬(對應屬性:

  mMeasureWidth),每個View的控件的實際寬高都是由父視圖和本身視圖決定的。

 

     具體的調用鏈如下

          ViewRoot根對象地屬性mView(其類型一般爲ViewGroup類型)調用measure()方法去計算View樹的大小,回調

View/ViewGroup對象的onMeasure()方法,該方法實現的功能如下:    

         1、設置本View視圖的最終大小,該功能的實現通過調用setMeasuredDimension()方法去設置實際的高(對應屬性:  

                mMeasuredHeight)和寬(對應屬性:mMeasureWidth)   ;

         2 、如果該View對象是個ViewGroup類型,需要重寫該onMeasure()方法,對其子視圖進行遍歷的measure()過程。

              

               2.1  對每個子視圖的measure()過程,是通過調用父類ViewGroup.java類裏的measureChildWithMargins()方法去

          實現,該方法內部只是簡單地調用了View對象的measure()方法。(由於measureChildWithMargins()方法只是一個過渡

          層更簡單的做法是直接調用View對象的measure()方法)。

              

     整個measure調用流程就是個樹形的遞歸過程

 

     measure函數原型爲 View.java 該函數不能被重載

      

  1. public final void measure(int widthMeasureSpec, int heightMeasureSpec) {  
  2.     //....  
  3.   
  4.     //回調onMeasure()方法    
  5.     onMeasure(widthMeasureSpec, heightMeasureSpec);  
  6.      
  7.     //more  
  8. }  

     爲了大家更好的理解,採用“二B程序員”的方式利用僞代碼描述該measure流程

 

  1. //回調View視圖裏的onMeasure過程  
  2. private void onMeasure(int height , int width){  
  3.  //設置該view的實際寬(mMeasuredWidth)高(mMeasuredHeight)  
  4.  //1、該方法必須在onMeasure調用,否者報異常。  
  5.  setMeasuredDimension(h , l) ;  
  6.    
  7.  //2、如果該View是ViewGroup類型,則對它的每個子View進行measure()過程  
  8.  int childCount = getChildCount() ;  
  9.    
  10.  for(int i=0 ;i<childCount ;i++){  
  11.   //2.1、獲得每個子View對象引用  
  12.   View child = getChildAt(i) ;  
  13.     
  14.   //整個measure()過程就是個遞歸過程  
  15.   //該方法只是一個過濾器,最後會調用measure()過程 ;或者 measureChild(child , h, i)方法都  
  16.   measureChildWithMargins(child , h, i) ;   
  17.     
  18.   //其實,對於我們自己寫的應用來說,最好的辦法是去掉框架裏的該方法,直接調用view.measure(),如下:  
  19.   //child.measure(h, l)  
  20.  }  
  21. }  
  22.   
  23. //該方法具體實現在ViewGroup.java裏 。  
  24. protected  void measureChildWithMargins(View v, int height , int width){  
  25.  v.measure(h,l)     
  26. }  

流程二、 layout佈局過程:

 

     主要作用 :爲將整個根據子視圖的大小以及佈局參數將View樹放到合適的位置上。

 

     具體的調用鏈如下:

       host.layout()開始View樹的佈局,繼而回調給View/ViewGroup類中的layout()方法。具體流程如下

  

        1 、layout方法會設置該View視圖位於父視圖的座標軸,即mLeft,mTop,mLeft,mBottom(調用setFrame()函數去實現)

  接下來回調onLayout()方法(如果該View是ViewGroup對象,需要實現該方法,對每個子視圖進行佈局) ;

       

       2、如果該View是個ViewGroup類型,需要遍歷每個子視圖chiildView,調用該子視圖的layout()方法去設置它的座標值。

 

          layout函數原型爲 ,位於View.java

  1. /* final 標識符 , 不能被重載 , 參數爲每個視圖位於父視圖的座標軸 
  2.  * @param l Left position, relative to parent 
  3.  * @param t Top position, relative to parent 
  4.  * @param r Right position, relative to parent 
  5.  * @param b Bottom position, relative to parent 
  6.  */  
  7. public final void layout(int l, int t, int r, int b) {  
  8.     boolean changed = setFrame(l, t, r, b); //設置每個視圖位於父視圖的座標軸  
  9.     if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) {  
  10.         if (ViewDebug.TRACE_HIERARCHY) {  
  11.             ViewDebug.trace(this, ViewDebug.HierarchyTraceType.ON_LAYOUT);  
  12.         }  
  13.   
  14.         onLayout(changed, l, t, r, b);//回調onLayout函數 ,設置每個子視圖的佈局  
  15.         mPrivateFlags &= ~LAYOUT_REQUIRED;  
  16.     }  
  17.     mPrivateFlags &= ~FORCE_LAYOUT;  
  18. }  


    同樣地, 將上面layout調用流程,用僞代碼描述如下: 
  1. // layout()過程  ViewRoot.java  
  2. // 發起layout()的"發號者"在ViewRoot.java裏的performTraversals()方法, mView.layout()  
  3.   
  4. private void  performTraversals(){  
  5.    
  6.     //...  
  7.       
  8.     View mView  ;  
  9.        mView.layout(left,top,right,bottom) ;  
  10.       
  11.     //....  
  12. }  
  13.   
  14. //回調View視圖裏的onLayout過程 ,該方法只由ViewGroup類型實現  
  15. private void onLayout(int left , int top , right , bottom){  
  16.   
  17.  //如果該View不是ViewGroup類型  
  18.  //調用setFrame()方法設置該控件的在父視圖上的座標軸  
  19.    
  20.  setFrame(l ,t , r ,b) ;  
  21.    
  22.  //--------------------------  
  23.    
  24.  //如果該View是ViewGroup類型,則對它的每個子View進行layout()過程  
  25.  int childCount = getChildCount() ;  
  26.    
  27.  for(int i=0 ;i<childCount ;i++){  
  28.   //2.1、獲得每個子View對象引用  
  29.   View child = getChildAt(i) ;  
  30.   //整個layout()過程就是個遞歸過程  
  31.   child.layout(l, t, r, b) ;  
  32.  }  
  33. }  



   流程三、 draw()繪圖過程

     由ViewRoot對象的performTraversals()方法調用draw()方法發起繪製該View樹,值得注意的是每次發起繪圖時,並不

  會重新繪製每個View樹的視圖,而只會重新繪製那些“需要重繪”的視圖,View類內部變量包含了一個標誌位DRAWN,當該

視圖需要重繪時,就會爲該View添加該標誌位。

 

   調用流程 :

     mView.draw()開始繪製,draw()方法實現的功能如下:

          1 、繪製該View的背景

          2 、爲顯示漸變框做一些準備操作(見5,大多數情況下,不需要改漸變框)          

          3、調用onDraw()方法繪製視圖本身   (每個View都需要重載該方法,ViewGroup不需要實現該方法)

          4、調用dispatchDraw ()方法繪製子視圖(如果該View類型不爲ViewGroup,即不包含子視圖,不需要重載該方法)

值得說明的是,ViewGroup類已經爲我們重寫了dispatchDraw ()的功能實現,應用程序一般不需要重寫該方法,但可以重載父類

  函數實現具體的功能。

 

            4.1 dispatchDraw()方法內部會遍歷每個子視圖,調用drawChild()去重新回調每個子視圖的draw()方法(注意,這個 

地方“需要重繪”的視圖纔會調用draw()方法)。值得說明的是,ViewGroup類已經爲我們重寫了dispatchDraw()的功能

實現,應用程序一般不需要重寫該方法,但可以重載父類函數實現具體的功能。

    

     5、繪製滾動條

 

  於是,整個調用鏈就這樣遞歸下去了。

    

     同樣地,使用僞代碼描述如下:

    

  1. // draw()過程     ViewRoot.java  
  2. // 發起draw()的"發號者"在ViewRoot.java裏的performTraversals()方法, 該方法會繼續調用draw()方法開始繪圖  
  3. private void  draw(){  
  4.    
  5.     //...  
  6.  View mView  ;  
  7.     mView.draw(canvas) ;    
  8.       
  9.     //....  
  10. }  
  11.   
  12. //回調View視圖裏的onLayout過程 ,該方法只由ViewGroup類型實現  
  13. private void draw(Canvas canvas){  
  14.  //該方法會做如下事情  
  15.  //1 、繪製該View的背景  
  16.  //2、爲繪製漸變框做一些準備操作  
  17.  //3、調用onDraw()方法繪製視圖本身  
  18.  //4、調用dispatchDraw()方法繪製每個子視圖,dispatchDraw()已經在Android框架中實現了,在ViewGroup方法中。  
  19.       // 應用程序程序一般不需要重寫該方法,但可以捕獲該方法的發生,做一些特別的事情。  
  20.  //5、繪製漸變框    
  21. }  
  22.   
  23. //ViewGroup.java中的dispatchDraw()方法,應用程序一般不需要重寫該方法  
  24. @Override  
  25. protected void dispatchDraw(Canvas canvas) {  
  26.  //   
  27.  //其實現方法類似如下:  
  28.  int childCount = getChildCount() ;  
  29.    
  30.  for(int i=0 ;i<childCount ;i++){  
  31.   View child = getChildAt(i) ;  
  32.   //調用drawChild完成  
  33.   drawChild(child,canvas) ;  
  34.  }       
  35. }  
  36. //ViewGroup.java中的dispatchDraw()方法,應用程序一般不需要重寫該方法  
  37. protected void drawChild(View child,Canvas canvas) {  
  38.  // ....  
  39.  //簡單的回調View對象的draw()方法,遞歸就這麼產生了。  
  40.  child.draw(canvas) ;  
  41.    
  42.  //.........  
  43. }  


   

View(視圖)繪製不同狀態背景圖片原理

1、View的幾種不同狀態屬性

           2、如何根據不同狀態去切換我們的背景圖片。

 

 

開篇介紹:android背景選擇器selector用法彙總


        對Android開發有經驗的同學,對 <selector>節點的使用一定很熟悉,該節點的作用就是定義一組狀態資源圖片,使其能夠

  在不同的狀態下更換某個View的背景圖片。例如,如下的hello_selection.xml文件定義:

  1. <?xml version="1.0" encoding="utf-8" ?>     
  2. <selector xmlns:android="http://schemas.android.com/apk/res/android">   
  3.   <!-- 觸摸時並且當前窗口處於交互狀態 -->    
  4.   <item android:state_pressed="true" android:state_window_focused="true" android:drawable= "@drawable/pic1" />  
  5.   <!--  觸摸時並且沒有獲得焦點狀態 -->    
  6.   <item android:state_pressed="true" android:state_focused="false" android:drawable="@drawable/pic2" />    
  7.   <!--選中時的圖片背景-->    
  8.   <item android:state_selected="true" android:drawable="@drawable/pic3" />     
  9.   <!--獲得焦點時的圖片背景-->    
  10.   <item android:state_focused="true" android:drawable="@drawable/pic4" />    
  11.   <!-- 窗口沒有處於交互時的背景圖片 -->    
  12.   <item android:drawable="@drawable/pic5" />   
  13. </selector>  

           更多關於 <selector>節點的使用請參考該博客<android背景選擇器selector用法彙總>


       其實,前面說的xml文件,最終會被Android框架解析成StateListDrawable類對象。

 

 

知識點一:StateListDrawable類介紹


    類功能說明:該類定義了不同狀態值下與之對應的圖片資源,即我們可以利用該類保存多種狀態值,多種圖片資源。

    常用方法爲:

       public void addState (int[] stateSet, Drawable drawable)

       功能: 給特定的狀態集合設置drawable圖片資源

       使用方式:參考前面的hello_selection.xml文件我們利用代碼去構建一個相同的StateListDrawable類對象,如下:

  1. //初始化一個空對象  
  2. StateListDrawable stalistDrawable = new StateListDrawable();  
  3. //獲取對應的屬性值 Android框架自帶的屬性 attr  
  4. int pressed = android.R.attr.state_pressed;  
  5. int window_focused = android.R.attr.state_window_focused;  
  6. int focused = android.R.attr.state_focused;  
  7. int selected = android.R.attr.state_selected;  
  8.   
  9. stalistDrawable.addState(new int []{pressed , window_focused}, getResources().getDrawable(R.drawable.pic1));  
  10. stalistDrawable.addState(new int []{pressed , -focused}, getResources().getDrawable(R.drawable.pic2);  
  11. stalistDrawable.addState(new int []{selected }, getResources().getDrawable(R.drawable.pic3);  
  12. stalistDrawable.addState(new int []{focused }, getResources().getDrawable(R.drawable.pic4);  
  13. //沒有任何狀態時顯示的圖片,我們給它設置我空集合  
  14. stalistDrawable.addState(new int []{}, getResources().getDrawable(R.drawable.pic5);  

       

        上面的“-”負號表示對應的屬性值爲false

        當我們爲某個View使用其作爲背景色時,會根據狀態進行背景圖的轉換。


      public boolean isStateful ()

     功能: 表明該狀態改變了,對應的drawable圖片是否會改變。

     注:在StateListDrawable類中,該方法返回爲true,顯然狀態改變後,我們的圖片會跟着改變。

 


知識點二:View的五種狀態值

 

       一般來說,Android框架爲View定義了四種不同的狀態,這些狀態值的改變會引發View相關操作,例如:更換背景圖片、是否

   觸發點擊事件等;

      視圖幾種不同狀態含義見下圖:

                             

     

   其中selected和focused的區別有如下幾點:

      1,我們通過查看setSelected()方法,來獲取相關信息。

        SDK中對setSelected()方法----對於與selected狀態有如下說明:

             public void setSelected (boolean selected)

             Since: APILevel 1

             Changes the selection state of this view. Aview can be selected or not. Note that selection is not the same as

        focus. Views are typically selected in the context of an AdapterView like ListView or GridView ;the selected view is 

        the view that is highlighted.

            Parameters selected   true if the view must be selected, false otherwise


           由以上可知:selected不同於focus狀態,通常在AdapterView類羣下例如ListView或者GridView會使某個View處於

     selected狀態,並且獲得該狀態的View處於高亮狀態。

 

    2、一個窗口只能有一個視圖獲得焦點(focus),而一個窗口可以有多個視圖處於”selected”狀態中。

 

      總結:focused狀態一般是由按鍵操作引起的;

                pressed狀態是由觸摸消息引起的;

                selected則完全是由應用程序主動調用setSelected()進行控制。

 

      例如:當我們觸摸某個控件時,會導致pressed狀態改變;獲得焦點時,會導致focus狀態變化。於是,我們可以通過這種

   更新後狀態值去更新我們對應的Drawable對象了。

 


問題:如何根據狀態值的改變去繪製/顯示對應的背景圖?


       當View任何狀態值發生改變時,都會調用refreshDrawableList()方法去更新對應的背景Drawable對象。

       其整體調用流程如下: View.java類中

  1. //路徑:\frameworks\base\core\java\android\view\View.java  
  2.     /* Call this to force a view to update its drawable state. This will cause 
  3.      * drawableStateChanged to be called on this view. Views that are interested 
  4.      * in the new state should call getDrawableState. 
  5.      */   
  6.     //主要功能是根據當前的狀態值去更換對應的背景Drawable對象  
  7.     public void refreshDrawableState() {  
  8.         mPrivateFlags |= DRAWABLE_STATE_DIRTY;  
  9.         //所有功能在這個函數裏去完成  
  10.         drawableStateChanged();  
  11.         ...  
  12.     }  
  13.     /* This function is called whenever the state of the view changes in such 
  14.      * a way that it impacts the state of drawables being shown. 
  15.      */  
  16.     // 獲得當前的狀態屬性--- 整型集合 ; 調用Drawable類的setState方法去獲取資源。  
  17.     protected void drawableStateChanged() {  
  18.         //該視圖對應的Drawable對象,通常對應於StateListDrawable類對象  
  19.         Drawable d = mBGDrawable;     
  20.         if (d != null && d.isStateful()) {  //通常都是成立的  
  21.             //getDrawableState()方法主要功能:會根據當前View的狀態屬性值,將其轉換爲一個整型集合  
  22.             //setState()方法主要功能:根據當前的獲取到的狀態,更新對應狀態下的Drawable對象。  
  23.             d.setState(getDrawableState());  
  24.         }  
  25.     }  
  26.     /*Return an array of resource IDs of the drawable states representing the 
  27.      * current state of the view. 
  28.      */  
  29.     public final int[] getDrawableState() {  
  30.         if ((mDrawableState != null) && ((mPrivateFlags & DRAWABLE_STATE_DIRTY) == 0)) {  
  31.             return mDrawableState;  
  32.         } else {  
  33.             //根據當前View的狀態屬性值,將其轉換爲一個整型集合,並返回  
  34.             mDrawableState = onCreateDrawableState(0);  
  35.             mPrivateFlags &= ~DRAWABLE_STATE_DIRTY;  
  36.             return mDrawableState;  
  37.         }  
  38.     }  


       通過這段代碼我們可以明白View內部是如何獲取更細後的狀態值以及動態獲取對應的背景Drawable對象----setState()方法

去完成的。這兒我簡單的分析下Drawable類裏的setState()方法的功能,把流程給走一下:

    

         Step 1 、 setState()函數原型 ,

             函數位於:frameworks\base\graphics\java\android\graphics\drawable\StateListDrawable.java 類中


  1. //如果狀態態值發生了改變,就回調onStateChange()方法。  
  2. public boolean setState(final int[] stateSet) {  
  3.     if (!Arrays.equals(mStateSet, stateSet)) {  
  4.         mStateSet = stateSet;  
  5.         return onStateChange(stateSet);  
  6.     }  
  7.     return false;  
  8. }  

           該函數的主要功能: 判斷狀態值是否發生了變化,如果發生了變化,就調用onStateChange()方法進一步處理。

    

       Step 2 、onStateChange()函數原型:

            該函數位於 frameworks\base\graphics\java\android\graphics\drawable\StateListDrawable.java 類中


  1. //狀態值發生了改變,我們需要找出第一個吻合的當前狀態的Drawable對象  
  2. protected boolean onStateChange(int[] stateSet) {  
  3.     //要找出第一個吻合的當前狀態的Drawable對象所在的索引位置, 具體匹配算法請自己深入源碼看看  
  4.     int idx = mStateListState.indexOfStateSet(stateSet);  
  5.     ...  
  6.     //獲取對應索引位置的Drawable對象  
  7.     if (selectDrawable(idx)) {  
  8.         return true;  
  9.     }  
  10.     ...  
  11. }  

          該函數的主要功能: 根據新的狀態值,從StateListDrawable實例對象中,找到第一個完全吻合該新狀態值的索引下標處 ;

   繼而,調用selectDrawable()方法去獲取索引下標的當前Drawable對象。

         具體查找算法在 mStateListState.indexOfStateSet(stateSet) 裏實現了。基本思路是:查找第一個能完全吻合該新狀態值

   的索引下標,如果找到了,則立即返回。 具體實現過程,只好看看源碼咯。

  

       Step 3 、selectDrawable()函數原型:

            該函數位於 frameworks\base\graphics\java\android\graphics\drawable\StateListDrawable.java 類中

  1. public boolean selectDrawable(int idx)  
  2. {  
  3.     if (idx >= 0 && idx < mDrawableContainerState.mNumChildren) {  
  4.         //獲取對應索引位置的Drawable對象  
  5.         Drawable d = mDrawableContainerState.mDrawables[idx];  
  6.         ...  
  7.         mCurrDrawable = d; //mCurrDrawable即使當前Drawable對象  
  8.         mCurIndex = idx;  
  9.         ...  
  10.     } else {  
  11.        ...  
  12.     }  
  13.     //請求該View刷新自己,這個方法我們稍後講解。  
  14.     invalidateSelf();  
  15.     return true;  
  16. }  

             該函數的主要功能是選擇當前索引下標處的Drawable對象,並保存在mCurrDrawable中。



知識點三: 關於Drawable.Callback接口

   

    該接口定義瞭如下三個函數:     

  1. //該函數位於 frameworks\base\graphics\java\android\graphics\drawable\Drawable.java 類中  
  2. public static interface Callback {  
  3.     //如果Drawable對象的狀態發生了變化,會請求View重新繪製,  
  4.     //因此我們對應於該View的背景Drawable對象能夠”繪製出來”.  
  5.     public void invalidateDrawable(Drawable who);  
  6.     //該函數目前還不懂  
  7.     public void scheduleDrawable(Drawable who, Runnable what, long when);  
  8.      //該函數目前還不懂  
  9.     public void unscheduleDrawable(Drawable who, Runnable what);  
  10. }  

其中比較重要的函數爲:


      public voidinvalidateDrawable(Drawable who)

        函數功能:如果Drawable對象的狀態發生了變化,會請求View重新繪製,因此我們對應於該View的背景Drawable對象

   能夠重新”繪製“出來。


    Android框架View類繼承了該接口,同時實現了這三個函數的默認處理方式,其中invalidateDrawable()方法如下:

  1. public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource   
  2. {  
  3.     ...  
  4.     //Invalidates the specified Drawable.  
  5.     //默認實現,重新繪製該視圖本身  
  6.     public void invalidateDrawable(Drawable drawable) {  
  7.         if (verifyDrawable(drawable)) { //是否是同一個Drawable對象,通常爲真  
  8.             final Rect dirty = drawable.getBounds();  
  9.             final int scrollX = mScrollX;  
  10.             final int scrollY = mScrollY;  
  11.             //重新請求繪製該View,即重新調用該View的draw()方法  ...  
  12.             invalidate(dirty.left + scrollX, dirty.top + scrollY,  
  13.                     dirty.right + scrollX, dirty.bottom + scrollY);  
  14.         }  
  15.     }  
  16.     ...  
  17. }  

   因此,我們的Drawable類對象必須將View設置爲回調對象,否則,即使改變了狀態,也不會顯示對應的背景圖。 如下:

            Drawable d  ;                // 圖片資源                        

            d.setCallback(View v) ;  // 視圖v的背景資源爲 d 對象


 

知識點四:View繪製背景圖片過程


      在前面的博客中《Android中View繪製流程以及invalidate()等相關方法分析》,我們知道了一個視圖的背景繪製過程時在

  View類裏的draw()方法裏完成的,我們這兒在回顧下draw()的流程,同時重點講解下繪製背景的操作。


  1. //方法所在路徑:frameworks\base\core\java\android\view\View.java  
  2. //draw()繪製過程  
  3. private void draw(Canvas canvas){    
  4. //該方法會做如下事情    
  5.   //1 、繪製該View的背景    
  6.     //其中背景圖片繪製過程如下:  
  7.     //是否透明, 視圖通常是透明的 , 爲true  
  8.      if (!dirtyOpaque) {  
  9.        //開始繪製視圖的背景  
  10.        final Drawable background = mBGDrawable;  
  11.        if (background != null) {  
  12.            final int scrollX = mScrollX;  //獲取偏移值  
  13.            final int scrollY = mScrollY;  
  14.            //視圖的佈局座標是否發生了改變, 即是否重新layout了。  
  15.            if (mBackgroundSizeChanged) {  
  16.              //如果是,我們的Drawable對象需要重新設置大小了,即填充該View。  
  17.                background.setBounds(00,  mRight - mLeft, mBottom - mTop);  
  18.                mBackgroundSizeChanged = false;  
  19.            }  
  20.            //View沒有發生偏移  
  21.            if ((scrollX | scrollY) == 0) {  
  22.                background.draw(canvas); //OK, 該方法會繪製當前StateListDrawable的當前背景Drawable  
  23.            } else {  
  24.              //View發生偏移,由於背景圖片值顯示在佈局座標中,即背景圖片不會發生偏移,只有視圖內容onDraw()會發生偏移  
  25.              //我們調整canvas對象的繪製區域,繪製完成後對canvas對象屬性調整回來  
  26.                canvas.translate(scrollX, scrollY);  
  27.                background.draw(canvas); //OK, 該方法會繪製當前StateListDrawable的當前背景Drawable  
  28.                canvas.translate(-scrollX, -scrollY);  
  29.            }  
  30.        }  
  31.    }  
  32.     ...  
  33.  //2、爲繪製漸變框做一些準備操作    
  34.  //3、調用onDraw()方法繪製視圖本身    
  35.  //4、調用dispatchDraw()方法繪製每個子視圖,dispatchDraw()已經在Android框架中實現了,在ViewGroup方法中。    
  36.  //5、繪製漸變框      
  37. }    


      That's all ! 我們用到的知識點也就這麼多吧。 如果大家有絲絲不明白的話,可以去看下源代碼,具體去分析下這些流程到底

  是怎麼走下來的。

      我們從宏觀的角度分析了View繪製不同狀態背景的原理,View框架就是這麼做的。爲了易於理解性,

  下面我們通過一個小Demo來演示前面種種流程。

   

 Demo 說明:


          我們參照View框架中繪製不同背景圖的實現原理,自定義一個View類,通過給它設定StateListDrawable對象,使其能夠在

   不同狀態時能動態"繪製"背景圖片。 基本流程方法和View.java類實現過程一模一樣。

    截圖如下:


                      


                 初始背景圖                                                            觸摸後顯示的背景圖(pressed)


  一、主文件MainActivity.java如下:

  1. /** 
  2.  *  
  3.  * @author http://http://blog.csdn.net/qinjuning 
  4.  */  
  5. public class MainActivity extends Activity  
  6. {  
  7.   
  8.     @Override  
  9.     public void onCreate(Bundle savedInstanceState)  
  10.     {  
  11.         super.onCreate(savedInstanceState);      
  12.   
  13.         LinearLayout ll  =  new LinearLayout(MainActivity.this);  
  14.         CustomView customView = new CustomView(MainActivity.this);   
  15.         //簡單設置爲 width 200px - height 100px吧   
  16.         ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(200 , 100);  
  17.         customView.setLayoutParams(lp);  
  18.         //需要將該View設置爲可點擊/觸摸狀態,否則觸摸該View沒有效果。  
  19.         customView.setClickable(true);  
  20.           
  21.         ll.addView(customView);  
  22.         setContentView(ll);   
  23.     }  
  24. }  

   功能很簡單,爲Activity設置了視圖 。


二、 自定義View如下 , CustomView.java :

  1. /**  
  2.  * @author http://http://blog.csdn.net/qinjuning 
  3.  */  
  4. //自定義View  
  5. public class CustomView extends View   /*extends Button*/  
  6. {  
  7.     private static String TAG = "TackTextView";  
  8.       
  9.     private Context mContext = null;  
  10.     private Drawable mBackground = null;  
  11.     private boolean mBGSizeChanged = true;;   //視圖View佈局(layout)大小是否發生變化  
  12.       
  13.     public CustomView(Context context)  
  14.     {  
  15.         super(context);  
  16.         mContext = context;         
  17.         initStateListDrawable(); // 初始化圖片資源  
  18.     }  
  19.   
  20.     // 初始化圖片資源  
  21.     private void initStateListDrawable()  
  22.     {  
  23.         //有兩種方式獲取我們的StateListDrawable對象:  
  24.         // 獲取方式一、手動構建一個StateListDrawable對象  
  25.         StateListDrawable statelistDrawable = new StateListDrawable();  
  26.           
  27.         int pressed = android.R.attr.state_pressed;  
  28.         int windowfocused = android.R.attr.state_window_focused;  
  29.         int enabled = android.R.attr.state_enabled;  
  30.         int stateFoucesd = android.R.attr.state_focused;  
  31.         //匹配狀態時,是一種優先包含的關係。  
  32.         // "-"號表示該狀態值爲false .即不匹配  
  33.         statelistDrawable.addState(new int[] { pressed, windowfocused },   
  34.                 mContext.getResources().getDrawable(R.drawable.btn_power_on_pressed));  
  35.         statelistDrawable.addState(new int[]{ -pressed, windowfocused },   
  36.                 mContext.getResources().getDrawable(R.drawable.btn_power_on_nor));      
  37.                  
  38.         mBackground = statelistDrawable;  
  39.           
  40.         //必須設置回調,當改變狀態時,會回掉該View進行invalidate()刷新操作.  
  41.         mBackground.setCallback(this);         
  42.         //取消默認的背景圖片,因爲我們設置了自己的背景圖片了,否則可能造成背景圖片重疊。  
  43.         this.setBackgroundDrawable(null);  
  44.           
  45.         // 獲取方式二、、使用XML獲取StateListDrawable對象  
  46.         // mBackground = mContext.getResources().getDrawable(R.drawable.tv_background);  
  47.     }  
  48.       
  49.     protected void drawableStateChanged()  
  50.     {  
  51.         Log.i(TAG, "drawableStateChanged");  
  52.         Drawable d = mBackground;  
  53.         if (d != null && d.isStateful())  
  54.         {  
  55.             d.setState(getDrawableState());  
  56.             Log.i(TAG, "drawableStateChanged  and is 111");  
  57.         }  
  58.   
  59.        Log.i(TAG, "drawableStateChanged  and is 222");  
  60.        super.drawableStateChanged();  
  61.     }  
  62.     //驗證圖片是否相等 , 在invalidateDrawable()會調用此方法,我們需要重寫該方法。  
  63.     protected boolean verifyDrawable(Drawable who)  
  64.     {  
  65.         return who == mBackground || super.verifyDrawable(who);  
  66.     }  
  67.     //draw()過程,繪製背景圖片...  
  68.     public void draw(Canvas canvas)  
  69.     {  
  70.         Log.i(TAG, " draw -----");  
  71.         if (mBackground != null)  
  72.         {  
  73.             if(mBGSizeChanged)  
  74.             {  
  75.                 //設置邊界範圍  
  76.                 mBackground.setBounds(00, getRight() - getLeft(), getBottom() - getTop());  
  77.                 mBGSizeChanged = false ;  
  78.             }  
  79.             if ((getScrollX() | getScrollY()) == 0)  //是否偏移  
  80.             {  
  81.                 mBackground.draw(canvas); //繪製當前狀態對應的圖片  
  82.             }  
  83.             else  
  84.             {  
  85.                 canvas.translate(getScrollX(), getScrollY());  
  86.                 mBackground.draw(canvas); //繪製當前狀態對應的圖片  
  87.                 canvas.translate(-getScrollX(), -getScrollY());  
  88.             }  
  89.         }  
  90.         super.draw(canvas);  
  91.     }  
  92.     public void onDraw(Canvas canvas) {      
  93.         ...  
  94.     }  
  95. }  


   將該View設置的背景圖片轉換爲節點xml,形式如下:

  1. <selector xmlns:android="http://schemas.android.com/apk/res/android">  
  2.   <item android:state_pressed="true"   
  3.         android:state_window_focused="true"   
  4.         android:drawable="@drawable/btn_power_on_pressed"></item>  
  5.   <item android:state_pressed="false"   
  6.         android:state_window_focused="true"    
  7.         android:drawable="@drawable/btn_power_on_nor"></item>      
  8.         
  9. </selector>  


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

 measure()過程和layout()過程即可 。


     這三種情況,最終會直接或間接調用到三個函數,分別爲invalidate(),requsetLaytout()以及requestFocus() ,接着

這三個函數最終會調用到ViewRoot中的schedulTraversale()方法,該函數然後發起一個異步消息,消息處理中調用

performTraverser()方法對整個View進行遍歷。

 

 

    invalidate()方法 :

 

   說明:請求重繪View樹,即draw()過程,假如視圖發生大小沒有變化就不會調用layout()過程,並且只繪製那些“需要重繪的”

視圖,即誰(View的話,只繪製該View ;ViewGroup,則繪製整個ViewGroup)請求invalidate()方法,就繪製該視圖。

 

     一般引起invalidate()操作的函數如下:

            1、直接調用invalidate()方法,請求重新draw(),但只會繪製調用者本身。

            2、setSelection()方法 :請求重新draw(),但只會繪製調用者本身。

            3、setVisibility()方法 : 當View可視狀態在INVISIBLE轉換VISIBLE時,會間接調用invalidate()方法,

                     繼而繪製該View。

            4 、setEnabled()方法 : 請求重新draw(),但不會重新繪製任何視圖包括該調用者本身。

 

    requestLayout()方法 :會導致調用measure()過程 和 layout()過程 。

 

           說明:只是對View樹重新佈局layout過程包括measure()和layout()過程,不會調用draw()過程,但不會重新繪製

任何視圖包括該調用者本身。

 

    一般引起invalidate()操作的函數如下:

         1、setVisibility()方法:

             當View的可視狀態在INVISIBLE/ VISIBLE 轉換爲GONE狀態時,會間接調用requestLayout() 和invalidate方法。

    同時,由於整個個View樹大小發生了變化,會請求measure()過程以及draw()過程,同樣地,只繪製需要“重新繪製”的視圖。

 

    requestFocus()函數說明:

 

          說明:請求View樹的draw()過程,但只繪製“需要重繪”的視圖。

 

 

    下面寫個簡單的小Demo吧,主要目的是給大家演示繪圖的過程以及每個流程裏該做的一些功能。截圖如下:


                                                



 1、    MyViewGroup.java  自定義ViewGroup類型

   

  1. /** 
  2.  * @author http://http://blog.csdn.net/qinjuning 
  3.  */  
  4. //自定義ViewGroup 對象  
  5. public class MyViewGroup  extends ViewGroup{  
  6.   
  7.   
  8.     private static String TAG = "MyViewGroup" ;  
  9.     private Context mContext ;  
  10.       
  11.     public MyViewGroup(Context context) {  
  12.         super(context);  
  13.         mContext = context ;  
  14.         init() ;  
  15.     }  
  16.   
  17.     //xml定義的屬性,需要該構造函數  
  18.     public MyViewGroup(Context context , AttributeSet attrs){  
  19.         super(context,attrs) ;  
  20.         mContext = context ;  
  21.         init() ;  
  22.     }  
  23.       
  24.     //爲MyViewGroup添加三個子View  
  25.     private void init(){  
  26.         //調用ViewGroup父類addView()方法添加子View  
  27.           
  28.         //child 對象一 : Button  
  29.         Button btn= new Button(mContext) ;  
  30.         btn.setText("I am Button") ;  
  31.         this.addView(btn) ;  
  32.           
  33.         //child 對象二 : ImageView   
  34.         ImageView img = new ImageView(mContext) ;  
  35.         img.setBackgroundResource(R.drawable.icon) ;  
  36.         this.addView(img) ;  
  37.           
  38.         //child 對象三 : TextView  
  39.         TextView txt = new TextView(mContext) ;  
  40.         txt.setText("Only Text") ;  
  41.         this.addView(txt) ;   
  42.           
  43.         //child 對象四 : 自定義View  
  44.         MyView myView = new MyView(mContext) ;  
  45.         this.addView(myView) ;   
  46.     }  
  47.       
  48.     @Override  
  49.     //對每個子View進行measure():設置每子View的大小,即實際寬和高  
  50.     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){  
  51.         //通過init()方法,我們爲該ViewGroup對象添加了三個視圖 , Button、 ImageView、TextView  
  52.         int childCount = getChildCount() ;  
  53.         Log.i(TAG, "the size of this ViewGroup is ----> " + childCount) ;  
  54.                           
  55.         Log.i(TAG, "**** onMeasure start *****") ;  
  56.           
  57.         //獲取該ViewGroup的實際長和寬  涉及到MeasureSpec類的使用  
  58.         int specSize_Widht = MeasureSpec.getSize(widthMeasureSpec) ;  
  59.         int specSize_Heigth = MeasureSpec.getSize(heightMeasureSpec) ;  
  60.           
  61.         Log.i(TAG, "**** specSize_Widht " + specSize_Widht+ " * specSize_Heigth   *****" + specSize_Heigth) ;  
  62.           
  63.         //設置本ViewGroup的寬高  
  64.         setMeasuredDimension(specSize_Widht , specSize_Heigth) ;  
  65.           
  66.           
  67.           
  68.           
  69.         for(int i=0 ;i<childCount ; i++){  
  70.             View child = getChildAt(i) ;   //獲得每個對象的引用  
  71.             child.measure(5050) ;   //簡單的設置每個子View對象的寬高爲 50px , 50px    
  72.             //或者可以調用ViewGroup父類方法measureChild()或者measureChildWithMargins()方法  
  73.             //this.measureChild(child, widthMeasureSpec, heightMeasureSpec) ;  
  74.         }  
  75.           
  76.     }  
  77.       
  78.     @Override  
  79.     //對每個子View視圖進行佈局  
  80.     protected void onLayout(boolean changed, int l, int t, int r, int b) {  
  81.         // TODO Auto-generated method stub  
  82.         //通過init()方法,我們爲該ViewGroup對象添加了三個視圖 , Button、 ImageView、TextView  
  83.         int childCount = getChildCount() ;  
  84.           
  85.         int startLeft = 0 ;//設置每個子View的起始橫座標   
  86.         int startTop = 10 ; //每個子View距離父視圖的位置 , 簡單設置爲10px吧 。 可以理解爲 android:margin=10px ;  
  87.           
  88.         Log.i(TAG, "**** onLayout start ****") ;  
  89.         for(int i=0 ;i<childCount ; i++){  
  90.             View child = getChildAt(i) ;   //獲得每個對象的引用  
  91.             child.layout(startLeft, startTop, startLeft+child.getMeasuredWidth(), startTop+child.getMeasuredHeight()) ;  
  92.             startLeft =startLeft+child.getMeasuredWidth() + 10;  //校準startLeft值,View之間的間距設爲10px ;  
  93.             Log.i(TAG, "**** onLayout startLeft ****" +startLeft) ;  
  94.         }             
  95.     }  
  96.     //繪圖過程Android已經爲我們封裝好了 ,這兒只爲了觀察方法調用程  
  97.     protected void dispatchDraw(Canvas canvas){  
  98.         Log.i(TAG, "**** dispatchDraw start ****") ;  
  99.           
  100.         super.dispatchDraw(canvas) ;  
  101.     }  
  102.       
  103.     protected boolean drawChild(Canvas canvas , View child, long drawingTime){  
  104.         Log.i(TAG, "**** drawChild start ****") ;  
  105.           
  106.         return super.drawChild(canvas, child, drawingTime) ;  
  107.     }  
  108. }  

   

          2、MyView.java 自定義View類型,重寫onDraw()方法 ,

  1. //自定義View對象  
  2.     public class MyView extends View{  
  3.   
  4.         private Paint paint  = new Paint() ;  
  5.           
  6.         public MyView(Context context) {  
  7.             super(context);  
  8.             // TODO Auto-generated constructor stub  
  9.         }  
  10.         public MyView(Context context , AttributeSet attrs){  
  11.             super(context,attrs);  
  12.         }  
  13.           
  14.         protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){  
  15.             //設置該View大小爲 80 80  
  16.             setMeasuredDimension(50 , 50) ;  
  17.         }  
  18.           
  19.           
  20.           
  21.         //存在canvas對象,即存在默認的顯示區域  
  22.         @Override  
  23.         public void onDraw(Canvas canvas) {  
  24.             // TODO Auto-generated method stub  
  25.             super.onDraw(canvas);  
  26.               
  27.             Log.i("MyViewGroup""MyView is onDraw ") ;  
  28.             //加粗  
  29.             paint.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD));  
  30.             paint.setColor(Color.RED);  
  31.             canvas.drawColor(Color.BLUE) ;  
  32.             canvas.drawRect(003030, paint);  
  33.             canvas.drawText("MyView"1040, paint);  
  34.         }  
  35.     }  


          主Activity只是顯示了該xml文件,在此也不羅嗦了。 大家可以查看該ViewGroup的Log仔細分析下View的繪製流程以及

相關方法的使用。第一次啓動後捕獲的Log如下,網上找了些資料,第一次View樹繪製過程會走幾遍,具體原因可能是某些

View 發生了改變,請求重新繪製,但這根本不影響我們的界面顯示效果 。

 

        總的來說: 整個繪製過程還是十分十分複雜地,每個具體方法的實現都是我輩難以立即的,感到悲劇啊。對Android提

 供的一些ViewGroup對象,比如LinearLayout、RelativeLayout佈局對象的實現也很有壓力。 本文重在介紹整個View樹的繪製

流程,希望大家在此基礎上,多接觸源代碼進行更深入地擴展。

 

 

詳解measure過程以及如何設置View寬高

今天,我着重講解下如下三個內容:

            1、 measure過程

            2、WRAP_CONTENT、MATCH_PARENT/FILL_PARENT屬性的原理說明

            3、xml佈局文件解析成View樹的流程分析。


     希望對大家能有幫助。- -  分析版本基於Android 2.3



 1、WRAP_CONTENT、MATCH_PARENT/FILL_PARENT 


       初入Android殿堂的同學們,對這三個屬性一定又愛又恨。愛的是使用起來挺爽地---照葫蘆畫瓢即可,恨的

  卻是時常混淆這幾個屬性地意義,需要三思而後行。在帶着大家重溫下這幾個屬性的用法吧(希望我沒有囉嗦)。


      這三個屬性都用來適應視圖的水平或垂直大小,一個以視圖的內容或尺寸爲基礎的佈局比精確地指定視圖範圍

  更加方便。

        ①  fill_parent

                設置一個視圖的佈局爲fill_parent將強制性地使視圖擴展至父元素大小。

        ② match_parent

               Android 中match_parent和fill_parent意思一樣,但match_parent更貼切,於是從2.2開始兩個詞都可以

          用,但2.3版本後建議使用match_parent。

       ③ wrap_content

              自適應大小,強制性地使視圖擴展以便顯示其全部內容。以TextView和ImageView控件爲例,設置爲

         wrap_content將完整顯示其內部的文本和圖像。佈局元素將根據內容更改大小。

       

      可不要重複造輪子,以上摘自<<Android fill_parent、wrap_content和match_parent的區別>>


      當然,我們可以設置View的確切寬高,而不是由以上屬性指定。

  1. android:layout_weight="wrap_content"   //自適應大小  
  2. android:layout_weight="match_parent"   //與父視圖等高  
  3. android:layout_weight="fill_parent"    //與父視圖等高  
  4. android:layout_weight="100dip"         //精確設置高度值爲 100dip  

      接下來,我們需要轉換下視角,看看ViewGroup.LayoutParams類及其派生類。


 2、ViewGroup.LayoutParams類及其派生類


    2.1、  ViewGroup.LayoutParams類說明

            Android API中如下介紹:

                LayoutParams are used by views to tell their parents how they want to be laid out.


     意思大概是說: View通過LayoutParams類告訴其父視圖它想要地大小(即,長度和寬度)。


    因此,每個View都包含一個ViewGroup.LayoutParams類或者其派生類,View類依賴於ViewGroup.LayoutParams。

       路徑:frameworks\base\core\java\android\view\View.java

  1. public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource {  
  2.   ...  
  3.   /** 
  4.    * The layout parameters associated with this view and used by the parent 
  5.    * {@link android.view.ViewGroup} to determine how this view should be 
  6.    * laid out. 
  7.    * {@hide} 
  8.    */  
  9.   //該View擁有的 LayoutParams屬性,父試圖添加該View時,會爲其賦值,特別注意,其類型爲ViewGroup.LayoutParams。  
  10.   protected ViewGroup.LayoutParams mLayoutParams;    
  11.   ...  
  12. }  

     2.2、  ViewGroup.LayoutParams源碼分析

      路徑位於:frameworks\base\core\java\android\view\ViewGroup.java

  1. public abstract class ViewGroup extends View implements ViewParent, ViewManager {  
  2.     ...  
  3.      public static class LayoutParams {  
  4.         /** 
  5.          * Special value for the height or width requested by a View. 
  6.          * FILL_PARENT means that the view wants to be as big as its parent, 
  7.          * minus the parent's padding, if any. This value is deprecated 
  8.          * starting in API Level 8 and replaced by {@link #MATCH_PARENT}. 
  9.          */  
  10.         @Deprecated  
  11.         public static final int FILL_PARENT = -1;  // 注意值爲-1,Android2.2版本不建議使用  
  12.         /** 
  13.          * Special value for the height or width requested by a View. 
  14.          * MATCH_PARENT means that the view wants to be as big as its parent, 
  15.          * minus the parent's padding, if any. Introduced in API Level 8. 
  16.          */  
  17.         public static final int MATCH_PARENT = -1// 注意值爲-1  
  18.         /** 
  19.          * Special value for the height or width requested by a View. 
  20.          * WRAP_CONTENT means that the view wants to be just large enough to fit 
  21.          * its own internal content, taking its own padding into account. 
  22.          */  
  23.         public static final int WRAP_CONTENT = -2// 注意值爲-2  
  24.         /** 
  25.          * Information about how wide the view wants to be. Can be one of the 
  26.          * constants FILL_PARENT (replaced by MATCH_PARENT , 
  27.          * in API Level 8) or WRAP_CONTENT. or an exact size. 
  28.          */  
  29.         public int width;  //該View的寬度,可以爲WRAP_CONTENT/MATCH_PARENT 或者一個具體值  
  30.         /** 
  31.          * Information about how tall the view wants to be. Can be one of the 
  32.          * constants FILL_PARENT (replaced by MATCH_PARENT , 
  33.          * in API Level 8) or WRAP_CONTENT. or an exact size. 
  34.          */  
  35.         public int height; //該View的高度,可以爲WRAP_CONTENT/MATCH_PARENT 或者一個具體值  
  36.         /** 
  37.          * Used to animate layouts. 
  38.          */  
  39.         public LayoutAnimationController.AnimationParameters layoutAnimationParameters;  
  40.         /** 
  41.          * Creates a new set of layout parameters. The values are extracted from 
  42.          * the supplied attributes set and context. The XML attributes mapped 
  43.          * to this set of layout parameters are:、 
  44.          */  
  45.         public LayoutParams(Context c, AttributeSet attrs) {  
  46.             TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_Layout);  
  47.             setBaseAttributes(a,  
  48.                     R.styleable.ViewGroup_Layout_layout_width,  
  49.                     R.styleable.ViewGroup_Layout_layout_height);  
  50.             a.recycle();  
  51.         }  
  52.   
  53.         /** 
  54.          * Creates a new set of layout parameters with the specified width 
  55.          * and height. 
  56.          */  
  57.         public LayoutParams(int width, int height) {  
  58.             this.width = width;  
  59.             this.height = height;  
  60.         }  
  61.         /** 
  62.          * Copy constructor. Clones the width and height values of the source. 
  63.          * 
  64.          * @param source The layout params to copy from. 
  65.          */  
  66.         public LayoutParams(LayoutParams source) {  
  67.             this.width = source.width;  
  68.             this.height = source.height;  
  69.         }  
  70.         /** 
  71.          * Used internally by MarginLayoutParams. 
  72.          * @hide 
  73.          */  
  74.         LayoutParams() {  
  75.         }  
  76.         /** 
  77.          * Extracts the layout parameters from the supplied attributes. 
  78.          * 
  79.          * @param a the style attributes to extract the parameters from 
  80.          * @param widthAttr the identifier of the width attribute 
  81.          * @param heightAttr the identifier of the height attribute 
  82.          */  
  83.         protected void setBaseAttributes(TypedArray a, int widthAttr, int heightAttr) {  
  84.             width = a.getLayoutDimension(widthAttr, "layout_width");  
  85.             height = a.getLayoutDimension(heightAttr, "layout_height");  
  86.         }  
  87. }  

       我們發現FILL_PARENT/MATCH_PARENT值爲 -1 ,WRAP_CONETENT值爲-2,是不是有點詫異? 將值

  設置爲負值的目的是爲了區別View的具體值(an exact size) 總是大於0的。


       ViewGroup子類可以實現自定義LayoutParams,自定義LayoutParams提供了更好地擴展性,例如LinearLayout

 就有LinearLayout. LayoutParams自定義類(見下文)。整個LayoutParams類家族還是挺複雜的。

      ViewGroup.LayoutParams及其常用派生類的類圖(部分類圖)如下:

                  


                             該類圖是在太龐大了,大家有興趣的去看看Android API吧。

           


      前面我們說過,每個View都包含一個ViewGroup.LayoutParams類或者其派生類,下面我們的疑問是Android框架

 中時如何爲View設置其LayoutParams屬性的。


     有兩種方法會設置View的LayoutParams屬性:

       1、 直接添加子View時,常見於如下幾種方法:ViewGroup.java

  1. //Adds a child view.      
  2. void addView(View child, int index)  
  3. //Adds a child view with this ViewGroup's default layout parameters   
  4. //and the specified width and height.  
  5. void addView(View child, int width, int height)  
  6. //Adds a child view with the specified layout parameters.         
  7. void addView(View child, ViewGroup.LayoutParams params)  

         三個重載方法的區別只是添加View時構造LayoutParams對象的方式不同而已,稍後我們探尋一下它們的源碼。

     2、 通過xml佈局文件指定某個View的屬性爲:android:layout_heigth=””以及android:layout_weight=”” 時。

    總的來說,這兩種方式都會設定View的LayoutParams屬性值----指定的或者Default值。

  方式1流程分析

     直接添加子View時,比較容易理解,我們先來看看這種方式設置LayoutParams的過程:

         路徑:\frameworks\base\core\java\android\view\ViewGroup.java

  1. public abstract class ViewGroup extends View implements ViewParent, ViewManager {  
  2.     ...  
  3.     /** 
  4.      * Adds a child view. If no layout parameters are already set on the child, the 
  5.      * default parameters for this ViewGroup are set on the child. 
  6.      * 
  7.      * @param child the child view to add 
  8.      * 
  9.      * @see #generateDefaultLayoutParams() 
  10.      */  
  11.     public void addView(View child) {  
  12.         addView(child, -1);  
  13.     }  
  14.     /** 
  15.      * Adds a child view. If no layout parameters are already set on the child, the 
  16.      * default parameters for this ViewGroup are set on the child. 
  17.      * 
  18.      * @param child the child view to add 
  19.      * @param index the position at which to add the child 
  20.      * 
  21.      * @see #generateDefaultLayoutParams() 
  22.      */  
  23.     public void addView(View child, int index) {  
  24.         LayoutParams params = child.getLayoutParams();  
  25.         if (params == null) {  
  26.             params = generateDefaultLayoutParams(); //返回默認地LayoutParams類,作爲該View的屬性值  
  27.             if (params == null) {//如果不能獲取到LayoutParams對象,則拋出異常。  
  28.                 throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");  
  29.             }  
  30.         }  
  31.         addView(child, index, params);  
  32.     }  
  33.     /** 
  34.      * Adds a child view with this ViewGroup's default layout parameters and the 
  35.      * specified width and height. 
  36.      * 
  37.      * @param child the child view to add 
  38.      */  
  39.     public void addView(View child, int width, int height) {  
  40.         //返回默認地LayoutParams類,作爲該View的屬性值  
  41.         final LayoutParams params = generateDefaultLayoutParams();   
  42.         params.width = width;   //重新設置width值  
  43.         params.height = height; //重新設置height值  
  44.         addView(child, -1, params); //這兒,我們有指定width、height的大小了。  
  45.     }  
  46.     /** 
  47.      * Adds a child view with the specified layout parameters. 
  48.      * 
  49.      * @param child the child view to add 
  50.      * @param params the layout parameters to set on the child 
  51.      */  
  52.     public void addView(View child, LayoutParams params) {  
  53.         addView(child, -1, params);  
  54.     }  
  55.     /** 
  56.      * Adds a child view with the specified layout parameters. 
  57.      * 
  58.      * @param child the child view to add 
  59.      * @param index the position at which to add the child 
  60.      * @param params the layout parameters to set on the child 
  61.      */  
  62.     public void addView(View child, int index, LayoutParams params) {  
  63.         ...  
  64.         // addViewInner() will call child.requestLayout() when setting the new LayoutParams  
  65.         // therefore, we call requestLayout() on ourselves before, so that the child's request  
  66.         // will be blocked at our level  
  67.         requestLayout();  
  68.         invalidate();  
  69.         addViewInner(child, index, params, false);  
  70.     }  
  71.     /** 
  72.      * Returns a set of default layout parameters. These parameters are requested 
  73.      * when the View passed to {@link #addView(View)} has no layout parameters 
  74.      * already set. If null is returned, an exception is thrown from addView. 
  75.      * 
  76.      * @return a set of default layout parameters or null 
  77.      */  
  78.     protected LayoutParams generateDefaultLayoutParams() {  
  79.         //width 爲 WRAP_CONTENT大小 , height 爲WRAP_CONTENT   
  80.         //ViewGroup的子類可以重寫該方法,達到其特定要求。稍後會以LinearLayout類爲例說明。  
  81.         return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);  
  82.     }  
  83.     private void addViewInner(View child, int index, LayoutParams params,  
  84.             boolean preventRequestLayout) {  
  85.   
  86.         if (!checkLayoutParams(params)) { //params對象是否爲null  
  87.             params = generateLayoutParams(params); //如果params對象是爲null,重新構造個LayoutParams對象  
  88.         }  
  89.         //preventRequestLayout值爲false  
  90.         if (preventRequestLayout) {    
  91.             child.mLayoutParams = params; //爲View的mLayoutParams屬性賦值  
  92.         } else {  
  93.             child.setLayoutParams(params);//爲View的mLayoutParams屬性賦值,但會調用requestLayout()請求重新佈局  
  94.         }  
  95.         //if else 語句會設置View爲mLayoutParams屬性賦值  
  96.         ...  
  97.     }  
  98.     ...  
  99. }  

      主要功能就是在添加子View時爲其構建了一個LayoutParams對象。但更重要的是,ViewGroup的子類可以重載

 上面的幾個方法,返回特定的LayoutParams對象,例如:對於LinearLayout而言,則是LinearLayout.LayoutParams

 對象。這麼做地目的是,能在其他需要它的地方,可以將其強制轉換成LinearLayout.LayoutParams對象。


      LinearLayout重寫函數地實現爲:

  1. public class LinearLayout extends ViewGroup {  
  2.     ...  
  3.     @Override  
  4.     public LayoutParams generateLayoutParams(AttributeSet attrs) {  
  5.         return new LinearLayout.LayoutParams(getContext(), attrs);  
  6.     }  
  7.     @Override  
  8.     protected LayoutParams generateDefaultLayoutParams() {  
  9.         //該LinearLayout是水平方向還是垂直方向  
  10.         if (mOrientation == HORIZONTAL) {   
  11.             return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);  
  12.         } else if (mOrientation == VERTICAL) {  
  13.             return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);  
  14.         }  
  15.         return null;  
  16.     }  
  17.     @Override  
  18.     protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {  
  19.         return new LayoutParams(p);  
  20.     }  
  21.     /** 
  22.      * Per-child layout information associated with ViewLinearLayout. 
  23.      *  
  24.      * @attr ref android.R.styleable#LinearLayout_Layout_layout_weight 
  25.      * @attr ref android.R.styleable#LinearLayout_Layout_layout_gravity 
  26.      */ //自定義的LayoutParams類  
  27.     public static class LayoutParams extends ViewGroup.MarginLayoutParams {  
  28.         /** 
  29.          * Indicates how much of the extra space in the LinearLayout will be 
  30.          * allocated to the view associated with these LayoutParams. Specify 
  31.          * 0 if the view should not be stretched. Otherwise the extra pixels 
  32.          * will be pro-rated among all views whose weight is greater than 0. 
  33.          */  
  34.         @ViewDebug.ExportedProperty(category = "layout")  
  35.         public float weight;      //  見於屬性,android:layout_weight=""  ;  
  36.         /** 
  37.          * Gravity for the view associated with these LayoutParams. 
  38.          * 
  39.          * @see android.view.Gravity 
  40.          */  
  41.         public int gravity = -1;  // 見於屬性, android:layout_gravity=""  ;   
  42.         /** 
  43.          * {@inheritDoc} 
  44.          */  
  45.         public LayoutParams(Context c, AttributeSet attrs) {  
  46.             super(c, attrs);  
  47.             TypedArray a =c.obtainStyledAttributes(attrs, com.android.internal.R.styleable.LinearLayout_Layout);  
  48.             weight = a.getFloat(com.android.internal.R.styleable.LinearLayout_Layout_layout_weight, 0);  
  49.             gravity = a.getInt(com.android.internal.R.styleable.LinearLayout_Layout_layout_gravity, -1);  
  50.   
  51.             a.recycle();  
  52.         }  
  53.         /** 
  54.          * {@inheritDoc} 
  55.          */  
  56.         public LayoutParams(int width, int height) {  
  57.             super(width, height);  
  58.             weight = 0;  
  59.         }  
  60.         /** 
  61.          * Creates a new set of layout parameters with the specified width, height 
  62.          * and weight. 
  63.          * 
  64.          * @param width the width, either {@link #MATCH_PARENT}, 
  65.          *        {@link #WRAP_CONTENT} or a fixed size in pixels 
  66.          * @param height the height, either {@link #MATCH_PARENT}, 
  67.          *        {@link #WRAP_CONTENT} or a fixed size in pixels 
  68.          * @param weight the weight 
  69.          */  
  70.         public LayoutParams(int width, int height, float weight) {  
  71.             super(width, height);  
  72.             this.weight = weight;  
  73.         }  
  74.         public LayoutParams(ViewGroup.LayoutParams p) {  
  75.             super(p);  
  76.         }  
  77.         public LayoutParams(MarginLayoutParams source) {  
  78.             super(source);  
  79.         }  
  80.     }  
  81.     ...  
  82. }  

       LinearLayout.LayoutParams類繼承至ViewGroup.MarginLayoutParams類,添加了對android:layout_weight以及

   android:layout_gravity這兩個屬性的獲取和保存。而且它的重寫函數返回的都是LinearLayout.LayoutParams

   類型。樣,我們可以再對子View進行其他操作時,可以將將其強制轉換成LinearLayout.LayoutParams對象進行

   使用。

         例如,LinearLayout進行measure過程,使用了LinearLayout.LayoutParam對象,有如下代碼:

  1. public class LinearLayout extends ViewGroup {  
  2.     ...  
  3.     @Override  //onMeasure方法。  
  4.     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
  5.         //判斷是垂直方向還是水平方向,這兒我們假設是VERTICAL垂直方向,  
  6.         if (mOrientation == VERTICAL) {  
  7.             measureVertical(widthMeasureSpec, heightMeasureSpec);  
  8.         } else {  
  9.             measureHorizontal(widthMeasureSpec, heightMeasureSpec);  
  10.         }  
  11.     }  
  12.      /** 
  13.      * Measures the children when the orientation of this LinearLayout is set 
  14.      * to {@link #VERTICAL}. 
  15.      * 
  16.      * @param widthMeasureSpec Horizontal space requirements as imposed by the parent. 
  17.      * @param heightMeasureSpec Vertical space requirements as imposed by the parent. 
  18.      * 
  19.      * @see #getOrientation() 
  20.      * @see #setOrientation(int) 
  21.      * @see #onMeasure(int, int) 
  22.      */  
  23.       void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {  
  24.             mTotalLength = 0;  
  25.             ...  
  26.             // See how tall everyone is. Also remember max width.  
  27.             for (int i = 0; i < count; ++i) {  
  28.                 final View child = getVirtualChildAt(i); //獲得索引處爲i的子VIew     
  29.                 ...  
  30.                 //注意,我們將類型爲 ViewGroup.LayoutParams的實例對象強制轉換爲了LinearLayout.LayoutParams,  
  31.                 //即父對象轉換爲了子對象,能這樣做的原因就是LinearLayout的所有子View的LayoutParams類型都爲  
  32.                 //LinearLayout.LayoutParams  
  33.                 LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();  
  34.                 ...  
  35.         }  
  36.     ...  
  37. }  


        超類ViewGroup.LayoutParams強制轉換爲了子類LinearLayout.LayoutParams,因爲LinearLayout的每個

  ”直接“子ViewLayoutParams屬性都是LinearLayout.LayoutParams類型,因此可以安全轉換。


       PS : Android 2.3源碼Launcher2中也實現了自定義的LayoutParams類,在IDLE界面的每個View至少包含如下

  信息:所在X方向的單元格索引和高度、所在Y方向的單元格索引和高度等。

            路徑: packages\apps\Launcher2\src\com\android\launcher2\CellLayout.java

  1. public class CellLayout extends ViewGroup {  
  2.     ...   
  3.     public static class LayoutParams extends ViewGroup.MarginLayoutParams {  
  4.             /** 
  5.              * Horizontal location of the item in the grid. 
  6.              */  
  7.             public int cellX;   //X方向的單元格索引  
  8.             /** 
  9.              * Vertical location of the item in the grid. 
  10.              */  
  11.             public int cellY;   //Y方向的單元格索引  
  12.             /** 
  13.              * Number of cells spanned horizontally by the item. 
  14.              */  
  15.             public int cellHSpan;  //水平方向所佔高度  
  16.             /** 
  17.              * Number of cells spanned vertically by the item. 
  18.              */  
  19.             public int cellVSpan;  //垂直方向所佔高度  
  20.             ...  
  21.             public LayoutParams(Context c, AttributeSet attrs) {  
  22.                 super(c, attrs);  
  23.                 cellHSpan = 1;  //默認爲高度 1  
  24.                 cellVSpan = 1;  
  25.             }  
  26.   
  27.             public LayoutParams(ViewGroup.LayoutParams source) {  
  28.                 super(source); //默認爲高度 1  
  29.                 cellHSpan = 1;  
  30.                 cellVSpan = 1;  
  31.             }  
  32.               
  33.             public LayoutParams(int cellX, int cellY, int cellHSpan, int cellVSpan) {  
  34.                 super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);  
  35.                 this.cellX = cellX;  
  36.                 this.cellY = cellY;  
  37.                 this.cellHSpan = cellHSpan;  
  38.                 this.cellVSpan = cellVSpan;  
  39.             }  
  40.             ...  
  41.         }  
  42.     ...  
  43. }  

     對該自定義CellLayout.LayoutParams類的使用可以參考LinearLayout.LayoutParams類,我也不再贅述了。


 方法2流程分析

        使用屬性android:layout_heigth=””以及android:layout_weight=”” 時,爲某個View設置LayoutParams值。

 

       其實這種賦值方法其實也如同前面那種,只不過它需要一個前期孵化過程---需要利用XML解析將佈局文件

  解析成一個完整的View樹,可別小看它了,所有Xxx.xml的佈局文件都需要解析成一個完整的View樹。下面,

  我們就來仔細走這個過程,重點關注如下兩個方面

         ①、xml佈局是如何解析成View樹的 ;

         ②、android:layout_heigth=””和android:layout_weight=””的解析。


        PS: 一直以來,我都想當然android:layout_heigth以及android:layout_weight這兩個屬性的解析過程是在

   View.java內部完成的,但當我真正去找尋時,卻一直沒有在View.java類或者ViewGroup.java類找到。直到一位

   網友的一次提問,才發現它們的藏身之地。


3、佈局文件解析流程分析


       解析佈局文件時,使用的類爲LayoutInflater。 關於該類的使用請參考如下博客:

                            <android中LayoutInflater的使用 >>

      主要有如下API方法:

        public View inflate (XmlPullParser parser, ViewGroup root, boolean attachToRoot)

          public View inflate (int resource, ViewGroup root)

          public View inflate (int resource, ViewGroup root, boolean attachToRoot)

     這三個類主要迷惑之處在於地三個參數attachToRoot,即是否將該View樹添加到root中去。具體可看這篇博客:

                                     <<關於inflate的第3個參數>>

    當然還有LayoutInflater的inflate()的其他重載方法,大家可以自行了解下。

     

    我利用下面的例子給大家走走這個流程 :

  1. public class MainActivity extends Activity {  
  2.     /** Called when the activity is first created. */  
  3.     @Override  
  4.     public void onCreate(Bundle savedInstanceState) {  
  5.         super.onCreate(savedInstanceState);  
  6.         //1、該方法最終也會調用到 LayoutInflater的inflate()方法中去解析。  
  7.         setContentView(R.layout.main);  
  8.           
  9.         //2、使用常見的API方法去解析xml佈局文件,  
  10.         LayoutInflater layoutInflater = (LayoutInflater)getSystemService();  
  11.         View root = layoutInflater.inflate(R.layout.main, null);  
  12.     }  
  13. }  

   Step 1、獲得LayoutInflater的引用。

         路徑:\frameworks\base\core\java\android\app\ContextImpl.java

  1. /** 
  2.  * Common implementation of Context API, which provides the base 
  3.  * context object for Activity and other application components. 
  4.  */  
  5. class ContextImpl extends Context {  
  6.     if (WINDOW_SERVICE.equals(name)) {  
  7.         return WindowManagerImpl.getDefault();  
  8.     } else if (LAYOUT_INFLATER_SERVICE.equals(name)) {  
  9.         synchronized (mSync) {  
  10.             LayoutInflater inflater = mLayoutInflater;  
  11.             //是否已經賦值,如果是,直接返回引用  
  12.             if (inflater != null) {  
  13.                 return inflater;  
  14.             }  
  15.             //返回一個LayoutInflater對象,getOuterContext()指的是我們的Activity、Service或者Application引用  
  16.             mLayoutInflater = inflater = PolicyManager.makeNewLayoutInflater(getOuterContext());  
  17.             return inflater;  
  18.         }  
  19.     } else if (ACTIVITY_SERVICE.equals(name)) {  
  20.         return getActivityManager();  
  21.     }...  
  22. }  

         繼續去PolicyManager查詢對應函數,看看內部實現。    

           徑:frameworks\base\core\java\com\android\internal\policy\PolicyManager.java

  1. public final class PolicyManager {  
  2.     private static final String POLICY_IMPL_CLASS_NAME = "com.android.internal.policy.impl.Policy";  
  3.     private static final IPolicy sPolicy;   // 這可不是Binder機制額,這只是是一個接口,別想多啦  
  4.     static {  
  5.         // Pull in the actual implementation of the policy at run-time  
  6.         try {  
  7.             Class policyClass = Class.forName(POLICY_IMPL_CLASS_NAME);  
  8.             sPolicy = (IPolicy)policyClass.newInstance();  
  9.         }  
  10.         ...  
  11.     }  
  12.     ...  
  13.     public static LayoutInflater makeNewLayoutInflater(Context context) {  
  14.         return sPolicy.makeNewLayoutInflater(context); //繼續去實現類中去查找  
  15.     }  
  16. }  
    IPolicy接口的實現對爲Policy類。路徑:/frameworks/base/policy/src/com/android/internal/policy/impl/Policy.java

  1. //Simple implementation of the policy interface that spawns the right  
  2. //set of objects  
  3. public class Policy implements IPolicy{  
  4.     ...  
  5.     public PhoneLayoutInflater makeNewLayoutInflater(Context context) {  
  6.         //實際上返回的是PhoneLayoutInflater類。  
  7.         return new PhoneLayoutInflater(context);  
  8.     }  
  9. }  
  10. //PhoneLayoutInflater繼承至LayoutInflater類  
  11. public class PhoneLayoutInflater extends LayoutInflater {  
  12.     ...  
  13.     /** 
  14.      * Instead of instantiating directly, you should retrieve an instance 
  15.      * through {@link Context#getSystemService} 
  16.      *  
  17.      * @param context The Context in which in which to find resources and other 
  18.      *                application-specific things. 
  19.      *  
  20.      * @see Context#getSystemService 
  21.      */  
  22.     public PhoneLayoutInflater(Context context) {  
  23.         super(context);  
  24.     }  
  25.     ...  
  26. }  


       LayoutInflater是個抽象類,實際上我們返回的是PhoneLayoutInflater類,但解析過程的操作基本上是在

  LayoutInflater中完成地。



   Step 2、調用inflate()方法去解析佈局文件。
  1. public abstract class LayoutInflater {  
  2.     ...  
  3.     public View inflate(int resource, ViewGroup root) {  
  4.         //繼續看下個函數,注意root爲null  
  5.         return inflate(resource, root, root != null);   
  6.     }  
  7.       
  8.     public View inflate(int resource, ViewGroup root, boolean attachToRoot) {  
  9.         //獲取一個XmlResourceParser來解析XML文件---佈局文件。  
  10.         //XmlResourceParser類以及xml是如何解析的,大家自己有興趣找找。  
  11.         XmlResourceParser parser = getContext().getResources().getLayout(resource);  
  12.         try {  
  13.             return inflate(parser, root, attachToRoot);  
  14.         } finally {  
  15.             parser.close();  
  16.         }  
  17.     }  
  18. }  
  19. /** 
  20.  * The XML parsing interface returned for an XML resource.  This is a standard 
  21.  * XmlPullParser interface, as well as an extended AttributeSet interface and 
  22.  * an additional close() method on this interface for the client to indicate 
  23.  * when it is done reading the resource. 
  24.  */  
  25. public interface XmlResourceParser extends XmlPullParser, AttributeSet {  
  26.     /** 
  27.      * Close this interface to the resource.  Calls on the interface are no 
  28.      * longer value after this call. 
  29.      */  
  30.     public void close();  
  31. }  


      我們獲得了一個當前應用程序環境的XmlResourceParser對象,該對象的主要作用就是來解析xml佈局文件的。

  XmlResourceParser類是個接口類,更多關於XML解析的,大家可以參考下面博客:

                             <<android之XmlResourceParser類使用實例>>

                                   <<android解析xml文件的方式(其一)>>

                                   <<android解析xml文件的方式(其二)>>

                                   <<android解析xml文件的方式(其三)>>



   Step 3、真正地開始解析工作 。
  1. public abstract class LayoutInflater {  
  2.     ...  
  3.     /** 
  4.      * Inflate a new view hierarchy from the specified XML node. Throws 
  5.      * {@link InflateException} if there is an error. 
  6.      */  
  7.     //我們傳遞過來的參數如下: root 爲null , attachToRoot爲false 。  
  8.     public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {  
  9.         synchronized (mConstructorArgs) {  
  10.             final AttributeSet attrs = Xml.asAttributeSet(parser);  
  11.             Context lastContext = (Context)mConstructorArgs[0];  
  12.             mConstructorArgs[0] = mContext;  //該mConstructorArgs屬性最後會作爲參數傳遞給View的構造函數  
  13.             View result = root;  //根View  
  14.   
  15.             try {  
  16.                 // Look for the root node.  
  17.                 int type;  
  18.                 while ((type = parser.next()) != XmlPullParser.START_TAG &&  
  19.                         type != XmlPullParser.END_DOCUMENT) {  
  20.                     // Empty  
  21.                 }  
  22.                 ...  
  23.                 final String name = parser.getName();  //節點名,即API中的控件或者自定義View完整限定名。  
  24.                 if (TAG_MERGE.equals(name)) { // 處理<merge />標籤  
  25.                     if (root == null || !attachToRoot) {  
  26.                         throw new InflateException("<merge /> can be used only with a valid "  
  27.                                 + "ViewGroup root and attachToRoot=true");  
  28.                     }  
  29.                     //將<merge />標籤的View樹添加至root中,該函數稍後講到。  
  30.                     rInflate(parser, root, attrs);  
  31.                 } else {  
  32.                     // Temp is the root view that was found in the xml  
  33.                     //創建該xml佈局文件所對應的根View。  
  34.                     View temp = createViewFromTag(name, attrs);   
  35.   
  36.                     ViewGroup.LayoutParams params = null;  
  37.   
  38.                     if (root != null) {  
  39.                         // Create layout params that match root, if supplied  
  40.                         //根據AttributeSet屬性獲得一個LayoutParams實例,記住調用者爲root。  
  41.                         params = root.generateLayoutParams(attrs);   
  42.                         if (!attachToRoot) { //重新設置temp的LayoutParams  
  43.                             // Set the layout params for temp if we are not  
  44.                             // attaching. (If we are, we use addView, below)  
  45.                             temp.setLayoutParams(params);  
  46.                         }  
  47.                     }  
  48.                     // Inflate all children under temp  
  49.                     //添加所有其子節點,即添加所有字View  
  50.                     rInflate(parser, temp, attrs);  
  51.                       
  52.                     // We are supposed to attach all the views we found (int temp)  
  53.                     // to root. Do that now.  
  54.                     if (root != null && attachToRoot) {  
  55.                         root.addView(temp, params);  
  56.                     }  
  57.                     // Decide whether to return the root that was passed in or the  
  58.                     // top view found in xml.  
  59.                     if (root == null || !attachToRoot) {  
  60.                         result = temp;  
  61.                     }  
  62.                 }  
  63.             }   
  64.             ...  
  65.             return result;  
  66.         }  
  67.     }  
  68.       
  69.     /* 
  70.      * default visibility so the BridgeInflater can override it. 
  71.      */  
  72.     View createViewFromTag(String name, AttributeSet attrs) {  
  73.         //節點是否爲View,如果是將其重新賦值,形如 <View class="com.qin.xxxView"></View>  
  74.         if (name.equals("view")) {    
  75.             name = attrs.getAttributeValue(null"class");  
  76.         }  
  77.         try {  
  78.             View view = (mFactory == null) ? null : mFactory.onCreateView(name,  
  79.                     mContext, attrs);  //沒有設置工廠方法  
  80.   
  81.             if (view == null) {  
  82.                 //通過這個判斷是Android API的View,還是自定義View  
  83.                 if (-1 == name.indexOf('.')) {  
  84.                     view = onCreateView(name, attrs); //創建Android API的View實例  
  85.                 } else {  
  86.                     view = createView(name, null, attrs);//創建一個自定義View實例  
  87.                 }  
  88.             }  
  89.             return view;  
  90.         }   
  91.         ...  
  92.     }  
  93.     //獲得具體視圖的實例對象  
  94.     public final View createView(String name, String prefix, AttributeSet attrs) {  
  95.         Constructor constructor = sConstructorMap.get(name);  
  96.         Class clazz = null;  
  97.         //以下功能主要是獲取如下三個類對象:  
  98.         //1、類加載器  ClassLoader  
  99.         //2、Class對象  
  100.         //3、類的構造方法句柄 Constructor  
  101.         try {  
  102.             if (constructor == null) {  
  103.             // Class not found in the cache, see if it's real, and try to add it  
  104.             clazz = mContext.getClassLoader().loadClass(prefix != null ? (prefix + name) : name);  
  105.             ...  
  106.             constructor = clazz.getConstructor(mConstructorSignature);  
  107.             sConstructorMap.put(name, constructor);  
  108.         } else {  
  109.             // If we have a filter, apply it to cached constructor  
  110.             if (mFilter != null) {  
  111.                 ...     
  112.             }  
  113.         }  
  114.             //傳遞參數獲得該View實例對象  
  115.             Object[] args = mConstructorArgs;  
  116.             args[1] = attrs;  
  117.             return (View) constructor.newInstance(args);  
  118.         }   
  119.         ...  
  120.     }  
  121.   
  122. }  

     這段代碼的作用是獲取xml佈局文件的root View,做了如下兩件事情

          1、獲取xml佈局的View實例,通過createViewFromTag()方法獲取,該方法會判斷節點名是API 控件

            還是自定義控件,繼而調用合適的方法去實例化View。

          2、判斷root以及attachToRoot參數,重新設置root View值以及temp變量的LayoutParams值。


        如果仔細看着段代碼,不知大家心裏有沒有疑惑:當root爲null時,我們的temp變量的LayoutParams值是爲

  null的,即它不會被賦值?有個View的LayoutParams值爲空,那麼,在系統中不會報異常嗎?見下面部分

  代碼:

  1. //我們傳遞過來的參數如下: root 爲null , attachToRoot爲false 。  
  2. public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {  
  3.     synchronized (mConstructorArgs) {  
  4.         ...  
  5.         try {  
  6.               
  7.             ...  
  8.             if (TAG_MERGE.equals(name)) { // 處理<merge />標籤  
  9.                 ...  
  10.             } else {  
  11.                 // Temp is the root view that was found in the xml  
  12.                 //創建該xml佈局文件所對應的根View。  
  13.                 View temp = createViewFromTag(name, attrs);   
  14.                 ViewGroup.LayoutParams params = null;  
  15.   
  16.                 //注意!!! root爲null時,temp變量的LayoutParams屬性不會被賦值的。  
  17.                 if (root != null) {  
  18.                     // Create layout params that match root, if supplied  
  19.                     //根據AttributeSet屬性獲得一個LayoutParams實例,記住調用者爲root。  
  20.                     params = root.generateLayoutParams(attrs);   
  21.                     if (!attachToRoot) { //重新設置temp的LayoutParams  
  22.                         // Set the layout params for temp if we are not  
  23.                         // attaching. (If we are, we use addView, below)  
  24.                         temp.setLayoutParams(params);  
  25.                     }  
  26.                 }  
  27.                 ...  
  28.             }  
  29.         }   
  30.         ...  
  31.     }  
  32. }  

        關於這個問題的詳細答案,我會在後面講到。這兒我簡單說下,任何View樹的頂層View被添加至窗口時,

  一般調用WindowManager.addView()添加至窗口時,在這個方法中去做進一步處理。即使LayoutParams

  值空,UI框架每次measure()時都忽略該View的LayoutParams值,而是直接傳遞MeasureSpec值至View樹。


       接下來,我們關注另外一個函數,rInflate(),該方法會遞歸調用每個View下的子節點,以當前View作爲根View

 形成一個View樹。

  1. /** 
  2.  * Recursive method used to descend down the xml hierarchy and instantiate 
  3.  * views, instantiate their children, and then call onFinishInflate(). 
  4.  */  
  5. //遞歸調用每個字節點  
  6. private void rInflate(XmlPullParser parser, View parent, final AttributeSet attrs)  
  7.         throws XmlPullParserException, IOException {  
  8.   
  9.     final int depth = parser.getDepth();  
  10.     int type;  
  11.   
  12.     while (((type = parser.next()) != XmlPullParser.END_TAG ||  
  13.             parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {  
  14.   
  15.         if (type != XmlPullParser.START_TAG) {  
  16.             continue;  
  17.         }  
  18.         final String name = parser.getName();  
  19.           
  20.         if (TAG_REQUEST_FOCUS.equals(name)) { //處理<requestFocus />標籤  
  21.             parseRequestFocus(parser, parent);  
  22.         } else if (TAG_INCLUDE.equals(name)) { //處理<include />標籤  
  23.             if (parser.getDepth() == 0) {  
  24.                 throw new InflateException("<include /> cannot be the root element");  
  25.             }  
  26.             parseInclude(parser, parent, attrs);//解析<include />節點  
  27.         } else if (TAG_MERGE.equals(name)) { //處理<merge />標籤  
  28.             throw new InflateException("<merge /> must be the root element");  
  29.         } else {  
  30.             //根據節點名構建一個View實例對象  
  31.             final View view = createViewFromTag(name, attrs);   
  32.             final ViewGroup viewGroup = (ViewGroup) parent;  
  33.             //調用generateLayoutParams()方法返回一個LayoutParams實例對象,  
  34.             final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);  
  35.             rInflate(parser, view, attrs); //繼續遞歸調用  
  36.             viewGroup.addView(view, params); //OK,將該View以特定LayoutParams值添加至父View中  
  37.         }  
  38.     }  
  39.     parent.onFinishInflate();  //完成了解析過程,通知....  
  40. }  

          值得注意的是,每次addView前都調用了viewGroup.generateLayoutParams(attrs)去構建一個LayoutParams

  實例,然後在addView()方法中爲其賦值。參見如下代碼:ViewGroup.java

  

  1. public abstract class ViewGroup extends View implements ViewParent, ViewManager {  
  2.     ...  
  3.       
  4.     public LayoutParams generateLayoutParams(AttributeSet attrs) {  
  5.         return new LayoutParams(getContext(), attrs);  
  6.     }  
  7.     public static class LayoutParams {  
  8.         ... //會調用這個構造函數  
  9.         public LayoutParams(Context c, AttributeSet attrs) {  
  10.             TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_Layout);  
  11.             setBaseAttributes(a,  
  12.                     R.styleable.ViewGroup_Layout_layout_width,  
  13.                     R.styleable.ViewGroup_Layout_layout_height);  
  14.             a.recycle();  
  15.         }  
  16.         protected void setBaseAttributes(TypedArray a, int widthAttr, int heightAttr) {  
  17.             width = a.getLayoutDimension(widthAttr, "layout_width");  
  18.             height = a.getLayoutDimension(heightAttr, "layout_height");  
  19.         }  
  20.       
  21. }  

    好吧 ~~ 我們還是探尋根底,去TypeArray類的getLayoutDimension()看看。

        路徑:/frameworks/base/core/java/android/content/res/TypedArray.java

  1. public class TypedArray {  
  2.     ...  
  3.     /** 
  4.      * Special version of {@link #getDimensionPixelSize} for retrieving 
  5.      * {@link android.view.ViewGroup}'s layout_width and layout_height 
  6.      * attributes.  This is only here for performance reasons; applications 
  7.      * should use {@link #getDimensionPixelSize}. 
  8.      *  
  9.      * @param index Index of the attribute to retrieve. 
  10.      * @param name Textual name of attribute for error reporting. 
  11.      *  
  12.      * @return Attribute dimension value multiplied by the appropriate  
  13.      * metric and truncated to integer pixels. 
  14.      */  
  15.     public int getLayoutDimension(int index, String name) {  
  16.         index *= AssetManager.STYLE_NUM_ENTRIES;  
  17.         final int[] data = mData;  
  18.         //獲得屬性對應的標識符 , Identifies,目前還沒有仔細研究相關類。  
  19.         final int type = data[index+AssetManager.STYLE_TYPE];  
  20.         if (type >= TypedValue.TYPE_FIRST_INT  
  21.                 && type <= TypedValue.TYPE_LAST_INT) {  
  22.             return data[index+AssetManager.STYLE_DATA];  
  23.         } else if (type == TypedValue.TYPE_DIMENSION) { //類型爲dimension類型  
  24.             return TypedValue.complexToDimensionPixelSize(  
  25.                 data[index+AssetManager.STYLE_DATA], mResources.mMetrics);  
  26.         }  
  27.         //沒有提供layout_weight和layout_height會來到此處 ,這兒會報異常!  
  28.         //因此佈局文件中的View包括自定義View必須加上屬性layout_weight和layout_height。  
  29.         throw new RuntimeException(getPositionDescription()  
  30.                 + ": You must supply a " + name + " attribute.");  
  31.     }  
  32.     ...  
  33. }  

         從上面得知,  我們將View的AttributeSet屬性傳遞給generateLayoutParams()方法,讓其構建合適地

   LayoutParams對象,並且初始化屬性值weight和height。同時我們也得知 佈局文件中的View包括自定義View

   必須加上屬性layout_weight和layout_height,否則會報異常。


    Step 3 主要做了如下事情:
       首先,獲得了了佈局文件地root View,即佈局文件中最頂層的View。

       其次,通過遞歸調用,我們形成了整個View樹以及設置了每個View的LayoutParams對象。


    

主要知識點如下:
                 1、MeasureSpc類說明
                 2、measure過程詳解(揭祕其細節);
                 3、root View被添加至窗口時,UI框架是如何設置其LayoutParams值得。

       在講解measure過程前,我們非常有必要理解MeasureSpc類的使用,否則理解起來也只能算是囫圇吞棗。


 1、MeasureSpc類說明


   1.1  SDK 說明如下

              A MeasureSpec encapsulates the layout requirements passed from parent to child. Each MeasureSpec

         represents a requirement for either the width or the height. A MeasureSpec is comprised of a size and

         a mode. 

        即:
             MeasureSpc類封裝了父View傳遞給子View的佈局(layout)要求。每個MeasureSpc實例代表寬度或者高度

   (只能是其一)要求。 它有三種模式:

            ①、UNSPECIFIED(未指定),父元素部隊自元素施加任何束縛,子元素可以得到任意想要的大小;

            ②、EXACTLY(完全),父元素決定自元素的確切大小,子元素將被限定在給定的邊界裏而忽略它本身大小;

            ③、AT_MOST(至多),子元素至多達到指定大小的值。


   常用的三個函數:

  static int getMode(int measureSpec)  :  根據提供的測量值(格式)提取模式(上述三個模式之一)

     static int getSize(int measureSpec) : 根據提供的測量值(格式)提取大小值(這個大小也就是我們通常所說的大小)

     static int makeMeasureSpec(int size,int mode)  :  根據提供的大小值和模式創建一個測量值(格式)


             以上摘取自:  <<
MeasureSpec介紹及使用詳解>>

   1.2   MeasureSpc類源碼分析   其爲View.java類的內部類,路徑:\frameworks\base\core\java\android\view\View.java

  1. public class View implements ... {  
  2.      ...  
  3.      public static class MeasureSpec {  
  4.         private static final int MODE_SHIFT = 30//移位位數爲30  
  5.         //int類型佔32位,向右移位30位,該屬性表示掩碼值,用來與size和mode進行"&"運算,獲取對應值。  
  6.         private static final int MODE_MASK  = 0x3 << MODE_SHIFT;  
  7.   
  8.         //向右移位30位,其值爲00 + (30位0)  , 即 0x0000(16進製表示)  
  9.         public static final int UNSPECIFIED = 0 << MODE_SHIFT;  
  10.         //向右移位30位,其值爲01 + (30位0)  , 即0x1000(16進製表示)  
  11.         public static final int EXACTLY     = 1 << MODE_SHIFT;  
  12.         //向右移位30位,其值爲02 + (30位0)  , 即0x2000(16進製表示)  
  13.         public static final int AT_MOST     = 2 << MODE_SHIFT;  
  14.   
  15.         //創建一個整形值,其高兩位代表mode類型,其餘30位代表長或寬的實際值。可以是WRAP_CONTENT、MATCH_PARENT或具體大小exactly size  
  16.         public static int makeMeasureSpec(int size, int mode) {  
  17.             return size + mode;  
  18.         }  
  19.         //獲取模式  ,與運算  
  20.         public static int getMode(int measureSpec) {  
  21.             return (measureSpec & MODE_MASK);  
  22.         }  
  23.         //獲取長或寬的實際值 ,與運算  
  24.         public static int getSize(int measureSpec) {  
  25.             return (measureSpec & ~MODE_MASK);  
  26.         }  
  27.   
  28.     }  
  29.     ...  
  30. }  

    MeasureSpec類的處理思路是:

      ①、右移運算,使int 類型的高兩位表示模式的實際值,其餘30位表示其餘30位代表長或寬的實際值----可以是

         WRAP_CONTENT、MATCH_PARENT或具體大小exactly size。


      ②、通過掩碼MODE_MASK進行與運算 “&”,取得模式(mode)以及長或寬(value)的實際值。


 2、measure過程詳解

 
   2.1  measure過程深入分析


       之前的一篇博文<< Android中View繪製流程以及invalidate()等相關方法分析>>,我們從”二B程序員”的角度簡單    解了measure過程的調用過程。過了這麼多,我們也該升級了,- - 。現在請開始從”普通程序員”角度去理解這個

 過程。我們重點查看measure過程中地相關方法。

     我們說過,當UI框架開始繪製時,皆是從ViewRoot.java類開始繪製的。


      ViewRoot類簡要說明: 任何顯示在設備中的窗口,例如:Activity、Dialog等,都包含一個ViewRoot實例,該

  類主要用來與遠端 WindowManagerService交互以及控制(開始/銷燬)繪製。


     Step 1、 開始UI繪製 , 具體繪製方法則是:

  1. 路徑:\frameworks\base\core\java\android\view\ViewRoot.java  
  2. public final class ViewRoot extends Handler implements ViewParent,View.AttachInfo.Callbacks {  
  3.     ...  
  4.     //mView對象指添加至窗口的root View ,對Activity窗口而言,則是DecorView對象。  
  5.     View mView;      
  6.       
  7.     //開始View繪製流程  
  8.     private void performTraversals(){  
  9.         ...  
  10.         //這兩個值我們在後面討論時,在回過頭來看看是怎麼賦值的。現在只需要記住其值MeasureSpec.makeMeasureSpec()構建的。  
  11.         int childWidthMeasureSpec; //其值由MeasureSpec類構建 , makeMeasureSpec  
  12.         int childHeightMeasureSpec;//其值由MeasureSpec類構建 , makeMeasureSpec  
  13.           
  14.   
  15.         // Ask host how big it wants to be  
  16.         host.measure(childWidthMeasureSpec, childHeightMeasureSpec);  
  17.         ...  
  18.     }  
  19.     ...  
  20. }  

   
      這兒,我並沒有說出childWidthMeasureSpec和childHeightMeasureSpec類的來由(爲了避免額外地開銷,等到
 第三部分時我們在來攻克它,現在只需要記住其值MeasureSpec.makeMeasureSpec()構建的。

    Step 2 、調用measure()方法去做一些前期準備

       measure()方法原型定義在View.java類中,final修飾符修飾,其不能被重載:

    

  1. public class View implements ... {  
  2.     ...  
  3.     /** 
  4.      * This is called to find out how big a view should be. The parent 
  5.      * supplies constraint information in the width and height parameters. 
  6.      * 
  7.      * @param widthMeasureSpec Horizontal space requirements as imposed by the 
  8.      *        parent 
  9.      * @param heightMeasureSpec Vertical space requirements as imposed by the 
  10.      *        parent 
  11.      * @see #onMeasure(int, int) 
  12.      */  
  13.     public final void measure(int widthMeasureSpec, int heightMeasureSpec) {  
  14.         //判斷是否爲強制佈局,即帶有“FORCE_LAYOUT”標記 以及 widthMeasureSpec或heightMeasureSpec發生了改變  
  15.         if ((mPrivateFlags & FORCE_LAYOUT) == FORCE_LAYOUT ||  
  16.                 widthMeasureSpec != mOldWidthMeasureSpec ||  
  17.                 heightMeasureSpec != mOldHeightMeasureSpec) {  
  18.   
  19.             // first clears the measured dimension flag  
  20.             //清除MEASURED_DIMENSION_SET標記   ,該標記會在onMeasure()方法後被設置  
  21.             mPrivateFlags &= ~MEASURED_DIMENSION_SET;   
  22.   
  23.             // measure ourselves, this should set the measured dimension flag back  
  24.             // 1、 測量該View本身的大小 ; 2 、 設置MEASURED_DIMENSION_SET標記,否則接寫來會報異常。  
  25.             onMeasure(widthMeasureSpec, heightMeasureSpec);  
  26.   
  27.             // flag not set, setMeasuredDimension() was not invoked, we raise  
  28.             // an exception to warn the developer  
  29.             if ((mPrivateFlags & MEASURED_DIMENSION_SET) != MEASURED_DIMENSION_SET) {  
  30.                 throw new IllegalStateException("onMeasure() did not set the"  
  31.                         + " measured dimension by calling" + " setMeasuredDimension()");  
  32.             }  
  33.   
  34.             mPrivateFlags |= LAYOUT_REQUIRED;  //下一步是layout了,添加LAYOUT_REQUIRED標記  
  35.         }  
  36.   
  37.         mOldWidthMeasureSpec = widthMeasureSpec;   //保存值  
  38.         mOldHeightMeasureSpec = heightMeasureSpec; //保存值  
  39.     }  
  40.     ...  
  41. }  


      參數widthMeasureSpec和heightMeasureSpec 由父View構建,表示父View給子View的測量要求。其值地構建

 會在下面步驟中詳解。  

   measure()方法顯示判斷是否需要重新調用設置改View大小,即調用onMeasure()方法,然後操作兩個標識符:

            ①、重置MEASURED_DIMENSION_SET   : onMeasure()方法中,需要添加該標識符,否則,會報異常;    

       ②、添加LAYOUT_REQUIRED: 表示需要進行layout操作。

    最後,保存當前的widthMeasureSpec和heightMeasureSpec值。


   Step 3 、調用onMeasure()方法去真正設置View的長寬值,其默認實現爲:

  1. /** 
  2.    * Measure the view and its content to determine the measured width and the 
  3.    * measured height. This method is invoked by {@link #measure(int, int)} and 
  4.    * should be overriden by subclasses to provide accurate and efficient 
  5.    * measurement of their contents. 
  6.    *  
  7.    * @param widthMeasureSpec horizontal space requirements as imposed by the parent. 
  8.    *                         The requirements are encoded with 
  9.    * @param heightMeasureSpec vertical space requirements as imposed by the parent. 
  10.    *                         The requirements are encoded with 
  11.    */  
  12.   //設置該View本身地大小  
  13.   protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
  14.       setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),  
  15.               getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));  
  16.   }  
  17.     
  18.   /** 
  19.    * Utility to return a default size. Uses the supplied size if the 
  20.    * MeasureSpec imposed no contraints. Will get larger if allowed 
  21.    * by the MeasureSpec. 
  22.    * 
  23.    * @param size Default size for this view 
  24.    * @param measureSpec Constraints imposed by the parent 
  25.    * @return The size this view should be. 
  26.    */  
  27.   //@param size參數一般表示設置了android:minHeight屬性或者該View背景圖片的大小值  
  28.   public static int getDefaultSize(int size, int measureSpec) {  
  29.       int result = size;    
  30.       int specMode = MeasureSpec.getMode(measureSpec);  
  31.       int specSize =  MeasureSpec.getSize(measureSpec);  
  32.   
  33.       //根據不同的mode值,取得寬和高的實際值。  
  34.       switch (specMode) {  
  35.       case MeasureSpec.UNSPECIFIED:  //表示該View的大小父視圖未定,設置爲默認值  
  36.           result = size;  
  37.           break;  
  38.       case MeasureSpec.AT_MOST:      //表示該View的大小由父視圖指定了  
  39.       case MeasureSpec.EXACTLY:  
  40.           result = specSize;  
  41.           break;  
  42.       }  
  43.       return result;  
  44.   }  
  45.   //獲得設置了android:minHeight屬性或者該View背景圖片的大小值, 最爲該View的參考值  
  46.   protected int getSuggestedMinimumWidth() {  
  47.       int suggestedMinWidth = mMinWidth;  //  android:minHeight  
  48.   
  49.       if (mBGDrawable != null) { // 背景圖片對應地Width。  
  50.           final int bgMinWidth = mBGDrawable.getMinimumWidth();  
  51.           if (suggestedMinWidth < bgMinWidth) {  
  52.               suggestedMinWidth = bgMinWidth;  
  53.           }  
  54.       }  
  55.   
  56.       return suggestedMinWidth;  
  57.   }  
  58.   //設置View在measure過程中寬和高  
  59.   protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {  
  60.       mMeasuredWidth = measuredWidth;  
  61.       mMeasuredHeight = measuredHeight;  
  62.   
  63.       mPrivateFlags |= MEASURED_DIMENSION_SET;  //設置了MEASURED_DIMENSION_SET標記  
  64.   }  

       主要功能就是根據該View屬性(android:minWidth和背景圖片大小)和父View對該子View的"測量要求",設置該      View的 mMeasuredWidth 和 mMeasuredHeight 值。


       這兒只是一般的View類型地實現方法。一般來說,父View,也就是ViewGroup類型,都需要在重寫onMeasure()   方法,遍歷所有子View,設置每個子View的大小。基本思想如下:遍歷所有子View,設置每個子View的大小。僞

  代碼錶示爲:

  1. //某個ViewGroup類型的視圖  
  2. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
  3.   //必須調用super.ononMeasure()或者直接調用setMeasuredDimension()方法設置該View大小,否則會報異常。  
  4.   super.onMeasure(widthMeasureSpec , heightMeasureSpec)  
  5.      //setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),  
  6.      //        getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));  
  7.        
  8.   //遍歷每個子View  
  9.   for(int i = 0 ; i < getChildCount() ; i++){  
  10.     View child = getChildAt(i);  
  11.     //調用子View的onMeasure,設置他們的大小。childWidthMeasureSpec , childHeightMeasureSpec ?  
  12.     child.onMeasure(childWidthMeasureSpec, childHeightMeasureSpec);  
  13.   }  
  14. }  


      Step 2、Step 3 代碼也比較好理解,但問題是我們示例代碼中widthMeasureSpec、heightMeasureSpec是如何

 確定的呢?父View是如何設定其值的?

  

      要想回答這個問題,我們看是去源代碼裏找找答案吧。在ViewGroup.java類中,爲我們提供了三個方法,去設置

個子View的大小,基本思想也如同我們之前描述的思想:遍歷所有子View,設置每個子View的大小。

     主要有如下方法:

  1. /** 
  2.  * Ask all of the children of this view to measure themselves, taking into 
  3.  * account both the MeasureSpec requirements for this view and its padding. 
  4.  * We skip children that are in the GONE state The heavy lifting is done in 
  5.  * getChildMeasureSpec. 
  6.  */  
  7. //widthMeasureSpec 和  heightMeasureSpec 表示該父View的佈局要求  
  8. //遍歷每個子View,然後調用measureChild()方法去實現每個子View大小  
  9. protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {  
  10.     final int size = mChildrenCount;  
  11.     final View[] children = mChildren;  
  12.     for (int i = 0; i < size; ++i) {  
  13.         final View child = children[i];  
  14.         if ((child.mViewFlags & VISIBILITY_MASK) != GONE) { // 不處於 “GONE” 狀態  
  15.             measureChild(child, widthMeasureSpec, heightMeasureSpec);  
  16.         }  
  17.     }  
  18. }  
  19.      
  20. /** 
  21.  * Ask one of the children of this view to measure itself, taking into 
  22.  * account both the MeasureSpec requirements for this view and its padding. 
  23.  * The heavy lifting is done in getChildMeasureSpec. 
  24.  * 
  25.  * @param child The child to measure 
  26.  * @param parentWidthMeasureSpec The width requirements for this view 
  27.  * @param parentHeightMeasureSpec The height requirements for this view 
  28.  */  
  29. //測量每個子View高寬時,清楚了該View本身的邊距大小,即android:padding屬性 或android:paddingLeft等屬性標記  
  30. protected void measureChild(View child, int parentWidthMeasureSpec,  
  31.         int parentHeightMeasureSpec) {  
  32.     final LayoutParams lp = child.getLayoutParams(); // LayoutParams屬性  
  33.     //設置子View的childWidthMeasureSpec屬性,去除了該父View的邊距值  mPaddingLeft + mPaddingRight  
  34.     final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,  
  35.             mPaddingLeft + mPaddingRight, lp.width);  
  36.     //設置子View的childHeightMeasureSpec屬性,去除了該父View的邊距值  mPaddingTop + mPaddingBottom  
  37.     final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,  
  38.             mPaddingTop + mPaddingBottom, lp.height);  
  39.   
  40.     child.measure(childWidthMeasureSpec, childHeightMeasureSpec);  
  41. }  
  

     measureChildren()方法:遍歷所有子View,調用measureChild()方法去設置該子View的屬性值。

     measureChild()  方法   : 獲取特定子View的widthMeasureSpec、heightMeasureSpec,調用measure()方法

 設置子View的實際寬高值。

    getChildMeasureSpec()就是獲取子View的widthMeasureSpec、heightMeasureSpec值。

  

  1. /** 
  2.  * Does the hard part of measureChildren: figuring out the MeasureSpec to 
  3.  * pass to a particular child. This method figures out the right MeasureSpec 
  4.  * for one dimension (height or width) of one child view. 
  5.  * 
  6.  * The goal is to combine information from our MeasureSpec with the 
  7.  * LayoutParams of the child to get the best possible results. 
  8.  */  
  9. // spec參數                                    表示該父View本身所佔的widthMeasureSpec 或  heightMeasureSpec值  
  10. // padding參數                          表示該父View的邊距大小,見於android:padding屬性 或android:paddingLeft等屬性標記  
  11. // childDimension參數  表示該子View內部LayoutParams屬性的值,可以是wrap_content、match_parent、一個精確指(an exactly size),  
  12. //           例如:由android:width指定等。  
  13. public static int getChildMeasureSpec(int spec, int padding, int childDimension) {  
  14.     int specMode = MeasureSpec.getMode(spec);  //獲得父View的mode  
  15.     int specSize = MeasureSpec.getSize(spec);  //獲得父View的實際值  
  16.   
  17.     int size = Math.max(0, specSize - padding); //父View爲子View設定的大小,減去邊距值,  
  18.   
  19.     int resultSize = 0;    //子View對應地 size 實際值 ,由下面的邏輯條件賦值  
  20.     int resultMode = 0;    //子View對應地 mode 值 , 由下面的邏輯條件賦值  
  21.   
  22.     switch (specMode) {  
  23.     // Parent has imposed an exact size on us  
  24.     //1、父View是EXACTLY的 !  
  25.     case MeasureSpec.EXACTLY:   
  26.         //1.1、子View的width或height是個精確值 (an exactly size)  
  27.         if (childDimension >= 0) {            
  28.             resultSize = childDimension;         //size爲精確值  
  29.             resultMode = MeasureSpec.EXACTLY;    //mode爲 EXACTLY 。  
  30.         }   
  31.         //1.2、子View的width或height爲 MATCH_PARENT/FILL_PARENT   
  32.         else if (childDimension == LayoutParams.MATCH_PARENT) {  
  33.             // Child wants to be our size. So be it.  
  34.             resultSize = size;                   //size爲父視圖大小  
  35.             resultMode = MeasureSpec.EXACTLY;    //mode爲 EXACTLY 。  
  36.         }   
  37.         //1.3、子View的width或height爲 WRAP_CONTENT  
  38.         else if (childDimension == LayoutParams.WRAP_CONTENT) {  
  39.             // Child wants to determine its own size. It can't be  
  40.             // bigger than us.  
  41.             resultSize = size;                   //size爲父視圖大小  
  42.             resultMode = MeasureSpec.AT_MOST;    //mode爲AT_MOST 。  
  43.         }  
  44.         break;  
  45.   
  46.     // Parent has imposed a maximum size on us  
  47.     //2、父View是AT_MOST的 !      
  48.     case MeasureSpec.AT_MOST:  
  49.         //2.1、子View的width或height是個精確值 (an exactly size)  
  50.         if (childDimension >= 0) {  
  51.             // Child wants a specific size... so be it  
  52.             resultSize = childDimension;        //size爲精確值  
  53.             resultMode = MeasureSpec.EXACTLY;   //mode爲 EXACTLY 。  
  54.         }  
  55.         //2.2、子View的width或height爲 MATCH_PARENT/FILL_PARENT  
  56.         else if (childDimension == LayoutParams.MATCH_PARENT) {  
  57.             // Child wants to be our size, but our size is not fixed.  
  58.             // Constrain child to not be bigger than us.  
  59.             resultSize = size;                  //size爲父視圖大小  
  60.             resultMode = MeasureSpec.AT_MOST;   //mode爲AT_MOST  
  61.         }  
  62.         //2.3、子View的width或height爲 WRAP_CONTENT  
  63.         else if (childDimension == LayoutParams.WRAP_CONTENT) {  
  64.             // Child wants to determine its own size. It can't be  
  65.             // bigger than us.  
  66.             resultSize = size;                  //size爲父視圖大小  
  67.             resultMode = MeasureSpec.AT_MOST;   //mode爲AT_MOST  
  68.         }  
  69.         break;  
  70.   
  71.     // Parent asked to see how big we want to be  
  72.     //3、父View是UNSPECIFIED的 !  
  73.     case MeasureSpec.UNSPECIFIED:  
  74.         //3.1、子View的width或height是個精確值 (an exactly size)  
  75.         if (childDimension >= 0) {  
  76.             // Child wants a specific size... let him have it  
  77.             resultSize = childDimension;        //size爲精確值  
  78.             resultMode = MeasureSpec.EXACTLY;   //mode爲 EXACTLY  
  79.         }  
  80.         //3.2、子View的width或height爲 MATCH_PARENT/FILL_PARENT  
  81.         else if (childDimension == LayoutParams.MATCH_PARENT) {  
  82.             // Child wants to be our size... find out how big it should  
  83.             // be  
  84.             resultSize = 0;                        //size爲0! ,其值未定  
  85.             resultMode = MeasureSpec.UNSPECIFIED;  //mode爲 UNSPECIFIED  
  86.         }   
  87.         //3.3、子View的width或height爲 WRAP_CONTENT  
  88.         else if (childDimension == LayoutParams.WRAP_CONTENT) {  
  89.             // Child wants to determine its own size.... find out how  
  90.             // big it should be  
  91.             resultSize = 0;                        //size爲0! ,其值未定  
  92.             resultMode = MeasureSpec.UNSPECIFIED;  //mode爲 UNSPECIFIED  
  93.         }  
  94.         break;  
  95.     }  
  96.     //根據上面邏輯條件獲取的mode和size構建MeasureSpec對象。  
  97.     return MeasureSpec.makeMeasureSpec(resultSize, resultMode);  
  98. }  

       爲了便於分析,我將上面的邏輯判斷語句使用列表項進行了說明.


  getChildMeasureSpec()方法的主要功能如下:


        
根據父View的measureSpec值(widthMeasureSpec,heightMeasureSpec)值以及子View的子View內部

  LayoutParams屬性值,共同決定子View的measureSpec值的大小。主要判斷條件主要爲MeasureSpec的mode

 類型以及LayoutParams的寬高實際值(lp.width,lp.height),見於以上所貼代碼中的列表項: 1、 1.1 ; 1.2 ; 1.3 ; 

  2、2.1等。


        例如,分析列表3:假設當父View爲MeasureSpec.UNSPECIFIED類型,即未定義時,只有當子View的width

 或height指定時,其mode才爲MeasureSpec.EXACTLY,否者該View size爲 0 ,mode爲MeasureSpec.UNSPECIFIED

 ,即處於未指定狀態。

      由此可以得出, 每個View大小的設定都事由其父View以及該View共同決定的。但這只是一個期望的大小,每個

 View在測量時最終大小的設定是由setMeasuredDimension()最終決定的。因此,最終確定一個View的“測量長寬“是

 由以下幾個方面影響:

        1、父View的MeasureSpec屬性;

        2、子View的LayoutParams屬性 ;

        3、setMeasuredDimension()或者其它類似設定 mMeasuredWidth 和 mMeasuredHeight 值的方法。

                setMeasuredDimension()原型:

  1. //設置View在measure過程中寬和高  
  2. protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {  
  3.     mMeasuredWidth = measuredWidth;  
  4.     mMeasuredHeight = measuredHeight;  
  5.   
  6.     mPrivateFlags |= MEASURED_DIMENSION_SET;  //設置了MEASURED_DIMENSION_SET標記  
  7. }  


  將上面列表項轉換爲表格爲:

                              

   這張表格更能幫助我們分析View的MeasureSpec的確定條件關係。


   爲了幫助大家理解,下面我們分析某個窗口使用地xml佈局文件,我們弄清楚該xml佈局文件中每個View的

MeasureSpec值的組成。

    

  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  3.     android:id="@+id/llayout"  
  4.        android:orientation="vertical"   
  5.     android:layout_width="match_parent"  
  6.        android:layout_height="match_parent">  
  7.       
  8.       
  9.     <TextView android:id="@+id/tv"   
  10.         android:layout_width="match_parent"  
  11.         android:layout_height="wrap_content"  
  12.         android:text="@string/hello" />  
  13.   
  14. </LinearLayout>     

     該佈局文件共有兩個View:  ①、id爲llayout的LinearLayout佈局控件 ;

                                                   ②、id爲tv的TextView控件。


      假設LinearLayout的父View對應地widthSpec和heightSpec值皆爲MeasureSpec.EXACTLY類型(Activity窗口

  的父View爲DecorView,具體原因見第三部分說明)。


       對LinearLayout而言比較簡單,由於android:layout_width="match_parent",因此其width對應地widthSpec 

  mode值爲MeasureSpec.EXACTLY , size由父視圖大小指定 ;  由於android:layout_height = "match_parent",

  因此其height對應地heightSpec modeMeasureSpec.EXACTLY,size由父視圖大小指定 ;


       對TextView而言 ,其父View爲LinearLayout的widthSpec和heightSpec值皆爲MeasureSpec.EXACTLY類型,

 由於android:layout_width="match_parent" , 因此其width對應地widthSpec mode值爲MeasureSpec.EXACTLY

 size由父視圖大小指定 ;  由於android:layout_width="wrap_content" , 因此其height對應地widthSpec mode值爲

 MeasureSpec.AT_MOST,size由父視圖大小指定 。


    我們繼續窺測下LinearLayout類是如何進行measure過程的:

  1.  public class LinearLayout extends ViewGroup {  
  2. ...  
  3. @Override  //onMeasure方法。  
  4. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
  5.     //判斷是垂直方向還是水平方向,這兒我們假設是VERTICAL垂直方向,  
  6.     if (mOrientation == VERTICAL) {  
  7.         measureVertical(widthMeasureSpec, heightMeasureSpec);  
  8.     } else {  
  9.         measureHorizontal(widthMeasureSpec, heightMeasureSpec);  
  10.     }  
  11. }  
  12. //垂直方向佈局  
  13.    void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {  
  14.        mTotalLength = 0;         //該LinearLayout測量子View時的總高度。  
  15.     float totalWeight = 0;    //所有子View的權重和 , android:layout_weight  
  16.     int maxWidth = 0;         //保存子View中最大width值  
  17.        ...  
  18.        final int count = getVirtualChildCount();  //子View的個數  
  19.          
  20.        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);  
  21.        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);  
  22.           ...  
  23.        // See how tall everyone is. Also remember max width.  
  24.        for (int i = 0; i < count; ++i) {  
  25.            final View child = getVirtualChildAt(i);  
  26.               ...  
  27.            LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();  
  28.   
  29.            totalWeight += lp.weight;    
  30.            //滿足該條件地View會在該LinearLayout有剩餘高度時,才真正調用measure()  
  31.            if (heightMode == MeasureSpec.EXACTLY && lp.height == 0 && lp.weight > 0) {  
  32.                ...  
  33.            } else {  
  34.                int oldHeight = Integer.MIN_VALUE;  
  35.                //如果View的hight值爲0,並且設置了android:layout_weight屬性,重新糾正其height值爲WRAP_CONTENT  
  36.                if (lp.height == 0 && lp.weight > 0) {  
  37.                    oldHeight = 0;  
  38.                    lp.height = LayoutParams.WRAP_CONTENT;  
  39.                }  
  40.                // Determine how big this child would like to be. If this or  
  41.                // previous children have given a weight, then we allow it to  
  42.                // use all available space (and we will shrink things later  
  43.                // if needed).  
  44.                //對每個子View調用measure()方法  
  45.                measureChildBeforeLayout(  
  46.                       child, i, widthMeasureSpec, 0, heightMeasureSpec,  
  47.                       totalWeight == 0 ? mTotalLength : 0);  
  48.                  
  49.                //這三行代碼做了如下兩件事情:  
  50.                //1、獲得該View的measuredHeight值,每個View都會根據他們地屬性正確設置值  > 0 ;  
  51.                //2、更新mTotalLength值:取當前高度mTotalLength值與mTotalLength + childHeight 的最大值  
  52.                // 於是對於android:layout_height="wrap_height"屬性地LinearLayout控件也就知道了它的確切高度值了。  
  53.                final int childHeight = child.getMeasuredHeight();  
  54.                final int totalLength = mTotalLength;  
  55.                mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +  
  56.                       lp.bottomMargin + getNextLocationOffset(child));  
  57.                ...  
  58.            }  
  59.            final int margin = lp.leftMargin + lp.rightMargin;  
  60.            final int measuredWidth = child.getMeasuredWidth() + margin;  
  61.            maxWidth = Math.max(maxWidth, measuredWidth);  
  62.            ...  
  63.        }  
  64.           //後續還有很多處理,包括繼續measure()某些符合條件地子View  
  65.        ...  
  66.    }  
  67.    void measureChildBeforeLayout(View child, int childIndex,  
  68.            int widthMeasureSpec, int totalWidth, int heightMeasureSpec,  
  69.            int totalHeight) {  
  70.     //調用measureChildWithMargins()方法去設置子View大小  
  71.        measureChildWithMargins(child, widthMeasureSpec, totalWidth,  
  72.                heightMeasureSpec, totalHeight);  
  73.    }  
  74. ...  

          

        繼續看看measureChildWithMargins()方法,該方法定義在ViewGroup.java內,基本流程同於measureChild()方法,但添加了對子View Margin的處理,即:android:margin屬性或者android:marginLeft等屬性的處理。

      measureChildWithMargins@ViewGroup.java 

  1. /** 
  2.  * Ask one of the children of this view to measure itself, taking into 
  3.  * account both the MeasureSpec requirements for this view and its padding 
  4.  * and margins. The child must have MarginLayoutParams The heavy lifting is 
  5.  * done in getChildMeasureSpec. 
  6.  */  
  7. //基本流程同於measureChild()方法,但添加了對子View Margin的處理,即:android:margin屬性或者android:marginLeft等屬性的處理  
  8. //widthUsed參數  表示該父View已經使用的寬度  
  9. //heightUsed參數  表示該父View已經使用的高度  
  10. protected void measureChildWithMargins(View child,  
  11.         int parentWidthMeasureSpec, int widthUsed,  
  12.         int parentHeightMeasureSpec, int heightUsed) {  
  13.     final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();  
  14.   
  15.     //獲得子View的childWidthMeasureSpec和childHeightMeasureSpec值  
  16.     final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,  
  17.             mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin  
  18.                     + widthUsed, lp.width);  
  19.     final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,  
  20.             mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin  
  21.                     + heightUsed, lp.height);  
  22.   
  23.     child.measure(childWidthMeasureSpec, childHeightMeasureSpec);  
  24. }  



    measure()過程時,LinearLayout類做了如下事情 :

            1、遍歷每個子View,對其調用measure()方法;

            2、子View measure()完成後,需要取得該子View地寬高實際值,繼而做處理(例如:LinearLayout屬性爲

       android:widht="wrap_content"時,LinearLayout的實際width值則是每個子View的width值的累加值)。

     

  2.2 WRAP_CONTENT、MATCH_PARENT以及measure動機揭祕


        子View地寬高實際值 ,即child.getMeasuredWidth()值得返回最終會是一個確定值?  難道WRAP_CONTENT(

其值爲-2) 、MATCH_PARENT(值爲-1)或者說一個具體值(an exactly size > 0)。前面我們說過,View最終“測量”值的

確定是有三個部分組成地:

         ①、父View的MeasureSpec屬性;

         ②、子View的LayoutParams屬性 ;

         ③、setMeasuredDimension()或者其它類似設定 mMeasuredWidth 和 mMeasuredHeight 值的方法。

   因此,一個View必須以某種合適地方法確定它地最終大小。例如,如下自定義View:

  1. //自定義View     
  2. public Class MyView extends View {  
  3.       
  4.      //針對不同地mode值,設置本View地大小  
  5.      protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){  
  6.          //獲得父View傳遞給我們地測量需求  
  7.          int widthMode = MeasureSpec.getMode(widthMeasureSpec);  
  8.          int heightMode = MeasureSpec.getMode(heightMeasureSpec);  
  9.            
  10.          int width = 0 ;  
  11.          int height = 0 ;  
  12.          //對UNSPECIFIED 則拋出異常  
  13.          if(widthMode == MeasureSpec.UNSPECIFIED || heightMode == MeasureSpec.UNSPECIFIED)  
  14.              throw new RuntimeException("widthMode or heightMode cannot be UNSPECIFIED");  
  15.           
  16.          //精確指定  
  17.          if(widthMode == MeasureSpec.EXACTLY){  
  18.              width = 100 ;  
  19.          }  
  20.          //模糊指定  
  21.          else if(widthMode == MeasureSpec.AT_MOST )  
  22.              width = 50 ;   
  23.            
  24.           //精確指定  
  25.          if(heightMode == MeasureSpec.EXACTLY){  
  26.              height = 100 ;  
  27.          }  
  28.          //模糊指定  
  29.          else if(heightMode == MeasureSpec.AT_MOST )  
  30.              height = 50 ;  
  31.            
  32.          setMeasuredDimension(width , height) ;  
  33.      }  
  34. }  



         該自定義View重寫了onMeasure()方法,根據傳遞過來的widthMeasureSpec和heightMeasureSpec簡單設置了

 該View的mMeasuredWidth 和 mMeasuredHeight值。

      對於TextView而言,如果它地mode不是Exactly類型 , 它會根據一些屬性,例如:android:textStyle

  、android:textSizeandroid:typeface等去確定TextView類地需要佔用地長和寬。

   

     因此,如果你地自定義View必須手動對不同mode做出處理。否則,則是mode對你而言是無效的。

   

      Android框架中提供地一系列View/ViewGroup都需要去進行這個measure()過程地 ,因爲在layout()過程中,父

  View需要調用getMeasuredWidth()或getMeasuredHeight()去爲每個子View設置他們地佈局座標,只有確定佈局

  座標後,才能真正地將該View 繪製(draw)出來,否則該View的layout大小爲0,得不到期望效果。我們繼續看看

  LinearLayout的layout佈局過程:

  1. public class LinearLayout extends ViewGroup {  
  2.     ...  
  3.     @Override  //layout 過程  
  4.     protected void onLayout(boolean changed, int l, int t, int r, int b) {  
  5.         //假定是垂直方向佈局  
  6.         if (mOrientation == VERTICAL) {  
  7.             layoutVertical();  
  8.         } else {  
  9.             layoutHorizontal();  
  10.         }  
  11.     }  
  12.     //對每個子View調用layout過程  
  13.     void layoutVertical() {  
  14.         ...  
  15.         final int count = getVirtualChildCount();  
  16.         ...  
  17.         for (int i = 0; i < count; i++) {  
  18.             final View child = getVirtualChildAt(i);  
  19.             if (child == null) {  //一般爲非null  
  20.                 childTop += measureNullChild(i);  
  21.             } else if (child.getVisibility() != GONE) {  
  22.                 //獲得子View測量時的實際寬高值,  
  23.                 final int childWidth = child.getMeasuredWidth();  
  24.                 final int childHeight = child.getMeasuredHeight();  
  25.                   
  26.                 ...  
  27.                 //  封裝了child.layout()方法,見如下  
  28.                 setChildFrame(child, childLeft, childTop + getLocationOffset(child),  
  29.                         childWidth, childHeight);   
  30.                 childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);  
  31.   
  32.                 i += getChildrenSkipCount(child, i);  
  33.             }  
  34.         }  
  35.     }  
  36.     //width = getMeasuredWidth() ; height = childHeight(); View的大小就是測量大小  
  37.     private void setChildFrame(View child, int left, int top, int width, int height) {  
  38.           
  39.         child.layout(left, top, left + width, top + height);  
  40.     }  
  41.     ...  
  42. }     

      對一個View進行measure操作地主要目的就是爲了確定該View地佈局大小,見上面所示代碼。但measure操作

 通常是耗時的,因此對自定義ViewGroup而言,我們可以自由控制measure、layout過程,如果我們知道如何layout

 一個View,我們可以跳過該ViewGroup地measure操作(onMeasure()方法中measure所有子View地),直接去layout


      在前面一篇博客<<Android中滑屏初探 ---- scrollTo 以及 scrollBy方法使用說明>>中,我們自定義了一個     ViewGroup,  並且重寫了onMeasure()和onLayout()方法去分別操作每個View。就該ViewGroup而言,我們只需要

  重寫onLayout()操作即可,因爲我們知道如何layout每個子View。如下代碼所示:


  1. //自定義ViewGroup , 包含了三個LinearLayout控件,存放在不同的佈局位置  
  2. public class MultiViewGroup extends ViewGroup {  
  3.     private void init() {  
  4.         // 初始化3個 LinearLayout控件  
  5.         LinearLayout oneLL = new LinearLayout(mContext);  
  6.         oneLL.setBackgroundColor(Color.RED);  
  7.         addView(oneLL);  
  8.         ...  
  9.     }  
  10.     @Override  
  11.     // 我們知曉每個子View的layout佈局大小,因此我們不需要爲每個子View進行measure()操作了。  
  12. //  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
  13. //      setMeasuredDimension(width, height);  
  14. //      // 設置該ViewGroup的大小  
  15. //      int width = MeasureSpec.getSize(widthMeasureSpec);  
  16. //      int height = MeasureSpec.getSize(heightMeasureSpec);  
  17. //      int childCount = getChildCount();  
  18. //      for (int i = 0; i < childCount; i++) {  
  19. //          View child = getChildAt(i);  
  20. //          // 設置每個子視圖的大小 , 即全屏  
  21. //          child.measure(MultiScreenActivity.screenWidth, MultiScreenActivity.scrrenHeight);  
  22. //      }  
  23.     }  
  24.   
  25.     // layout過程  
  26.     @Override  
  27.     protected void onLayout(boolean changed, int l, int t, int r, int b) {  
  28.         // TODO Auto-generated method stub  
  29.         Log.i(TAG, "--- start onLayout --");  
  30.         int startLeft = 0// 每個子視圖的起始佈局座標  
  31.         int startTop = 10// 間距設置爲10px 相當於 android:marginTop= "10px"  
  32.         int childCount = getChildCount();  
  33.         Log.i(TAG, "--- onLayout childCount is -->" + childCount);  
  34.         for (int i = 0; i < childCount; i++) {  
  35.             View child = getChildAt(i);  
  36.             child.layout(startLeft, startTop,   
  37.                     startLeft + MultiScreenActivity.screenWidth,   
  38.                     startTop + MultiScreenActivity.scrrenHeight);  
  39.             startLeft = startLeft + MultiScreenActivity.screenWidth ; //校準每個子View的起始佈局位置  
  40.             //三個子視圖的在屏幕中的分佈如下 [0 , 320] / [320,640] / [640,960]  
  41.         }  
  42.     }  
  43. }    

     更多關於自定義ViewGroup無須重寫measure動作的,可以參考 Android API :

               <<Optimizing the View >>

     中文翻譯見於:<< Android中View繪製優化之三---- 優化View>>


 3、root View被添加至窗口時,UI框架是如何設置其LayoutParams值


         老子道德經有言:“道生一,一生二,二生三,三生萬物。”  UI繪製也就是個遞歸過程。理解其基本架構後,
 也就“掌握了一箇中心點”了。在第一節中,我們沒有說明開始UI繪製時 ,沒有說明mView.measure()參數地由來,
 參數也就是我們本節需要弄懂的“道” --- root View的widthMeasureSpec和heightMeasureSpec 是如何確定的。

   對於如下佈局文件: main.xml
  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  3.     android:orientation="vertical"  
  4.     android:layout_width="fill_parent"  
  5.     android:layout_height="fill_parent"  
  6.     >  
  7. <TextView    
  8.     android:layout_width="fill_parent"   
  9.     android:layout_height="wrap_content"   
  10.     android:text="@string/hello"  
  11.     />  
  12. </LinearLayout>  
  
    當使用LayoutInflater類解析成View時 ,LinearLayout對象的LayoutParams參數爲null 。具體原因請參考上篇博文

    任何一個View被添加至窗口時,都需要利用WindowManager類去操作。例如,如下代碼:
 
  1. //顯示一個懸浮窗吧 , just so so   
  2. public void showView()  
  3. {  
  4.     //解析佈局文件  
  5.     LayoutInflater layoutInflater = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);  
  6.     //rootView對應地LayoutParams屬性值爲null,將會在UI繪製時設定其值  
  7.     View rootView = layoutInflater.inflate(R.layout.main, null);  
  8.       
  9.     WindowManager windowManager = (WindowManager)getSystemService(Context.WINDOW_SERVICE);  
  10.     //設置WindowManager.LayoutParams參數值,作爲該窗口的各種屬性  
  11.     WindowManager.LayoutParams winparams = WindowManager.LayoutParams();  
  12.      // 以屏幕左上角爲原點,設置x、y初始值  
  13.     winparams.x = 0;  
  14.     winparams.y = 0;  
  15.   
  16.     //設置懸浮窗口長寬數據  
  17.     winparams.width = WindowManager.LayoutParams.WRAP_CONTENT;;  
  18.     winparams.height = WindowManager.LayoutParams.WRAP_CONTENT;;  
  19.        
  20.     windowManager.addView(rootView, winparams);  
  21. }  

  1.   
       關於WindowManager的使用請看如下博客 :
                               <<android學習---- WindowManager 接口 >>
                              <<在Android中使用WindowManager實現懸浮窗口>>
      關於WindowManager.LayoutParams類說明請看如下博客: 
                              << android學習---- WindowManager.LayoutParams>>
       下面,我們從獲得WindowManager對象引用開始,一步步觀察addView()做了一些什麼事情。
   Step 1 、獲得WindowManager對象服務 ,具體實現類在ContextImpl.java內中
          路徑: /frameworks/base/core/java/android/app/ContextImpl.java        
  1. @Override  
  2. public Object getSystemService(String name) {  
  3.     if (WINDOW_SERVICE.equals(name)) {  
  4.         return WindowManagerImpl.getDefault();  
  5.     }  
  6.     ...  
  7. }  
        WindowManager是個接口,具體返回對象則是WindowManagerImpl的單例對象。

 Step 2 、 獲得WindowManagerImpl的單例對象,以及部分源碼分析
          路徑: /frameworks/base/core/java/android/view/WindowManagerImpl.java 
  1. public class WindowManagerImpl implements WindowManager{  
  2.          
  3.    public static WindowManagerImpl getDefault()  
  4.    {  
  5.        return mWindowManager;  
  6.    }  
  7.    //以特定Window屬性添加一個窗口  
  8.    public void addView(View view, ViewGroup.LayoutParams params)  
  9.    {  
  10.        addView(view, params, false);  
  11.    }  
  12.    //參數nest表示該窗口是不是一個字窗口  
  13.    private void addView(View view, ViewGroup.LayoutParams params, boolean nest)  
  14.    {   ...  
  15.        final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;  
  16.          
  17.        ViewRoot root;  
  18.        View panelParentView = null;    //該子窗口對應地父窗口View  
  19.          
  20.        synchronized (this) {  
  21.             
  22.            ...//需要對傳遞過來地參數進行檢測...  
  23.              
  24.            //對每個窗口皆構建一個ViewRoot對象  
  25.            root = new ViewRoot(view.getContext());  
  26.            root.mAddNesting = 1;  
  27.            //設置root View 的LayoutParams爲wparams,即WindowManager.LayoutParams類型  
  28.            view.setLayoutParams(wparams);  
  29.            ...//對參數檢測,以及拷貝原有數組...  
  30.              
  31.            //將窗口對應地view、root、wparams保存在屬性集合中  
  32.            mViews[index] = view;  
  33.            mRoots[index] = root;  
  34.            mParams[index] = wparams;  
  35.        }  
  36.        // do this last because it fires off messages to start doing things  
  37.        // 調用ViewRoot對象去通知系統添加一個窗口  
  38.        root.setView(view, wparams, panelParentView);  
  39.    }  
  40.    ...  
  41.    //這三個數組分別保存了一個窗口對應地屬性  
  42.    private View[] mViews;         //root View對象 , View類型  
  43.    private ViewRoot[] mRoots;     //ViewRoot類型 , 與WMS通信  
  44.    private WindowManager.LayoutParams[] mParams;  //窗口屬性  
  45.      
  46.    //WindowManagerImpl實現了單例模式  
  47.    private static WindowManagerImpl mWindowManager = new WindowManagerImpl();  
  48. }  


      WindowManagerImpl類的三個數組集合保存了每個窗口相關屬性,這樣我們可以通過這些屬性去操作特定的
 窗口(例如,可以根據View去更新/銷燬該窗口)。當參數檢查成功時,構建一個ViewRoot對象,並且設置設置root
 View 的LayoutParams爲wparams,即WindowManager.LayoutParams類型。最後調用root.setView()方法去通知
 系統需要創建該窗口。我們接下來往下看看ViewRoot類相關操作。
  
    Step 3、
   
  1. public final class ViewRoot extends Handler implements ViewParent,View.AttachInfo.Callbacks {  
  2.          
  3.     View mView;   //所有窗口地root View     
  4.     final WindowManager.LayoutParams mWindowAttributes = new WindowManager.LayoutParams();    
  5.   
  6.     ...  
  7.      /** 
  8.      * We have one child 
  9.      */  
  10.     public void setView(View view, WindowManager.LayoutParams attrs,  
  11.             View panelParentView) {  
  12.         synchronized (this) {  
  13.             if (mView == null) {  
  14.                 mView = view;  
  15.                 mWindowAttributes.copyFrom(attrs); //保存WindowManager.LayoutParams屬性值  
  16.                 attrs = mWindowAttributes;  
  17.                 ...  
  18.                   
  19.                 mAdded = true;  
  20.                 int res; /* = WindowManagerImpl.ADD_OKAY; */  
  21.   
  22.                 // Schedule the first layout -before- adding to the window  
  23.                 // manager, to make sure we do the relayout before receiving  
  24.                 // any other events from the system.  
  25.                 requestLayout();   //請求UI開始繪製。  
  26.                 mInputChannel = new InputChannel();  //創建一個InputChannel對象,接受消息  
  27.                 try {  
  28.                     //通知WindowManagerService添加一個窗口  
  29.                     res = sWindowSession.add(mWindow, mWindowAttributes,  
  30.                             getHostVisibility(), mAttachInfo.mContentInsets,  
  31.                             mInputChannel);  
  32.                 }   
  33.                 ...  
  34.                 view.assignParent(this);  //將root View的父View設置爲該ViewRoot對象(實現了ViewParent接口)  
  35.                 ...  
  36.             }  
  37.         }  
  38.     }  
  39. }  
           說明:ViewRoot類繼承了Handler,實現了ViewParent接口

  setView()方法地主要功能如下:
        1、保存相關屬性值,例如:mView、mWindowAttributes等;
        2、調用requestLayout()方法請求UI繪製,由於ViewRoot是個Handler對象,異步請求;
        3、通知WindowManagerService添加一個窗口;
        4、註冊一個事件監聽管道,用來監聽:按鍵(KeyEvent)和觸摸(MotionEvent)事件。
  我們這兒重點關注 requestLayout()方法請求UI繪製地流程。

  Step 4、異步調用請求UI繪製
   
  1. /** 
  2.  * {@inheritDoc} 
  3.  */  
  4. public void requestLayout() {  
  5.     checkThread();        //檢查是不是UI線程調用,如果不是UI線程,會報異常  
  6.     mLayoutRequested = true;   //置爲真,表示需要進行measure和layout過程  
  7.     scheduleTraversals();    
  8. }  
  9. //開始UI繪製流程  
  10. public void scheduleTraversals() {  
  11.     if (!mTraversalScheduled) {  
  12.         mTraversalScheduled = true;       //防止多次調用  
  13.         sendEmptyMessage(DO_TRAVERSAL);   //異步請求UI繪製  
  14.     }  
  15. }  
  16. @Override  
  17. public void handleMessage(Message msg) {  
  18.  switch (msg.what) {  
  19.         case DO_TRAVERSAL:  
  20.              performTraversals();  //開始UI繪製  
  21.              break;  
  22.  }  
  23. }  
   
          由於performTraversals()方法比較複雜,我們側重於第一次設置root View的widhtSpecSize以及    
  heightSpecSize值。
  1. private void performTraversals() {  
  2.     // cache mView since it is used so much below...  
  3.     final View host = mView;  
  4.   
  5.     mTraversalScheduled = false;           
  6.     boolean surfaceChanged = false;  
  7.     WindowManager.LayoutParams lp = mWindowAttributes;    
  8.   
  9.     int desiredWindowWidth;              //表示該窗口期望width值  
  10.     int desiredWindowHeight;             //表示該窗口期望width值  
  11.     int childWidthMeasureSpec;           //保存root View的widthMeasureSpec  
  12.     int childHeightMeasureSpec;          //保存root View的heightMeasureSpec  
  13.   
  14.     final View.AttachInfo attachInfo = mAttachInfo;  
  15.   
  16.     final int viewVisibility = getHostVisibility();  
  17.     boolean viewVisibilityChanged = mViewVisibility != viewVisibility  
  18.             || mNewSurfaceNeeded;  
  19.   
  20.     float appScale = mAttachInfo.mApplicationScale;  
  21.   
  22.     WindowManager.LayoutParams params = null;  
  23.     if (mWindowAttributesChanged) {  
  24.         mWindowAttributesChanged = false;  
  25.         surfaceChanged = true;  
  26.         params = lp;  
  27.     }  
  28.     Rect frame = mWinFrame;  
  29.     if (mFirst) {   //mFirst表示是否是第一次繪製該Window  
  30.         fullRedrawNeeded = true;  
  31.         mLayoutRequested = true;  
  32.   
  33.         DisplayMetrics packageMetrics =  
  34.             mView.getContext().getResources().getDisplayMetrics();  
  35.         //第一次繪製時desiredWindowWidth,desiredWindowHeight 值大小爲屏幕大小  
  36.         desiredWindowWidth = packageMetrics.widthPixels;  
  37.         desiredWindowHeight = packageMetrics.heightPixels;  
  38.         ...  
  39.     } else {   //不是第一次繪製,則desiredWindowWidth值爲frame保存大小,frame值會由WMS填充  
  40.         desiredWindowWidth = frame.width();  
  41.         desiredWindowHeight = frame.height();  
  42.         ...  
  43.     }  
  44.     ...  
  45.     boolean insetsChanged = false;  
  46.   
  47.     if (mLayoutRequested) {  
  48.         ...//獲得root View的widthMeasureSpec 和 heightMeasureSpec值  
  49.         childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);  
  50.         childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);  
  51.         //開始measure過程  
  52.         host.measure(childWidthMeasureSpec, childHeightMeasureSpec);  
  53.     }  
  54.     ...  
  55.     final boolean didLayout = mLayoutRequested;  
  56.       
  57.     boolean triggerGlobalLayoutListener = didLayout  
  58.             || attachInfo.mRecomputeGlobalAttributes;  
  59.     if (didLayout) {  
  60.         ... //layout過程  
  61.        host.layout(00, host.mMeasuredWidth, host.mMeasuredHeight);  
  62.         ...  
  63.     }  
  64.     ...  
  65.     if (!cancelDraw && !newSurface) {  
  66.         mFullRedrawNeeded = false;  
  67.         draw(fullRedrawNeeded);  
  68.         ...  
  69. }  
  1. /** 
  2.   * @param windowSize  The available width or height of the window 
  3.   * 
  4.   * @param rootDimension The layout params for one dimension (width or height) of the window. 
  5.  */  
  6.  private int getRootMeasureSpec(int windowSize, int rootDimension) {  
  7.      int measureSpec;  
  8.      switch (rootDimension) {  
  9.      case ViewGroup.LayoutParams.MATCH_PARENT:  
  10.          // Window can't resize. Force root view to be windowSize.  
  11.          measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);  
  12.          break;  
  13.      case ViewGroup.LayoutParams.WRAP_CONTENT:  
  14.          // Window can resize. Set max size for root view.  
  15.          measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);  
  16.          break;  
  17.      default:  
  18.          // Window wants to be an exact size. Force root view to be that size.  
  19.          measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);  
  20.          break;  
  21.      }  
  22.      return measureSpec;  
  23.  }         


      調用root View的measure()方法時,其參數是由getRootMeasureSpec()設置的


第二步我們看measure與onmeasure

android中onMeasure

有兩個對佈局界面影響很的方法,onDraw(),和onMeasure().

onDraw()比較好理解.onMeasure()就比較難理解一些,也更復雜些 ,引用文檔中的說法就是:

onMeasure() is a little more involved.
其實還有另一個方面的原因就是我對這個單詞measure不是很知道,然後果了下詞典,就放了下心,確實是測量的意思.

實現onMeasure()方法基本需要完成下面三個方面的事情(最終結果是你自己寫相應代碼得出測量值並調用view的一個方法進行設置,告訴給你的view安排位置大小的父容器你要多大的空間.):

1.傳遞進來的參數,widthMeasureSpec,和heightMeasureSpec是你對你應該得出來的測量值的限制.

 

The overidden onMeasure() method is called with width and height measure specifications(widthMeasureSpec and heightMeasureSpec parameters,both are integer codes representing dimensions) which should be treated as requirements for the restrictions on the width and height measurements you should produce.

2. 你在onMeasure計算出來設置的width和height將被用來渲染組件.應當儘量在傳遞進來的width和height 聲明之間.

雖然你也可以選擇你設置的尺寸超過傳遞進來的聲明.但是這樣的話,父容器可以選擇,如clipping,scrolling,或者拋出異常,或者(也許是用新的聲明參數)再次調用onMeasure()

Your component's onMeasure() method should calculate a measurement width and height which will be required to render the component.it should try to stay within the specified passed in.although it can choose to exceed them(in this case,the parent can choose what to do,including clipping,scrolling,throwing an excption,or asking the onMeasure to try again,perhaps with different measurement specifications).

3.一但width和height計算好了,就應該調用View.setMeasuredDimension(int width,int height)方法,否則將導致拋出異常.
Once the width and height are calculated,the setMeasureDimension(int width,int height) method must be called with the calculated measurements.Failure to do this will result in an exceptiion being thrown
   

在Android提提供的一個自定義View示例中(在API demos 中的 view/LabelView)可以看到一個重寫onMeasure()方法的

實例,也比較好理解.

01 /**
02  * @see android.view.View#measure(int, int)
03  */
04 @Override
05 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
06     setMeasuredDimension(measureWidth(widthMeasureSpec),
07             measureHeight(heightMeasureSpec));
08 }
09  
10 /**
11  * Determines the width of this view
12  * @param measureSpec A measureSpec packed into an int
13  * @return The width of the view, honoring constraints from measureSpec
14  */
15 private int measureWidth(int measureSpec) {
16     int result = 0;
17     int specMode = MeasureSpec.getMode(measureSpec);
18     int specSize = MeasureSpec.getSize(measureSpec);
19  
20     if (specMode == MeasureSpec.EXACTLY) {
21         // We were told how big to be
22         result = specSize;
23     else {
24         // Measure the text
25         result = (int) mTextPaint.measureText(mText) + getPaddingLeft()
26                 + getPaddingRight();
27         if (specMode == MeasureSpec.AT_MOST) {
28             // Respect AT_MOST value if that was what is called for by measureSpec
29             result = Math.min(result, specSize);
30         }
31     }
32  
33     return result;
34 }

 

直接看measureWidth()

首先看到的是參數,分別代表寬度和高度的MeasureSpec

android2.2文檔中對於MeasureSpec中的說明是:

一個MeasureSpec封裝了從父容器傳遞給子容器的佈局需求.

每一個MeasureSpec代表了一個寬度,或者高度的說明.

一個MeasureSpec是一個大小跟模式的組合值.一共有三種模式.

A MeasureSpec encapsulates the layout requirements passed from parent to child Each MeasureSpec represents a requirement for either the width or the height.A MeasureSpec is compsized of a size and a mode.There are three possible modes:

 (1)UPSPECIFIED :父容器對於子容器沒有任何限制,子容器想要多大就多大.
UNSPECIFIED The parent has not imposed any constraint on the child.It can be whatever size it wants

 (2) EXACTLY

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

EXACTLY The parent has determined and exact size for the child.The child is going to be given those bounds regardless of how big it wants to be.

(3) AT_MOST

 子容器可以是聲明大小內的任意大小.

AT_MOST The child can be as large as it wants up to the specified size

MeasureSpec是View類下的靜態公開類,MeasureSpec中的值作爲一個整型是爲了減少對象的分配開支.此類用於

將size和mode打包或者解包爲一個整型.

MeasureSpecs are implemented as ints to reduce object allocation.This class is provided to pack and unpack the size,mode tuple into the int

我比較好奇的是怎麼樣將兩個值打包到一個int中,又如何解包.

MeasureSpec類代碼如下 :(註釋已經被我刪除了,因爲在上面說明了.)

01 public static class MeasureSpec {
02     private static final int MODE_SHIFT = 30;
03     private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
04  
05     public static final int UNSPECIFIED = 0 << MODE_SHIFT;
06     public static final int EXACTLY     = 1 << MODE_SHIFT;
07     public static final int AT_MOST     = 2 << MODE_SHIFT;
08  
09     public static int makeMeasureSpec(int size, int mode) {
10         return size + mode;
11     }
12     public static int getMode(int measureSpec) {
13         return (measureSpec & MODE_MASK);
14     }
15     public static int getSize(int measureSpec) {
16         return (measureSpec & ~MODE_MASK);
17     }  }

我無聊的將他們的十進制值打印出來了:

mode_shift=30,mode_mask=-1073741824,UNSPECIFIED=0,EXACTLY=1073741824,AT_MOST=-2147483648

然後覺得也應該將他們的二進制值打印出來,如下:

mode_shift=11110, // 30

mode_mask=11000000000000000000000000000000,

UNSPECIFIED=0, 

EXACTLY=1000000000000000000000000000000, 

AT_MOST=10000000000000000000000000000000

 

1 MODE_MASK  = 0x3 << MODE_SHIFT //也就是說MODE_MASK是由11左移30位得到的.因爲Java用補碼錶示數值.最後得到的值最高位是1所以就是負數了
1  
對於上面的數值我們應該這樣想,不要把0x3看成3而要看成二進制的11,

而把MODE_SHIFF就看成30.那爲什麼是二進制 的11呢?

呢,因爲只有三各模式,如果有四種模式就是111了因爲111三個位纔可以有四種組合對吧.

我們這樣來看,

UNSPECIFIED=00000000000000000000000000000000, 

      EXACTLY=01000000000000000000000000000000, 

    AT_MOST=10000000000000000000000000000000

也就是說,0,1,2

對應   00,01,10

當跟11想與時  00 &11 還是得到 00,11&01 -> 01,10&

我覺得到了這個份上相信,看我博客的也都理解了.

 return (measureSpec & ~MODE_MASK);應該是 return (measureSpec & (~MODE_MASK));


第二步這裏我補充幾點


measure()方法是final類型的,onmeasure()方法可以重寫

measure方法裏面會調用onmeasure方法

onmeasure方法裏面確定寬高

如果是ViewGroup則再次調用measure方法,然後onmeasure確定寬高


layout()方法也如此即layout是final的,onlayout是可以重寫的

繼承自GroupView的時候得重寫onLayout方法,調用layout(l,t,r,b)佈局子View視圖座標

第三步我們看draw方法--------->>總結如下幾點:

     draw()方法與上面不同的是他可以重寫

    View的draw()方法調用了ondraw(),dispatchDraw()方法

第四步我們看dispatchDraw()方法

         * 繪製VIew本身的內容,通過調用View.onDraw(canvas)函數實現,繪製自己的child通過dispatchDraw(canvas)實現
         * 繪製自己的孩子通過dispatchDraw(canvas)實現 當它沒有背景時直接調用的是dispatchDraw()方法,
         * 而繞過了draw()方法,當它有背景的時候就調用draw()方法,而draw()方法裏包含了dispatchDraw()方法的調用。
         * 因此要在ViewGroup上繪製東西的時候往往重寫的是dispatchDraw()方法而不是onDraw()方法
         * 注意此處有三個方法一個draw 一個ondraw() 一個dispatchdraw
        

接下來我們進入第二個階段 研究onInterceptTouchEvent

onInterceptTouchEvent和onTouchEvent調用時序

onInterceptTouchEvent()是ViewGroup的一個方法,目的是在系統向該ViewGroup及其各個childView觸發onTouchEvent()之前對相關事件進行一次攔截,Android這麼設計的想法也很好理解,由於ViewGroup會包含若干childView,因此需要能夠統一監控各種touch事件的機會,因此純粹的不能包含子view的控件是沒有這個方法的,如LinearLayout就有,TextView就沒有。 

onInterceptTouchEvent()使用也很簡單,如果在ViewGroup裏覆寫了該方法,那麼就可以對各種touch事件加以攔截。但是如何攔截,是否所有的touch事件都需要攔截則是比較複雜的,touch事件在onInterceptTouchEvent()和onTouchEvent以及各個childView間的傳遞機制完全取決於onInterceptTouchEvent()和onTouchEvent()的返回值。並且,針對down事件處理的返回值直接影響到後續move和up事件的接收和傳遞。 

關於返回值的問題,基本規則很清楚,如果return true,那麼表示該方法消費了此次事件,如果return false,那麼表示該方法並未處理完全,該事件仍然需要以某種方式傳遞下去繼續等待處理。

SDK給出的說明如下:

·  You will receive the down event here.

·  The down event will be handled either by a child of this view group, or given to your own onTouchEvent() method to handle; this means you should implement onTouchEvent() to return true, so you will continue to see the rest of the gesture (instead of looking for a parent view to handle it). Also, by returning true from onTouchEvent(), you will not receive any following events in onInterceptTouchEvent() and all touch processing must happen in onTouchEvent() like normal.

·  For as long as you return false from this function, each following event (up to and including the final up) will be delivered first here and then to the target's onTouchEvent().

·  If you return true from here, you will not receive any following events: the target view will receive the same event but with the action ACTION_CANCEL, and all further events will be delivered to your onTouchEvent() method and no longer appear here.

 

由於onInterceptTouchEvent()的機制比較複雜,上面的說明寫的也比較複雜,總結一下,基本的規則是:

1.       down事件首先會傳遞到onInterceptTouchEvent()方法

2.       如果該ViewGroup的onInterceptTouchEvent()在接收到down事件處理完成之後return false,那麼後續的move, up等事件將繼續會先傳遞給該ViewGroup,之後才和down事件一樣傳遞給最終的目標view的onTouchEvent()處理。

3.       如果該ViewGroup的onInterceptTouchEvent()在接收到down事件處理完成之後return true,那麼後續的move, up等事件將不再傳遞給onInterceptTouchEvent(),而是和down事件一樣傳遞給該ViewGroup的onTouchEvent()處理,注意,目標view將接收不到任何事件。

4.       如果最終需要處理事件的view的onTouchEvent()返回了false,那麼該事件將被傳遞至其上一層次的view的onTouchEvent()處理。

5.       如果最終需要處理事件的view 的onTouchEvent()返回了true,那麼後續事件將可以繼續傳遞給該view的onTouchEvent()處理。

 

下面用一個簡單的實驗說明上述複雜的規則。視圖自底向上共3層,其中LayoutView1和LayoutView2就是LinearLayout, MyTextView就是TextView:

對應的xml佈局文件如下:

<?xml version="1.0" encoding="utf-8"?>

<com.touchstudy.LayoutView1 xmlns:android="http://schemas.android.com/apk/res/android"

    android:orientation="vertical"

    android:layout_width="fill_parent"

    android:layout_height="fill_parent" >

    <com.touchstudy.LayoutView2

        android:orientation="vertical"

        android:layout_width="fill_parent"

        android:layout_height="fill_parent"

        android:gravity="center">

       <com.touchstudy.MyTextView 

            android:layout_width="wrap_content"

            android:layout_height="wrap_content"

            android:id="@+id/tv"

            android:text="AB"

            android:textSize="40sp"

            android:textStyle="bold"

            android:background="#FFFFFF"

            android:textColor="#0000FF"/>

   </com.touchstudy.LayoutView2>

</com.touchstudy.LayoutView1>

 

下面看具體情況:

1.       onInterceptTouchEvent()處理down事件均返回falseonTouchEvent()處理事件均返回true

------------------------------------------------------------------------------------------------------------------------------

04-11 03:58:42.620: DEBUG/LayoutView1(614): onInterceptTouchEvent action:ACTION_DOWN

04-11 03:58:42.620: DEBUG/LayoutView2(614): onInterceptTouchEvent action:ACTION_DOWN

04-11 03:58:42.620: DEBUG/MyTextView(614): onTouchEvent action:ACTION_DOWN

04-11 03:58:42.800: DEBUG/LayoutView1(614): onInterceptTouchEvent action:ACTION_MOVE

04-11 03:58:42.800: DEBUG/LayoutView2(614): onInterceptTouchEvent action:ACTION_MOVE

04-11 03:58:42.800: DEBUG/MyTextView(614): onTouchEvent action:ACTION_MOVE

…… //省略過多的ACTION_MOVE

04-11 03:58:43.130: DEBUG/LayoutView1(614): onInterceptTouchEvent action:ACTION_UP

04-11 03:58:43.130: DEBUG/LayoutView2(614): onInterceptTouchEvent action:ACTION_UP

04-11 03:58:43.150: DEBUG/MyTextView(614): onTouchEvent action:ACTION_UP

------------------------------------------------------------------------------------------------------------------------------

這是最常見的情況,onInterceptTouchEvent並沒有做任何改變事件傳遞時序的操作,效果上和沒有覆寫該方法是一樣的。可以看到,各種事件的傳遞本身是自底向上的,次序是:LayoutView1->LayoutView2->MyTextView。注意,在onInterceptTouchEvent均返回false時,LayoutView1和LayoutView2的onTouchEvent並不會收到事件,而是最終傳遞給了MyTextView。

 

2.     LayoutView1onInterceptTouchEvent()處理down事件返回true

MyTextViewonTouchEvent()處理事件返回true

------------------------------------------------------------------------------------------------------------------------------

04-11 03:09:27.589: DEBUG/LayoutView1(446): onInterceptTouchEvent action:ACTION_DOWN

04-11 03:09:27.589: DEBUG/LayoutView1(446): onTouchEvent action:ACTION_DOWN

04-11 03:09:27.629: DEBUG/LayoutView1(446): onTouchEvent action:ACTION_MOVE

04-11 03:09:27.689: DEBUG/LayoutView1(446): onTouchEvent action:ACTION_MOVE

…… //省略過多的ACTION_MOVE

04-11 03:09:27.959: DEBUG/LayoutView1(446): onTouchEvent action:ACTION_UP

------------------------------------------------------------------------------------------------------------------------------

從Log可以看到,由於LayoutView1在攔截第一次down事件時return true,所以後續的事件(包括第一次的down)將由LayoutView1本身處理,事件不再傳遞下去。

 

3.       LayoutView1LayoutView2onInterceptTouchEvent()處理down事件返回false

MyTextViewonTouchEvent()處理事件返回false

LayoutView2onTouchEvent()處理事件返回true

----------------------------------------------------------------------------------------------------------------------------

04-11 09:50:21.147: DEBUG/LayoutView1(301): onInterceptTouchEvent action:ACTION_DOWN

04-11 09:50:21.147: DEBUG/LayoutView2(301): onInterceptTouchEvent action:ACTION_DOWN

04-11 09:50:21.147: DEBUG/MyTextView(301): onTouchEvent action:ACTION_DOWN

04-11 09:50:21.147: DEBUG/LayoutView2(301): onTouchEvent action:ACTION_DOWN

04-11 09:50:21.176: DEBUG/LayoutView1(301): onInterceptTouchEvent action:ACTION_MOVE

04-11 09:50:21.176: DEBUG/LayoutView2(301): onTouchEvent action:ACTION_MOVE

04-11 09:50:21.206: DEBUG/LayoutView1(301): onInterceptTouchEvent action:ACTION_MOVE

04-11 09:50:21.217: DEBUG/LayoutView2(301): onTouchEvent action:ACTION_MOVE

…… //省略過多的ACTION_MOVE

04-11 09:50:21.486: DEBUG/LayoutView1(301): onInterceptTouchEvent action:ACTION_UP

04-11 09:50:21.486: DEBUG/LayoutView2(301): onTouchEvent action:ACTION_UP

----------------------------------------------------------------------------------------------------------------------------

可以看到,由於MyTextView在onTouchEvent()中return false,down事件被傳遞給其父view,即LayoutView2的onTouchEvent()方法處理,由於在LayoutView2的onTouchEvent()中return true,所以down事件傳遞並沒有上傳到LayoutView1。注意,後續的move和up事件均被傳遞給LayoutView2的onTouchEvent()處理,而沒有傳遞給MyTextView。

 

----------------------------------------------------------------------------------------------------------------

應大家的要求,我把源代碼貼上,其實很簡單,就是基礎文件,主要是用來觀察事件的傳遞。

 

主Activity: InterceptTouchStudyActivity.java:

 

public class InterceptTouchStudyActivityextends Activity {

    static final String TAG = "ITSActivity";

    TextView tv;

   

    /** Calledwhen the activity is first created. */

    @Override

    public void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.layers_touch_pass_test);

     }

 }

 


      LayoutView1.java:


      public class LayoutView1 extends LinearLayout {

     private final String TAG = "LayoutView1";

       public LayoutView1(Context context, AttributeSet attrs) {

         super(context, attrs);

         Log.d(TAG,TAG);

     }

 

     @Override

     public boolean onInterceptTouchEvent(MotionEvent ev) {

         int action = ev.getAction();

         switch(action){

         case MotionEvent.ACTION_DOWN:

              Log.d(TAG,"onInterceptTouchEventaction:ACTION_DOWN");

//            return true;

              break;

         case MotionEvent.ACTION_MOVE:

              Log.d(TAG,"onInterceptTouchEventaction:ACTION_MOVE");

              break;

         case MotionEvent.ACTION_UP:

              Log.d(TAG,"onInterceptTouchEventaction:ACTION_UP");

              break;

         case MotionEvent.ACTION_CANCEL:

              Log.d(TAG,"onInterceptTouchEventaction:ACTION_CANCEL");

              break;

         }

        

         return false;

     }

 

     @Override

     public boolean onTouchEvent(MotionEvent ev) {

         int action = ev.getAction();

         switch(action){

         case MotionEvent.ACTION_DOWN:

              Log.d(TAG,"onTouchEventaction:ACTION_DOWN");

              break;

         case MotionEvent.ACTION_MOVE:

              Log.d(TAG,"onTouchEventaction:ACTION_MOVE");

              break;

         case MotionEvent.ACTION_UP:

              Log.d(TAG,"onTouchEventaction:ACTION_UP");

              break;

         case MotionEvent.ACTION_CANCEL:

              Log.d(TAG,"onTouchEventaction:ACTION_CANCEL");

              break;

         }

        

         return true;

     }

 

     @Override

     protected void onLayout(boolean changed, int l, int t, int r, int b) {

         // TODO Auto-generatedmethod stub

         super.onLayout(changed, l, t, r, b);

     }

 

     @Override

     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

         // TODO Auto-generatedmethod stub

         super.onMeasure(widthMeasureSpec, heightMeasureSpec);

     }

}


LayoutView2.java:

 

public class LayoutView2 extends LinearLayout {

    private final String TAG = "LayoutView2";

   

    public LayoutView2(Context context, AttributeSet attrs) {

       super(context, attrs);

       Log.d(TAG,TAG);

    }

 

    @Override

    public boolean onInterceptTouchEvent(MotionEvent ev) {

       int action = ev.getAction();

       switch(action){

       case MotionEvent.ACTION_DOWN:

           Log.d(TAG,"onInterceptTouchEventaction:ACTION_DOWN");

           break;

       case MotionEvent.ACTION_MOVE:

           Log.d(TAG,"onInterceptTouchEventaction:ACTION_MOVE");

           break;

       case MotionEvent.ACTION_UP:

           Log.d(TAG,"onInterceptTouchEventaction:ACTION_UP");

           break;

       case MotionEvent.ACTION_CANCEL:

           Log.d(TAG,"onInterceptTouchEventaction:ACTION_CANCEL");

           break;

       }

      

       return false;

    }

 

    @Override

    public boolean onTouchEvent(MotionEvent ev) {

       int action = ev.getAction();

       switch(action){

       case MotionEvent.ACTION_DOWN:

           Log.d(TAG,"onTouchEventaction:ACTION_DOWN");

           break;

       case MotionEvent.ACTION_MOVE:

           Log.d(TAG,"onTouchEventaction:ACTION_MOVE");

           break;

       case MotionEvent.ACTION_UP:

           Log.d(TAG,"onTouchEventaction:ACTION_UP");

           break;

       case MotionEvent.ACTION_CANCEL:

           Log.d(TAG,"onTouchEventaction:ACTION_CANCEL");

           break;

       }

      

       return true;

    } 

}


MyTextView.java:

public class MyTextView extends TextView {

    private final String TAG = "MyTextView";

   

    public MyTextView(Context context, AttributeSet attrs) {

       super(context, attrs);

       Log.d(TAG,TAG);

    }

 

    @Override

    public boolean onTouchEvent(MotionEvent ev) {

       int action = ev.getAction();

       switch(action){

       case MotionEvent.ACTION_DOWN:

           Log.d(TAG,"onTouchEventaction:ACTION_DOWN");

           break;

       case MotionEvent.ACTION_MOVE:

           Log.d(TAG,"onTouchEventaction:ACTION_MOVE");

           break;

       case MotionEvent.ACTION_UP:

           Log.d(TAG,"onTouchEventaction:ACTION_UP");

           break;

       case MotionEvent.ACTION_CANCEL:

           Log.d(TAG,"onTouchEventaction:ACTION_CANCEL");

           break;

       }

      

       return false;

    }

   

    public void onClick(View v) {

       Log.d(TAG, "onClick");

    }

   

    public boolean onLongClick(View v) {

       Log.d(TAG, "onLongClick");

       return false;

    }

}





最後我們來研究Scroller與computeScroll

Scroller這個類理解起來有一定的困難,剛開始接觸Scroller類的程序員可能無法理解Scroller和View系統是怎麼樣聯繫起來的。我經過自己的學習和實踐,對Scroller的用法和工作原理有了一定的理解,在這裏和大家分享一下,希望大家多多指教。

      首先從源碼開始分析:

        View.java

 

  1.   /** 
  2.  * Called by a parent to request that a child update its values for mScrollX 
  3.  * and mScrollY if necessary. This will typically be done if the child is 
  4.  * animating a scroll using a {@link android.widget.Scroller Scroller} 
  5.  * object. 
  6.  */  
  7. public void computeScroll()  
  8. {  
  9. }  



 

    computeScroll是一個空函數,很明顯我們需要去實現它,至於做什麼,就由我們自己來決定了。

    因爲View的子類很多,在下面的例子中,我會在一個自定義的類MyLinearLayout中去實現它。

 

    ViewGroup.java

   

  1. @Override  
  2. protected void dispatchDraw(Canvas canvas) {  
  3.   
  4.             .......  
  5.   
  6.             .......  
  7.   
  8.             .......  
  9.   
  10.             .......  
  11.   
  12.             for (int i = 0; i < count; i++) {  
  13.             final View child = children[i];  
  14.             if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null)  
  15.   
  16.             {  
  17.                 more |= drawChild(canvas, child, drawingTime);  
  18.             }  
  19.   
  20.             .......  
  21.   
  22.             .......  
  23.   
  24.             .......  


從dispatchDraw函數可以看出,ViewGroup會對它的每個孩子調用drawChild(),  在下面的例子中, ContentLinearLayout的孩子有2個,是2個MyLinearLayout類型的實例。

再看看drawChild函數:

  1.  protected boolean drawChild(Canvas canvas, View child, long drawingTime) {  
  2.   
  3.             ................  
  4.   
  5.             ................  
  6.   
  7.            child.computeScroll();  
  8.   
  9.             ................  
  10.   
  11.             ................  
  12.   
  13. }  

 

看到這裏,我想大家應該就明白了,在父容器重畫自己的孩子時,它會調用孩子的computScroll方法,也就是說例程中的ContentLinearLayout在調用dispatchDraw()函數時會調用MyLinearLayout的computeScroll方法。

     這個computeScroll()函數正是我們大展身手的地方,在這個函數裏我們可以去取得事先設置好的成員變量mScroller中的位置信息、速度信息等等,用這些參數來做我們想做的事情。

    看到這裏大家一定迫不及待的想看代碼了,代碼如下:

    

  1. package com.yulongfei.scroller;  
  2.   
  3. import android.widget.LinearLayout;  
  4. import android.widget.Scroller;  
  5. import android.app.Activity;  
  6. import android.content.Context;  
  7. import android.graphics.Canvas;  
  8. import android.os.Bundle;  
  9. import android.util.Log;  
  10. import android.view.View;  
  11. import android.widget.Button;  
  12. import android.view.View.OnClickListener;  
  13.   
  14. public class TestScrollerActivity extends Activity {  
  15.  private static final String TAG = "TestScrollerActivity";  
  16.     LinearLayout lay1,lay2,lay0;  
  17.      private Scroller mScroller;  
  18.     @Override  
  19.     public void onCreate(Bundle savedInstanceState) {  
  20.         super.onCreate(savedInstanceState);  
  21.         mScroller = new Scroller(this);  
  22.         lay1 = new MyLinearLayout(this);  
  23.         lay2 = new MyLinearLayout(this);  
  24.    
  25.         lay1.setBackgroundColor(this.getResources().getColor(android.R.color.darker_gray));  
  26.         lay2.setBackgroundColor(this.getResources().getColor(android.R.color.white));  
  27.         lay0 = new ContentLinearLayout(this);  
  28.         lay0.setOrientation(LinearLayout.VERTICAL);  
  29.         LinearLayout.LayoutParams p0 = new LinearLayout.LayoutParams  
  30.         (LinearLayout.LayoutParams.FILL_PARENT,LinearLayout.LayoutParams.FILL_PARENT);      
  31.         this.setContentView(lay0, p0);  
  32.    
  33.         LinearLayout.LayoutParams p1 = new LinearLayout.LayoutParams  
  34.         (LinearLayout.LayoutParams.FILL_PARENT,LinearLayout.LayoutParams.FILL_PARENT);      
  35.         p1.weight=1;  
  36.         lay0.addView(lay1,p1);  
  37.         LinearLayout.LayoutParams p2 = new LinearLayout.LayoutParams  
  38.         (LinearLayout.LayoutParams.FILL_PARENT,LinearLayout.LayoutParams.FILL_PARENT);      
  39.         p2.weight=1;  
  40.         lay0.addView(lay2,p2);  
  41.         MyButton btn1 = new MyButton(this);  
  42.         MyButton btn2 = new MyButton(this);  
  43.         btn1.setText("btn in layout1");  
  44.         btn2.setText("btn in layout2");  
  45.         btn1.setOnClickListener(new OnClickListener(){  
  46.             @Override  
  47.             public void onClick(View v) {  
  48.                     mScroller.startScroll(00, -30, -3050);  
  49.                 }  
  50.         });  
  51.         btn2.setOnClickListener(new OnClickListener(){  
  52.             @Override  
  53.             public void onClick(View v) {  
  54.                     mScroller.startScroll(2020, -50, -5050);  
  55.                 }  
  56.         });  
  57.         lay1.addView(btn1);  
  58.         lay2.addView(btn2);  
  59.     }  
  60.     class MyButton extends Button  
  61.     {  
  62.      public MyButton(Context ctx)  
  63.      {  
  64.       super(ctx);  
  65.      }  
  66.      @Override  
  67.      protected void onDraw(Canvas canvas)  
  68.      {  
  69.       super.onDraw(canvas);  
  70.       Log.d("MyButton"this.toString() + " onDraw------");  
  71.      }  
  72.     }  
  73.       
  74.     class MyLinearLayout extends LinearLayout  
  75.     {  
  76.      public MyLinearLayout(Context ctx)  
  77.      {  
  78.       super(ctx);  
  79.      }  
  80.        
  81.      @Override  
  82.         /** 
  83.          * Called by a parent to request that a child update its values for mScrollX 
  84.          * and mScrollY if necessary. This will typically be done if the child is 
  85.          * animating a scroll using a {@link android.widget.Scroller Scroller} 
  86.          * object. 
  87.          */  
  88.         public void computeScroll()   
  89.      {    
  90.     Log.d(TAG, this.toString() + " computeScroll-----------");  
  91.     if (mScroller.computeScrollOffset())//如果mScroller沒有調用startScroll,這裏將會返回false。  
  92.     {    
  93.         //因爲調用computeScroll函數的是MyLinearLayout實例,  
  94.      //所以調用scrollTo移動的將是該實例的孩子,也就是MyButton實例  
  95.         scrollTo(mScroller.getCurrX(), 0);   
  96.         Log.d(TAG, "getCurrX = " +  mScroller.getCurrX());  
  97.   
  98.         //繼續讓系統重繪  
  99.         getChildAt(0).invalidate();   
  100.     }  
  101.      }  
  102.     }  
  103.       
  104.     class ContentLinearLayout extends LinearLayout  
  105.     {  
  106.      public ContentLinearLayout(Context ctx)  
  107.      {  
  108.       super(ctx);  
  109.      }  
  110.        
  111.      @Override  
  112.      protected void dispatchDraw(Canvas canvas)  
  113.      {  
  114.       Log.d("ContentLinearLayout""contentview dispatchDraw");  
  115.       super.dispatchDraw(canvas);  
  116.      }  
  117.     }  
  118. }  


    對代碼做一個簡單介紹:

    例子中定義了2個MyButton實例btn1和btn2,它們將被其父容器MyLinearLayout實例lay1和lay2通過調用scrollTo來移動。

  ContentLinearLayout實例lay0爲Activity的contentview,它有2個孩子,分別是lay1和lay2。

   mScroller是一個封裝位置和速度等信息的變量,startScroll()函數只是對它的一些成員變量做一些設置,這個設置的唯一效果就是導致mScroller.computeScrollOffset()    返回true。

      這裏大家可能有個疑問,既然startScroll()只是虛晃一槍,那scroll的動態效果到底是誰觸發的呢?

後面我將給出答案。

 

運行程序,我們來看看Log

點擊btn1:

 

 

點擊btn2:


   對照Log,我從button被點擊開始,對整個繪製流程進行分析,首先button被點擊(這裏將回答上文的問題),button的背景將發生變化,這時button將調用invalidate()請求重繪,這就是View系統重繪的源頭,即scroll動態效果的觸發者。與此同時,mScroller.startScroll被調用了,mScroller在此時被設置了一些有效值。

   好了,既然重繪請求已發出了,那麼整個View系統就會來一次自上而下的繪製了,首先輸出的Log就是“contentview dispatchDraw”了,它將繪製需要重繪的孩子(lay1和lay2中的一個),接着會調用drawChild,使得computeScroll函數被觸發(drawChild裏面會調用child.computeScroll()),於是,lay1或者lay2就會以mScroller的位置信息爲依據來調用scrollTo了,它的孩子btn1或者btn2就會被移動了。之後又調用了getChildAt(0).invalidate(),這將導致系統不斷重繪,直到startScroll中設置的時間耗盡mScroller.computeScrollOffset()返回false才停下來。

   好了,現在整個流程都分析完了,相信大家應該清楚了Scroller類與View系統的關係了吧。理解了Scroller的工作原理,你會發現原來Scroller類並不神祕,甚至有點被動,它除了儲存一些數值,什麼其他的事情都沒做,Scroller類中的一些變量mStartX, mFinalX, mDuration等等的意義也很好理解。

注意:

1、之前有朋友反饋button點擊不能移動,這是因爲android 4.0默認打開了硬件加速,如果想讓button移動,請在AndroidManifest的Application標籤或者activity標籤中加入:android:hardwareAccelerated="false"

2、代碼中的 getChildAt(0).invalidate(); 是多餘的,可以不寫,多謝網友指出這一點。



scroller講解

1、掌握View(視圖)的"視圖座標"以及"佈局座標",以及scrollTo()和scrollBy()方法的作用 ----- 必須理解

                      如果對這方面知識不太清楚的話,建議先看看我的這篇博客

                         <Android中滑屏初探 ---- scrollTo 以及 scrollBy方法使用說明>, 

                   不誇張地說,這篇博客理論上來說是我們這篇博文的基礎。


             2、知道onInterceptTouchEvent()以及onTouchEvent()對觸摸事件的分發流程         ---- 不是必須

             3、知道怎麼繪製自定義ViewGroup即可                                        ---- 不是必須

 

 

     OK。 繼續往下看,請一定有所準備 。大家跟着我一步一步來咯。

 

 知識點一:  關於scrollTo()和scrollBy()以及偏移座標的設置/取值問題


        在前面一篇博文中《Android中滑屏初探 ---- scrollTo 以及 scrollBy方法使用說明》,我們掌握了scrollTo()和

scrollBy()方法的作用,這兩個方法的主要作用是將View/ViewGroup移至指定的座標中,並且將偏移量保存起來。另外:

                  mScrollX 代表X軸方向的偏移座標

                  mScrollY 代表Y軸方向的偏移座標


          關於偏移量的設置我們可以參看下源碼:

  1. package com.qin.customviewgroup;  
  2.   
  3. public class View {  
  4.     ....  
  5.     protected int mScrollX;   //該視圖內容相當於視圖起始座標的偏移量   , X軸 方向      
  6.     protected int mScrollY;   //該視圖內容相當於視圖起始座標的偏移量   , Y軸方向  
  7.     //返回值  
  8.     public final int getScrollX() {  
  9.         return mScrollX;  
  10.     }  
  11.     public final int getScrollY() {  
  12.         return mScrollY;  
  13.     }  
  14.     public void scrollTo(int x, int y) {  
  15.         //偏移位置發生了改變  
  16.         if (mScrollX != x || mScrollY != y) {  
  17.             int oldX = mScrollX;  
  18.             int oldY = mScrollY;  
  19.             mScrollX = x;  //賦新值,保存當前便宜量  
  20.             mScrollY = y;  
  21.             //回調onScrollChanged方法  
  22.             onScrollChanged(mScrollX, mScrollY, oldX, oldY);  
  23.             if (!awakenScrollBars()) {  
  24.                 invalidate();  //一般都引起重繪  
  25.             }  
  26.         }  
  27.     }  
  28.     // 看出原因了吧 。。 mScrollX 與 mScrollY 代表我們當前偏移的位置 , 在當前位置繼續偏移(x ,y)個單位  
  29.     public void scrollBy(int x, int y) {  
  30.         scrollTo(mScrollX + x, mScrollY + y);  
  31.     }  
  32.     //...  
  33. }  

     於是,在任何時刻我們都可以獲取該View/ViewGroup的偏移位置了,即調用getScrollX()方法和getScrollY()方法

 

 知識點二: Scroller類的介紹


         在初次看Launcher滑屏的時候,我就對Scroller類的學習感到非常蛋疼,完全失去了繼續研究的慾望。如今,沒得辦法,

  得重新看Launcher模塊,基本上將Launcher大部分類以及功能給掌握了。當然,也花了一天時間來學習Launcher裏的滑屏實現

 ,基本上業是撥開雲霧見真知了。

     

       我們知道想把一個View偏移至指定座標(x,y)處,利用scrollTo()方法直接調用就OK了,但我們不能忽視的是,該方法本身

   來的的副作用:非常迅速的將View/ViewGroup偏移至目標點,而沒有對這個偏移過程有任何控制,對用戶而言可能是不太

   友好的。於是,基於這種偏移控制,Scroller類被設計出來了,該類的主要作用是爲偏移過程制定一定的控制流程(後面我們會

   知道的更多),從而使偏移更流暢,更完美。

   

     可能上面說的比較懸乎,道理也沒有講透。下面我就根據特定情景幫助大家分析下:


        情景: 從上海如何到武漢?

            普通的人可能會想,so easy : 飛機、輪船、11路公交車...

            文藝的人可能會想,  小 case : 時空忍術(火影的招數)、翻個筋斗(孫大聖的招數)...


     不管怎麼樣,我們想出來的套路可能有兩種:

               1、有個時間控制過程才能抵達(緩慢的前進)                              -----     對應於Scroller的作用

                      假設做火車,這個過程可能包括: 火車速率,花費週期等;

               2、瞬間抵達(超神太快了,都眩暈了,用戶體驗不太好)                     ------   對應於scrollTo()的作用

 

    模擬Scroller類的實現功能:


        假設從上海做動車到武漢需要10個小時,行進距離爲1000km ,火車速率200/h 。採用第一種時間控制方法到達武漢的

   整個配合過程可能如下:

        我們每隔一段時間(例如1小時),計算火車應該行進的距離,然後調用scrollTo()方法,行進至該處。10小時過完後,

    我們也就達到了目的地了。

 

    相信大家心裏應該有個感覺了。我們就分析下源碼裏去看看Scroller類的相關方法.

 

     其源代碼(部分)如下: 路徑位於 \frameworks\base\core\java\android\widget\Scroller.java

  1. public class Scroller  {  
  2.   
  3.     private int mStartX;    //起始座標點 ,  X軸方向  
  4.     private int mStartY;    //起始座標點 ,  Y軸方向  
  5.     private int mCurrX;     //當前座標點  X軸, 即調用startScroll函數後,經過一定時間所達到的值  
  6.     private int mCurrY;     //當前座標點  Y軸, 即調用startScroll函數後,經過一定時間所達到的值  
  7.      
  8.     private float mDeltaX;  //應該繼續滑動的距離, X軸方向  
  9.     private float mDeltaY;  //應該繼續滑動的距離, Y軸方向  
  10.     private boolean mFinished;  //是否已經完成本次滑動操作, 如果完成則爲 true  
  11.   
  12.     //構造函數  
  13.     public Scroller(Context context) {  
  14.         this(context, null);  
  15.     }  
  16.     public final boolean isFinished() {  
  17.         return mFinished;  
  18.     }  
  19.     //強制結束本次滑屏操作  
  20.     public final void forceFinished(boolean finished) {  
  21.         mFinished = finished;  
  22.     }  
  23.     public final int getCurrX() {  
  24.         return mCurrX;  
  25.     }  
  26.      /* Call this when you want to know the new location.  If it returns true, 
  27.      * the animation is not yet finished.  loc will be altered to provide the 
  28.      * new location. */    
  29.     //根據當前已經消逝的時間計算當前的座標點,保存在mCurrX和mCurrY值中  
  30.     public boolean computeScrollOffset() {  
  31.         if (mFinished) {  //已經完成了本次動畫控制,直接返回爲false  
  32.             return false;  
  33.         }  
  34.         int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);  
  35.         if (timePassed < mDuration) {  
  36.             switch (mMode) {  
  37.             case SCROLL_MODE:  
  38.                 float x = (float)timePassed * mDurationReciprocal;  
  39.                 ...  
  40.                 mCurrX = mStartX + Math.round(x * mDeltaX);  
  41.                 mCurrY = mStartY + Math.round(x * mDeltaY);  
  42.                 break;  
  43.             ...  
  44.         }  
  45.         else {  
  46.             mCurrX = mFinalX;  
  47.             mCurrY = mFinalY;  
  48.             mFinished = true;  
  49.         }  
  50.         return true;  
  51.     }  
  52.     //開始一個動畫控制,由(startX , startY)在duration時間內前進(dx,dy)個單位,即到達座標爲(startX+dx , startY+dy)出  
  53.     public void startScroll(int startX, int startY, int dx, int dy, int duration) {  
  54.         mFinished = false;  
  55.         mDuration = duration;  
  56.         mStartTime = AnimationUtils.currentAnimationTimeMillis();  
  57.         mStartX = startX;       mStartY = startY;  
  58.         mFinalX = startX + dx;  mFinalY = startY + dy;  
  59.         mDeltaX = dx;            mDeltaY = dy;  
  60.         ...  
  61.     }  
  62. }  

     其中比較重要的兩個方法爲:


            public void startScroll(int startX, int startY, int dx, int dy, int duration)

                   函數功能說明:根據當前已經消逝的時間計算當前的座標點,保存在mCurrX和mCurrY值中

            public void startScroll(int startX, int startY, int dx, int dy, int duration)

                  函數功能說明:開始一個動畫控制,由(startX , startY)在duration時間內前進(dx,dy)個單位,到達座標爲

                      (startX+dx , startY+dy)處。


        PS : 強烈建議大家看看該類的源碼,便於後續理解。


知識點二: computeScroll()方法介紹


       爲了易於控制滑屏控制,Android框架提供了 computeScroll()方法去控制這個流程。在繪製View時,會在draw()過程調用該

  方法。因此, 再配合使用Scroller實例,我們就可以獲得當前應該的偏移座標,手動使View/ViewGroup偏移至該處。

     computeScroll()方法原型如下,該方法位於ViewGroup.java類中      

  1. /** 
  2.      * Called by a parent to request that a child update its values for mScrollX 
  3.      * and mScrollY if necessary. This will typically be done if the child is 
  4.      * animating a scroll using a {@link android.widget.Scroller Scroller} 
  5.      * object. 
  6.      */由父視圖調用用來請求子視圖根據偏移值 mScrollX,mScrollY重新繪製  
  7.     public void computeScroll() { //空方法 ,自定義ViewGroup必須實現方法體  
  8.           
  9.     }  

          爲了實現偏移控制,一般自定義View/ViewGroup都需要重載該方法 。

 

     其調用過程位於View繪製流程draw()過程中,如下:

  1. @Override  
  2. protected void dispatchDraw(Canvas canvas){  
  3.     ...  
  4.       
  5.     for (int i = 0; i < count; i++) {  
  6.         final View child = children[getChildDrawingOrder(count, i)];  
  7.         if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {  
  8.             more |= drawChild(canvas, child, drawingTime);  
  9.         }  
  10.     }  
  11. }  
  12. protected boolean drawChild(Canvas canvas, View child, long drawingTime) {  
  13.     ...  
  14.     child.computeScroll();  
  15.     ...  
  16. }  

   Demo說明:

           我們簡單的複用了之前寫的一個自定義ViewGroup,與以前一次有區別的是,我們沒有調用scrollTo()方法去進行瞬間

       偏移。 本次做法如下:

                   第一、調用Scroller實例去產生一個偏移控制(對應於startScroll()方法)

                   第二、手動調用invalid()方法去重新繪製,剩下的就是在 computeScroll()里根據當前已經逝去的時間,獲取當前

                       應該偏移的座標(由Scroller實例對應的computeScrollOffset()計算而得),

                   第三、當前應該偏移的座標,調用scrollBy()方法去緩慢移動至該座標處。

 

  截圖如下:



                                                         


                                         原始界面                                     點擊按鈕或者觸摸屏之後的顯示界面


        附:由於滑動截屏很難,只是簡單的截取了兩個個靜態圖片,觸摸的話可以實現左右滑動切屏了。


           更多知識點,請看代碼註釋。。

 

  1. //自定義ViewGroup , 包含了三個LinearLayout控件,存放在不同的佈局位置,通過scrollBy或者scrollTo方法切換  
  2. public class MultiViewGroup extends ViewGroup {  
  3.     ...  
  4.     //startScroll開始移至下一屏  
  5.     public void startMove(){  
  6.         curScreen ++ ;  
  7.         Log.i(TAG, "----startMove---- curScreen " + curScreen);  
  8.           
  9.         //使用動畫控制偏移過程 , 3s內到位  
  10.         mScroller.startScroll((curScreen-1) * getWidth(), 0, getWidth(), 0,3000);  
  11.         //其實點擊按鈕的時候,系統會自動重新繪製View,我們還是手動加上吧。  
  12.         invalidate();  
  13.         //使用scrollTo一步到位  
  14.         //scrollTo(curScreen * MultiScreenActivity.screenWidth, 0);  
  15.     }  
  16.     // 由父視圖調用用來請求子視圖根據偏移值 mScrollX,mScrollY重新繪製  
  17.     @Override  
  18.     public void computeScroll() {     
  19.         // TODO Auto-generated method stub  
  20.         Log.e(TAG, "computeScroll");  
  21.         // 如果返回true,表示動畫還沒有結束  
  22.         // 因爲前面startScroll,所以只有在startScroll完成時 纔會爲false  
  23.         if (mScroller.computeScrollOffset()) {  
  24.             Log.e(TAG, mScroller.getCurrX() + "======" + mScroller.getCurrY());  
  25.             // 產生了動畫效果,根據當前值 每次滾動一點  
  26.             scrollTo(mScroller.getCurrX(), mScroller.getCurrY());  
  27.               
  28.             Log.e(TAG, "### getleft is " + getLeft() + " ### getRight is " + getRight());  
  29.             //此時同樣也需要刷新View ,否則效果可能有誤差  
  30.             postInvalidate();  
  31.         }  
  32.         else  
  33.             Log.i(TAG, "have done the scoller -----");  
  34.     }  
  35.     //馬上停止移動,如果已經超過了下一屏的一半,我們強制滑到下一個屏幕  
  36.     public void stopMove(){  
  37.           
  38.         Log.v(TAG, "----stopMove ----");  
  39.           
  40.         if(mScroller != null){  
  41.             //如果動畫還沒結束,我們就按下了結束的按鈕,那我們就結束該動畫,即馬上滑動指定位置  
  42.             if(!mScroller.isFinished()){  
  43.                   
  44.                 int scrollCurX= mScroller.getCurrX() ;  
  45.                 //判斷是否超過下一屏的中間位置,如果達到就抵達下一屏,否則保持在原屏幕  
  46.                 // 這樣的一個簡單公式意思是:假設當前滑屏偏移值即 scrollCurX 加上每個屏幕一半的寬度,除以每個屏幕的寬度就是  
  47.                 //  我們目標屏所在位置了。 假如每個屏幕寬度爲320dip, 我們滑到了500dip處,很顯然我們應該到達第二屏,索引值爲1  
  48.                 //即(500 + 320/2)/320 = 1  
  49.                 int descScreen = ( scrollCurX + getWidth() / 2) / getWidth() ;  
  50.                   
  51.                 Log.i(TAG, "-mScroller.is not finished scrollCurX +" + scrollCurX);  
  52.                 Log.i(TAG, "-mScroller.is not finished descScreen +" + descScreen);  
  53.                 mScroller.abortAnimation();  
  54.   
  55.                 //停止了動畫,我們馬上滑倒目標位置  
  56.                 scrollTo(descScreen *getWidth() , 0);  
  57.                 curScreen = descScreen ; //糾正目標屏位置  
  58.             }  
  59.             else  
  60.                 Log.i(TAG, "----OK mScroller.is  finished ---- ");  
  61.         }     
  62.     }  
  63.     ...  
  64. }  

 如何實現觸摸滑屏? 


      其實網上有很多關於Launcher實現滑屏的博文,基本上也把道理闡釋的比較明白了 。我這兒也是基於自己的理解,將一些

 重要方面的知識點給補充下,希望能幫助大家理解。


      想要實現滑屏操作,值得考慮的事情包括如下幾個方面:


        其中:onInterceptTouchEvent()主要功能是控制觸摸事件的分發,例如是子視圖的點擊事件還是滑動事件。

        其他所有處理過程均在onTouchEvent()方法裏實現了。

            1、屏幕的滑動要根據手指的移動而移動  ---- 主要實現在onTouchEvent()方法中

            2、當手指鬆開時,可能我們並沒有完全滑動至某個屏幕上,這是我們需要手動判斷當前偏移至去計算目標屏(當前屏或者

               前後屏),並且優雅的偏移到目標屏(當然是用Scroller實例咯)。

           3、調用computeScroll ()去實現緩慢移動過程。

 

  知識點介紹:              

    VelocityTracker類

           功能:  根據觸摸位置計算每像素的移動速率。

           常用方法有:     

                     public void addMovement (MotionEvent ev)

                   功能:添加觸摸對象MotionEvent , 用於計算觸摸速率。   

            public void computeCurrentVelocity (int units)

                   功能:以每像素units單位考覈移動速率。額,其實我也不太懂,賦予值1000即可。
                   參照源碼 該units的意思如下:
                           參數 units : The units you would like the velocity in.  A value of 1
                             provides pixels per millisecond, 1000 provides pixels per second, etc.

            public float getXVelocity ()

                           功能:獲得X軸方向的移動速率。

    ViewConfiguration類

           功能: 獲得一些關於timeouts(時間)、sizes(大小)、distances(距離)的標準常量值 。

           常用方法:

                  public int getScaledEdgeSlop()

                      說明:獲得一個觸摸移動的最小像素值。也就是說,只有超過了這個值,才代表我們該滑屏處理了。

                 public static int getLongPressTimeout()

                     說明:獲得一個執行長按事件監聽(onLongClickListener)的值。也就是說,對某個View按下觸摸時,只有超過了

         這個時間值在,才表示我們該對該View回調長按事件了;否則,小於這個時間點鬆開手指,只執行onClick監聽

 

 

        我能寫下來的也就這麼多了,更多的東西參考代碼註釋吧。 在掌握了上面我羅列的知識後(重點scrollTo、Scroller類),

    其他方面的知識都是關於點與點之間的計算了以及觸摸事件的分發了。這方面感覺也沒啥可寫的。

  1. //自定義ViewGroup , 包含了三個LinearLayout控件,存放在不同的佈局位置,通過scrollBy或者scrollTo方法切換  
  2. public class MultiViewGroup extends ViewGroup {  
  3.   
  4.     private static String TAG = "MultiViewGroup";  
  5.       
  6.     private int curScreen = 0 ;  //當前屏幕  
  7.     private Scroller mScroller = null ; //Scroller對象實例  
  8.       
  9.     public MultiViewGroup(Context context) {  
  10.         super(context);  
  11.         mContext = context;  
  12.         init();  
  13.     }  
  14.     public MultiViewGroup(Context context, AttributeSet attrs) {  
  15.         super(context, attrs);  
  16.         mContext = context;  
  17.         init();  
  18.     }  
  19.     //初始化  
  20.     private void init() {     
  21.         ...  
  22.         //初始化Scroller實例  
  23.         mScroller = new Scroller(mContext);  
  24.         // 初始化3個 LinearLayout控件  
  25.         ...  
  26.         //初始化一個最小滑動距離  
  27.         mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();  
  28.     }  
  29.     // 由父視圖調用用來請求子視圖根據偏移值 mScrollX,mScrollY重新繪製  
  30.     @Override  
  31.     public void computeScroll() {     
  32.         // TODO Auto-generated method stub  
  33.         Log.e(TAG, "computeScroll");  
  34.         // 如果返回true,表示動畫還沒有結束  
  35.         // 因爲前面startScroll,所以只有在startScroll完成時 纔會爲false  
  36.         if (mScroller.computeScrollOffset()) {  
  37.             Log.e(TAG, mScroller.getCurrX() + "======" + mScroller.getCurrY());  
  38.             // 產生了動畫效果,根據當前值 每次滾動一點  
  39.             scrollTo(mScroller.getCurrX(), mScroller.getCurrY());  
  40.               
  41.             Log.e(TAG, "### getleft is " + getLeft() + " ### getRight is " + getRight());  
  42.             //此時同樣也需要刷新View ,否則效果可能有誤差  
  43.             postInvalidate();  
  44.         }  
  45.         else  
  46.             Log.i(TAG, "have done the scoller -----");  
  47.     }  
  48.     //兩種狀態: 是否處於滑屏狀態  
  49.     private static final int TOUCH_STATE_REST = 0;  //什麼都沒做的狀態  
  50.     private static final int TOUCH_STATE_SCROLLING = 1;  //開始滑屏的狀態  
  51.     private int mTouchState = TOUCH_STATE_REST; //默認是什麼都沒做的狀態  
  52.     //--------------------------   
  53.     //處理觸摸事件 ~  
  54.     public static int  SNAP_VELOCITY = 600 ;  //最小的滑動速率  
  55.     private int mTouchSlop = 0 ;              //最小滑動距離,超過了,才認爲開始滑動  
  56.     private float mLastionMotionX = 0 ;       //記住上次觸摸屏的位置  
  57.     //處理觸摸的速率  
  58.     private VelocityTracker mVelocityTracker = null ;  
  59.       
  60.     // 這個感覺沒什麼作用 不管true還是false 都是會執行onTouchEvent的 因爲子view裏面onTouchEvent返回false了  
  61.     @Override  
  62.     public boolean onInterceptTouchEvent(MotionEvent ev) {  
  63.         // TODO Auto-generated method stub  
  64.         Log.e(TAG, "onInterceptTouchEvent-slop:" + mTouchSlop);  
  65.   
  66.         final int action = ev.getAction();  
  67.         //表示已經開始滑動了,不需要走該Action_MOVE方法了(第一次時可能調用)。  
  68.         //該方法主要用於用戶快速鬆開手指,又快速按下的行爲。此時認爲是出於滑屏狀態的。  
  69.         if ((action == MotionEvent.ACTION_MOVE) && (mTouchState != TOUCH_STATE_REST)) {  
  70.             return true;  
  71.         }  
  72.           
  73.         final float x = ev.getX();  
  74.         final float y = ev.getY();  
  75.   
  76.         switch (action) {  
  77.         case MotionEvent.ACTION_MOVE:  
  78.             Log.e(TAG, "onInterceptTouchEvent move");  
  79.             final int xDiff = (int) Math.abs(mLastionMotionX - x);  
  80.             //超過了最小滑動距離,就可以認爲開始滑動了  
  81.             if (xDiff > mTouchSlop) {  
  82.                 mTouchState = TOUCH_STATE_SCROLLING;  
  83.             }  
  84.             break;  
  85.   
  86.         case MotionEvent.ACTION_DOWN:  
  87.             Log.e(TAG, "onInterceptTouchEvent down");  
  88.             mLastionMotionX = x;  
  89.             mLastMotionY = y;  
  90.             Log.e(TAG, mScroller.isFinished() + "");  
  91.             mTouchState = mScroller.isFinished() ? TOUCH_STATE_REST : TOUCH_STATE_SCROLLING;  
  92.   
  93.             break;  
  94.   
  95.         case MotionEvent.ACTION_CANCEL:  
  96.         case MotionEvent.ACTION_UP:  
  97.             Log.e(TAG, "onInterceptTouchEvent up or cancel");  
  98.             mTouchState = TOUCH_STATE_REST;  
  99.             break;  
  100.         }  
  101.         Log.e(TAG, mTouchState + "====" + TOUCH_STATE_REST);  
  102.         return mTouchState != TOUCH_STATE_REST;  
  103.     }  
  104.     public boolean onTouchEvent(MotionEvent event){  
  105.   
  106.         super.onTouchEvent(event);  
  107.           
  108.         Log.i(TAG, "--- onTouchEvent--> " );  
  109.   
  110.         // TODO Auto-generated method stub  
  111.         Log.e(TAG, "onTouchEvent start");  
  112.         //獲得VelocityTracker對象,並且添加滑動對象  
  113.         if (mVelocityTracker == null) {  
  114.             mVelocityTracker = VelocityTracker.obtain();  
  115.         }  
  116.         mVelocityTracker.addMovement(event);  
  117.           
  118.         //觸摸點  
  119.         float x = event.getX();  
  120.         float y = event.getY();  
  121.         switch(event.getAction()){  
  122.         case MotionEvent.ACTION_DOWN:  
  123.             //如果屏幕的動畫還沒結束,你就按下了,我們就結束上一次動畫,即開始這次新ACTION_DOWN的動畫  
  124.             if(mScroller != null){  
  125.                 if(!mScroller.isFinished()){  
  126.                     mScroller.abortAnimation();   
  127.                 }  
  128.             }  
  129.             mLastionMotionX = x ; //記住開始落下的屏幕點  
  130.             break ;  
  131.         case MotionEvent.ACTION_MOVE:  
  132.             int detaX = (int)(mLastionMotionX - x ); //每次滑動屏幕,屏幕應該移動的距離  
  133.             scrollBy(detaX, 0);//開始緩慢滑屏咯。 detaX > 0 向右滑動 , detaX < 0 向左滑動 ,  
  134.               
  135.             Log.e(TAG, "--- MotionEvent.ACTION_MOVE--> detaX is " + detaX );  
  136.             mLastionMotionX = x ;  
  137.             break ;  
  138.         case MotionEvent.ACTION_UP:  
  139.               
  140.             final VelocityTracker velocityTracker = mVelocityTracker  ;  
  141.             velocityTracker.computeCurrentVelocity(1000);  
  142.             //計算速率  
  143.             int velocityX = (int) velocityTracker.getXVelocity() ;    
  144.             Log.e(TAG , "---velocityX---" + velocityX);  
  145.               
  146.             //滑動速率達到了一個標準(快速向右滑屏,返回上一個屏幕) 馬上進行切屏處理  
  147.             if (velocityX > SNAP_VELOCITY && curScreen > 0) {  
  148.                 // Fling enough to move left  
  149.                 Log.e(TAG, "snap left");  
  150.                 snapToScreen(curScreen - 1);  
  151.             }  
  152.             //快速向左滑屏,返回下一個屏幕)  
  153.             else if(velocityX < -SNAP_VELOCITY && curScreen < (getChildCount()-1)){  
  154.                 Log.e(TAG, "snap right");  
  155.                 snapToScreen(curScreen + 1);  
  156.             }  
  157.             //以上爲快速移動的 ,強制切換屏幕  
  158.             else{  
  159.                 //我們是緩慢移動的,因此先判斷是保留在本屏幕還是到下一屏幕  
  160.                 snapToDestination();  
  161.             }  
  162.             //回收VelocityTracker對象  
  163.             if (mVelocityTracker != null) {  
  164.                 mVelocityTracker.recycle();  
  165.                 mVelocityTracker = null;  
  166.             }  
  167.             //修正mTouchState值  
  168.             mTouchState = TOUCH_STATE_REST ;  
  169.               
  170.             break;  
  171.         case MotionEvent.ACTION_CANCEL:  
  172.             mTouchState = TOUCH_STATE_REST ;  
  173.             break;  
  174.         }  
  175.           
  176.         return true ;  
  177.     }  
  178.     ////我們是緩慢移動的,因此需要根據偏移值判斷目標屏是哪個?  
  179.     private void snapToDestination(){  
  180.         //當前的偏移位置  
  181.         int scrollX = getScrollX() ;  
  182.         int scrollY = getScrollY() ;  
  183.           
  184.         Log.e(TAG, "### onTouchEvent snapToDestination ### scrollX is " + scrollX);  
  185.         //判斷是否超過下一屏的中間位置,如果達到就抵達下一屏,否則保持在原屏幕      
  186.         //直接使用這個公式判斷是哪一個屏幕 前後或者自己  
  187.         //判斷是否超過下一屏的中間位置,如果達到就抵達下一屏,否則保持在原屏幕  
  188.         // 這樣的一個簡單公式意思是:假設當前滑屏偏移值即 scrollCurX 加上每個屏幕一半的寬度,除以每個屏幕的寬度就是  
  189.         //  我們目標屏所在位置了。 假如每個屏幕寬度爲320dip, 我們滑到了500dip處,很顯然我們應該到達第二屏  
  190.         int destScreen = (getScrollX() + MultiScreenActivity.screenWidth / 2 ) / MultiScreenActivity.screenWidth ;  
  191.           
  192.         Log.e(TAG, "### onTouchEvent  ACTION_UP### dx destScreen " + destScreen);  
  193.           
  194.         snapToScreen(destScreen);  
  195.     }  
  196.     //真正的實現跳轉屏幕的方法  
  197.     private void snapToScreen(int whichScreen){   
  198.         //簡單的移到目標屏幕,可能是當前屏或者下一屏幕  
  199.         //直接跳轉過去,不太友好  
  200.         //scrollTo(mLastScreen * MultiScreenActivity.screenWidth, 0);  
  201.         //爲了友好性,我們在增加一個動畫效果  
  202.         //需要再次滑動的距離 屏或者下一屏幕的繼續滑動距離  
  203.           
  204.         curScreen = whichScreen ;  
  205.         //防止屏幕越界,即超過屏幕數  
  206.         if(curScreen > getChildCount() - 1)  
  207.             curScreen = getChildCount() - 1 ;  
  208.         //爲了達到下一屏幕或者當前屏幕,我們需要繼續滑動的距離.根據dx值,可能想左滑動,也可能像又滑動  
  209.         int dx = curScreen * getWidth() - getScrollX() ;  
  210.           
  211.         Log.e(TAG, "### onTouchEvent  ACTION_UP### dx is " + dx);  
  212.           
  213.         mScroller.startScroll(getScrollX(), 0, dx, 0,Math.abs(dx) * 2);  
  214.           
  215.         //由於觸摸事件不會重新繪製View,所以此時需要手動刷新View 否則沒效果  
  216.         invalidate();  
  217.     }  
  218.     //開始滑動至下一屏  
  219.     public void startMove(){  
  220.         ...       
  221.     }  
  222.     //理解停止滑動  
  223.     public void stopMove(){  
  224.         ...  
  225.     }  
  226.     // measure過程  
  227.     @Override  
  228.     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
  229.        ...  
  230.     }  
  231.     // layout過程  
  232.     @Override  
  233.     protected void onLayout(boolean changed, int l, int t, int r, int b) {  
  234.         ...  
  235.     }  
  236. }  
發佈了37 篇原創文章 · 獲贊 8 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章