Android藝術開發探索第四章——View的工作原理(上)

來源:互聯網
上載者:User

標籤:attach   進位   介面   源碼   暫停   空間   sha   效果   find   

  這章就比較好玩了,主要介紹一下View的工作原理,還有自訂View的實現方法,在Android中,View是一個很重要的角色,簡單來說,View是Android中視覺的呈現,在介面上Android提供了一套完整的GUI庫,裡面有很多控制項,但是有時候往往並不能滿足於需求,所以只有自訂View了,我們會簡單的說下流程,然後再去實踐除了View的三大流程之外,View常見的回調方法也是必須掌握的,比如構造方法,onAttach,onVisibilityChanged,onDetach,另外對於一些有滑動效果的自訂View,還要處理滑動事件和滑動衝突,總的來說,自訂View有幾種固定的類型,View或者ViewGroup,有的直接重寫原生控制項,這個就要看需求了,好的,我們直接開始吧!
  
  一.初識ViewRoot和在正式介紹View的三大流程之前,我們還是要瞭解一些基本的概念,所以本章會說下ViewRoot和對應於Impl類,他是串連WindowManager和DecorView的紐帶,View的三大流程都是通過來完成的,在ActivityThread中,當Activity被建立完畢後,會將DecorView添加到Window值班費,同時會建立Impl對象,並將Impl對象和DecorView建立聯絡,這個可以參照官網:1212View的繪製流程從ViewRoot的perfromTraversals方法開始,他警告measure,layout和draw三個過程才能將View畫出來,,其中measure測量,layout確定view在容器的位置,draw開始繪製在螢幕上,針對perfromTraversals的大致流程,可以看圖這裡寫圖片描述圖中的perfromTraversals會依次調用perfromMeasure,perfromLayout,perfromDraw,他們分別完成頂級View的measure,layout和draw這三大流程,其中在perfromMeasure中會調用measure方法,在measure方法中又調用onMeasure,這個時候measure流程就從父容器傳遞到子項目了,這樣就完成了一次measure過程,接著子項目會重複父容器的measure過程,如此反覆的完成了整個View樹的遍曆,同理,其他兩個也是如此,唯一有點區別的是perfromDraw的傳遞過程是在draw反覆中通過dispatchDraw來實現的,不過這並沒有什麼本質的區別過程決定了View的寬高,Measure完成之後可以通過getMeasureWidth和getMeasureHeight來擷取View測量後的高寬,在所有的情況下塔幾乎都是等於最終的寬高,但是特殊情況除外,這點以後說,layout過程決定了view的四個頂點的座標和實際View的寬高,完成之後,通過getTop,getLeft,getRight,getBottom獲得,,Draw決定了View的顯示,只有draw方法完成了之後,view才會顯示在螢幕上如,頂級View DecorView,一般情況下他內部會包含一個豎直方向的LinearLayout,這裡面有上下兩部分,上面是標題列,下面是內容,在Activity中,我們可用通過setContentView設定布局檔案就是放在內容裡,而內容欄的id為content,因此我們可以理解為實際上是在setView,那如何得到content呢?你可以ViewGroup content = findviewbyid(android.R.id.content),如何得到我們設定的View呢:content.getChildAt(0),同時,通過源碼我們可用知道,DeaorView其實就是一個FrameLayout,View層事件都先經過DecorView,然後傳遞給View這裡寫圖片描述二.理解為了更好的理解View的測量過程,我們還需要理解MeasureSpec,從名字上看,MeasureSpec看起來像“測量規格”或者“測量說明書”,不管怎麼翻譯,他看起來就好像是或多或少的決定了View的測量過程,通過源碼可以發現,MeasureSpec的確參與了View的測量過程,讀者可能有疑問,MeasureSpec是幹什麼的呢?MeasureSpec在很大程度上決定了一個View的尺寸規格,之所以說很大程度上是因為這個過程還收到了父容器的影像,因為父容器影像MeasureSpec的建立過程,在測量過程中,系統會將View的LayoutParams根據父容器所施加的規則轉換成對應的MeasureSpec,然後再根據這個measureSpec來測量出View的寬高,MeasureSpec看起來有點複雜,其實他的實現很簡單,我們來詳細分解一下代表一個32位int值,高兩位代表SpecMode,低30位代表SpecSize,SpecMode是指測量模式,而SpecSize是指在某個測量模式下的規格大小,下面先看一下,內部的一些常量定義,通過這些就不難理解的工作原理了}}}}123456789101112131415161718192021222324123456789101112131415161718192021222324通過將SpecMode和SpecSize打包成一個int值來避免過多的對象記憶體配置,為了方便操作,其提供了打包和解包的作用,SpecMode和specSize也是一個int值,一直SpecMode和specSize可以打包成一個,一個可以通過解包的形式來得出其原始的SpecMode和SpecSize,需要注意的是這裡提到的是指所代表的int值,而非本身。
  
  有三類,每一類都有特殊的含義父容器不對View有任何的限制,要多大給多大,這種情況一般用於系統內部,表示一種測量的狀態父容器已經檢測出View所需要的精度大小,這個時候View的最終大小就是SpecSize所指定的值,它對應於LayoutParams中的match_parent,和具體的數值這兩種模式父容器指定了一個可用大小,即SpecSize,view的大小不能大於這個值,具體是什麼值要看不同view的具體實現,它對應於LayoutParams中和 LayoutParams 的對應關係系統內部是通過MeasureSpec來進行View的測量,但是正常情況下我們使用View的測量,但是正常情況下我們使用View指定MeasureSpec,但是儘管如此,我們也可以給View設定layoutparams,在view測量的時候,系統會將layoutparams在父容器的約束下轉換成對應的MeasureSpec,然後再根據這個MeasureSpec來確定view測量後的寬高,需要注意的是,MeasureSpec不是唯一由layoutparams決定的,layoutparams需要和父容器一起決定view的MeasureSpec從而進一步決定view的寬高,對於頂級view(DecorView)和普通的view來說,MeasureSpec的轉換過程有些不同,對於decorview,其MeasureSpec由父容器的MeasureSpec和自身的layoutparams來決定,MeasureSpec一旦確定後,MeasureSpec就可以去為view測量了對於DecorView來說,在ViewRootImpl中的measureHierarchy方法中有這麼一段代碼。他展示了DecorViwew的MeasureSpec建立過程,其中desiredWindowWidth和desiredWindowHeight是螢幕的尺寸123123接下來看下getRootMeasureSpec方法的實現:}}12345678910111213141516171819201234567891011121314151617181920通過上述代碼,DecorView的MesourSpec的產生過程就很明確了,具體來說其遵守了如下格式,根據layoutparams的寬高的參數來劃分精確模式,大小就是視窗的大小最大模式,大小不定,但是不能超出螢幕的大小固定大小(比如100dp):精確模式,大小為LayoutParams中指定的大小對於普通的View來說,這裡是指我們布局中的View,View的measure過程由ViewGroup傳遞而來,先看下ViewGroup的measureChildWithMargis方法}12345678910111213141234567891011121314上述的方法會對子項目進行measure,在調用子項目的measure方法之前會通過getChildMeasureSpec方法得到子項目的MesureSpec,從代碼上看,很顯然,子項目的MesureSpec的建立和父容器的MesureSpec和子項目的LayoutParams有關,此外,還和view的margin有關,具體可以看下ViewGroup的getChildMeasureSpec方法}}}}}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869上述方法不難理解,他的主要作用是根據父容器的MeasureSpec同時結合view本身來layoutparams來確定子項目的MesureSpec,參數中的pading是指父容器中已佔有的控制項大小,因此子項目可以用的大小為父容器的尺寸減去pading,具體代碼123123清楚的展示了普通View的MeasureSpec同時結合View本身的LayoutParams來確定子項目的MeaureSpec的建立規則,更加清晰的理解的邏輯,這裡提供一個表,表中對的工作原理進行了梳理,表中的parentSize是指父容器中目前可使用的大小:這張表暫時不畫,可以到書中看 182頁針對這張表,這裡再做一下說明。前面已經提到,對於普通View,其MeasureSpec 由父容器的MeasureSpec和自身的LayoutParams來共同決定,那麼針對不同的父容器和Viev本身不同的LayoutParams,View就可以有多種MeasureSpec。這裡簡單說一下,當View採用固定寬/高的時候,不管父容器的MeasureSpec是什麼,View 的MeasureSpee都是精確模式,那麼View也是精準模式並且其大小是父容器的剩餘空間;如果父容器是最大模式,那麼View也是最大模式並且其大小不會超過父容器的剩餘空間。當View的寬/高是wrap_content時,不管父容器的模式是精準還是最大化,View的模式總是最大化,並且大小不能超過父容器的剩餘空間,可能讀者會發現,在我們的分析中漏掉了UNSPECIFIED模式,那是因為這個模式主要用於系統內部多次Measure的情形,一般來說,我們不需要關注此模式。
  
  通過這張表可以看出,只要提供父容器的MeasureSpec和子項目的LayoutParams,就可以快速地確定出子項目的MeasureSpec了,有了 MeasureSpec就可以進一步確定出子元親測量後的大小了。需要說明的是,表中並非是什麼經驗總結,它只是這個方法以表格的方式呈現出來而已的工作流程View的工作流程主要是指measure、layout、draw這三大流程,即測量、布局和繪製,其中measure確定View的測量寬/高,layout確定View的最終寬/高和四個頂點的位置,而draww則將View繪製到螢幕上。
  
  過程過程要分情況來看,如果只是一個原始的View,那麼通過方法就可以完成了其測量過程,如果是一個ViewGroup,除了完成自己的測量過程外,還會遍曆去調用所有子項目的方法,各個子項目再遞迴去執行這個流程,下面針對這兩種情況分別討論的measure過程的 measure過程由其measure方法來完成,measure方法是一個final類型的方法,這就意味著子類不能重寫此方法,在View的measure方法中去調用View的onMesure方法,因此只需要看onMeasure的實現即可,View的onMesure方法如下所示:}12345671234567上面的代碼很簡介,但是簡潔不代表簡單,setMeasuredDimension會設定View寬/高的測量值,因此我們只需要getDefaultSize方法即可。
  
  }}1234567891011121314151612345678910111213141516可以看出,getDefaultSize這個邏輯很簡單,對於我們來說,我們只需要看AT_MOST和EXACTLY這兩種情況,簡單的理解,其實getDefaultSize返回的大小就是mesourSpec中的specSize,而這個specSize就是view的大小,這裡多次提到測量後的大小,是因為View最終的大小,是在layout階段的,所以這裡必須要加以區分,但是幾乎所有情況下的View的測量大小和最終大小是相等的至於UNSPECIFIED這種情況,一般用於系統內部的測量過程,在這種情況下,View的大小為getDefaultSize的第一個參數是size,即寬高分別為getSuggestedMinimumWidth和getSuggestedMinimumHeight()這兩個方法的傳回值:}}123456789123456789這裡只分析getSuggestedMinimumWidth方法的實現,getSuggestedMinimumHeight和他的原理是一樣的。從 getSuggestedMinimumWidth的代碼可以看出,如果View沒有設定背景,View的寬度為mMinwidth,而mMinwidth對應於android:minwidth這個屬性所指定的值,因此View的寬度即為android:minwidth屬性所指定的值。這個屬性如果不指定,那麼MinWidth則預設為0;如果View指定了背景,則View的寬度為,mMinwidthh的含義我們已經知道了,那麼mBackground.getMinimumWidth()是什麼呢?我們看一下Drwable的 getMinimumWidth方法,如下所示:}12341234可以看出,getMinimumWidth返回的就是Drawable的原始寬度,前提是這個Drawable有原始寬度,否則就返回0。那麼Drawable在什麼情況下有原始寬度呢?這裡先舉個例子說明一下,ShapeDrawable無原始寬/高,而BitmapDrawable有原始寬/高(圖片的尺寸),詳細內容會在第6章進行介紹。
  
  這裡再總結一下getSuggestedMinimumWidth的邏輯:如果View沒有設定背景,那麼返回android:minwidth這個屬性所指定的值,這個值可以為0:如果View設定了背景,則返回 android:minwidth和背景的最小寬度這兩者中的最大值,getSuggestedMinimumWidth和getSuggestedMinimumHeight的傳回值就是View 在UNSPECIFIED情況下的測量寬/高。
  
  從getDefaulSize方法的實現來看,View的寬/高由specSize決定,所以我們可以得出如下結論:直接繼承View的自訂控制項需要重寫onMeasure方法並設定wrapcontent時的自身大小,否則在布局中使用wrap_content就相當於使用matchparent。為什麼呢?這個原因需要結合上述代碼和之前的表才能更好地理解。從上述代碼中我們知道,如果View在布局中使用wrapcontent,那麼它的specMode是AT_MOST模式,在這種模式下,它的寬/高等於 specSize;查表4-1可知,這種情況下View的specSize是parentSize,而parentSize是父容器中目前可以使用的大小,也就是父容器當前剩餘的空間大小。很顯然,View的寬/高就等於父容器當前剩餘的空間大小,這種效果和在布局中使用match_parent完全一致。如何解決這個問題呢?也很簡單,代碼如下所示。
  
  }}1234567891011121314151612345678910111213141516在上面的代碼中,我們只需要給View指定一個預設的內部寬/高(mWidth和mHeight)),並在wrapcontent時設定此寬/高即可。對於非wrapcontent情形,我們沿用系統的測量值即可,至於這個預設的內部寬/高的大小如何指定,這個沒有固定的依據,根據需要靈活指定即可。如果查看TextView、Imageview等的源碼就可以知道,針對 wrapcontent情形,它們的onMeasure方法均做了特殊處理,讀者可以自行查看它們的源碼。
  
  的measure過程對於ViewGroup來說,除了完成自己的measure過程以外,還會遍曆去調用所有子項目的measure方法,各個子項目再通歸去執行這個過程。和View不同的是,ViewGroup是一個抽象類別,因此它沒有重寫View的onMeasure方法,但是它提供了一個叫}}}12345678910111234567891011從上述代碼中看到,在ViewGroup的measure時,會對每一個子項目進行測量,那麼這個方法就很好理解了}12345678910111234567891011很顯然,measurechild的思想就是取出子項目的LayoutParams,然後再通過getChidMeasureSpec來建立子項目的MeasureSpec,接著將MeasureSpec直接傳遞給View的measure方法來進行測量。getchildMeasureSpec的工作過程已經在上面進行了詳細分析。
  
  我們知道,ViewGroup並沒有定義其測量的具體過程,這是因為ViewGroup是一個抽象類別,其測量過程的onMeasure方法需要各個子類去具體實現,比如LinearLayout,RelativeLayout等,為什麼ViewGroup不像View一樣對其onMeasure方法做統一的實現呢?那是因為不同的ViewGroup子類有不同的布局特性,這導致它們的測量細節各不相同,比如Lineartayout和RelativeL.ayout這兩者的布局特性顯然不同,因此ViewGroup無法做統一實現。下面就通過LinearLayout的onMeasure方法來分析ViewGroup的 measure過程,其他Layout類型讀者可以自行分析。
  
  首先,我們來看一下LinearLayout的onMeasure方法}}1234567812345678上述的代碼很簡單我們選擇一個來看下,比如選中豎直方向的LinearLayout測量過程,即measureVertical,他的源碼還比較長,我們看:}}}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051從上面的代碼可以看出,系統會遍曆子項目並對每一個子項目執行measureChildBeforeLayout方法,這個方法內部會調用子項目的measure方法,這樣各個子項目就開始依次進入measure過程,並且系統通過mTotalLength這個變數來儲存LinearLayout在豎直方向上的初步高度,沒測量一個子項目,mTotalLength就會增加,增加的部分主要包括子項目的高度以及豎直方向上的margin等,當子項目測量完畢之後,LinearLayout會測量自己的大小,看源碼:}}}}}}}}}}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566www.gouyifl.cn676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707www.gouyiflb.cn172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122這裡對上述代碼進行說明,當子項目測量完畢之後,LinearLayout會根據子項目的情況來測量自己的大小,針對豎直的LinearLayout而言,他的水平方向的測量過程遵循View的測量過程,在豎直方向的測量過程和View有些不同,具體來說,是指,如果他的布局中高度採用的是match_parent或者具體值,那麼他的繪製過程和View一致,即高度為specSize,如果他的布局中高度採用warp_content,那麼她的高度是所有的子項目所佔用的高度綜合,但是仍然不能超過他的父容器剩餘空間,但是他的最終高度還是需要考慮其他的豎直方向上的pading,這個過程進一步參看源碼:}}}123456789101112131415161718192021123456789101112131415161718192021View的onMeasure是三大流程中最複雜的一個,measure完成以後,通過getMeasureWidth/Height就可以正確地擷取到View的測量寬/高。需要注意的是,在某些極端情況下measure才能確定最終的測量寬/高,在這種情形下,系統可能要多次調用measure方法進行測量,在這種情況下,載onMeasure方法中拿到的測量值很可能是不準確的。一個比較好的習慣是在onLayout方法中去擷取View的測量寬/高或者最終寬/高。
  
  上面已經對Viaw的measure過程進行了詳細的分析,現在考慮一種情況,比如我們想在Activity已啟動的時候就做一件任務,但是這一件任務需要擷取某個View的寬/高,讀者可能會說,這很簡單啊,在onCreate或者onResume裡面去擷取這個View的寬/高就行了,讀者可以自行試一下,實際上在onCreate、onStart、onResume中均無法正確得View的寬/高資訊,這是因為View的measure過程和Activity的生命週期方法不是同步執行的,因此無法保證Activiy執行了onCreate、onStart、onResume時某個Vicw已經完畢了,如果View還沒有測量完畢,那麼獲得的寬/高就是0。有沒有什麼方法能解決問題呢?答案是有的,這裡給出四種方法來解決這個問題:。
  
  這個方法的含義是:View已經初始化完畢了,寬/高已經準備好了,這個時候去擷取寬/高是沒問題的。需要注意的是,會被調用多次,當Activity的視窗得到焦點和失去焦點時均會被調用一次。具體來說,當Activity繼續執行和暫停執行時,均會被調用,如果頻繁地進行onResume和onPause,那麼也會被頻繁地調用。典型代碼如下:}}}}12345678910111213141516171234567891011121314151617(2)
  
  通過post可以將一個runnable投遞到訊息佇列,然後等到Lopper調用runnable的時候,View也就初始化好了,典型代碼如下:}});}123456789101112123456789101112使用ViewTreeObserver的眾多回調可以完成這個功能,比如使用OnGlobalLayoutListener這個介面,當View樹的狀態發生改變或者View樹內部的View的可見度發生改變,onGlobalLayout方法就會回調,因此這是擷取View的寬高一個很好的例子,需要注意的是,伴隨著View樹狀態的改變,這個方法也會被調用多次,典型代碼如下}});}12345678910111213141234567891011121314通過手動測量View的寬高,這種方法比較複雜,這裡要分情況來處理,根據View的LayoutParams來處理直接放棄,無法測量出具體的寬高,根據View的測量過程,構造這種measureSpec需要知道parentSize,即父容器的剩下空間,而這個時候我們無法知道parentSize的大小,所以理論上我們不可能測量出View的大小具體的數值比如寬高都是100dp,那我們可以這樣:123123如下123123注意到(1<<30)-1, 通過分析MeasureSpec的實現可以知道,View的尺寸三十位的二進位表示,也就是說最大是30個1(2^30-1),也就是(1<30-1),在最大的模www.boyuanyl.cn式下,我們用View理論上能支援最大值去構造MwasureSpec是合理的關於View的measure,網路上有兩個錯誤的用法,為什麼說是錯誤的,首先其違背了系統的內部實現規範(因為無法通過錯誤的MeasureSpec去得出合理的SpecMode,從而導致measure過程出錯,其次不能保證mwasure出正確的結果)
  
  第一種錯誤的方法:123123第二種錯誤的用法11過程的作用是ViewGroup用來確定子項目的作用的,當ViewGroup的位置被確認之後,他的layout就會去遍曆所有子項目並且調用on方法,在layout方法中onLayou又被調用,layout的過程和measure過程相比就要簡單很多了,layout方法確定了View本身的位置,而on方法則會確定所有子項目的位置,先看View的layout方法}}}}}12345678910111213141516171819202122232425262728293031321234567891011121314151617181920212223242526272829303132的方法的大致流程如下,首先會通過一個setFrame方法來設定View的四個頂點的位置,即初始化mLeft,mTop,mRight,mBottom這四個值,View的四個頂點一旦確定,那麼View在父容器的位置也就確定了,接下來會調用方法,這個方法的用途是調用父容器確定子項目的位置,和onMeasure類似,的具體位置實現同樣和具體布局有關,所有View和ViewGroup均沒有真正的實現方www.lafei333.cn 法,我們來看一下LinearLayout的}}1234567812345678很好理解,是吧,,這個和onMeasure有點類似,我們拿layoutVertical來說,先看源碼:}}}}}}}1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818212345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182這裡分析一下layoutVertical的代碼邏輯,可以看到,此方法會遍曆所有子項目並調用setChildFrame方法來為子項目指定對應的位置,其中childTop會逐漸層大,這就意味著後面的子項目會被放置在靠下的位置,這剛好符合樹立方向的線性布局,至於setChildFrame,他僅僅是調用元素的layout方法而已,這樣的父容器在layout方法中完成自己的定位以後,就通過onLayout方法去調用,子項目又會通過自己的lawww.hsl85.cn/ yout方法來確定自己的位置,這樣一層一層傳遞下去完成整個View樹的layout過程,setChildFrame方法可以看:}123123我們注意到setChildFrame中的width和height實際上就是子項目測量寬高,從下面的代碼可以看出123123而在Layout方法中通過setFrame去設定子項目的四個頂點位置,方法中有這麼幾句:1234512345下面我們來回到之前的問題,View的測量寬高和最終寬高有什麼區別,這個問題現在可以具體回答了,View的getMeasureWidth和getWidth這兩個方法有什麼區別?至於getMeasureHeight和getHeight是完全一樣的,為了回答這個問題我們首先來看下getWidth和getHeight具體實現

Android藝術開發探索第四章——View的工作原理(上)

聯繫我們

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

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

A Free Trial That Lets You Build Big!

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

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

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

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