View框架淺析

參考文章如下,這幾篇文章很好,圖文並茂,我這裏只是取了一些原文的概念放到這裏方便複習:
http://www.jianshu.com/p/a3014f8442b0

整體View結構

控件主要分爲兩類,一類是View,一類是ViewGroup
如下是View的一些原理

  • 所有的View都是矩形的
  • View是不能添加子View的,ViewGroup可以
  • Activity之所以能加載並且控制View,是因爲它包含了一個Window,所有的圖形化界面都是由View顯示的而Service之所以稱之爲沒有界面的activity是因爲它不包含有Window,不能夠加載View;
  • 一個View有且只能有一個父View;
  • 在Android中Window對象通常由PhoneWindow來實現的,PhoneWindow將一個DecorView設置爲整個應用窗口的根View,即DecorView爲整個Window界面的最頂層View。
  • DecorView是FrameLayout的子類,它繼承了FrameLayout,即頂層的FrameLayout的實現類是Decorview,它是在phoneWindow裏面創建的;
  • 頂層的FrameLayout的父view是Handler,Handler的作用除了線程之間的通訊以外,還可以跟WindowManagerService進行通訊;windowManagerService是後臺的一個服務,它控制並且管理者屏幕;
  • 一個應用可以有很多個window,其由windowManager來管理,而windowManager又由windowManagerService來管理;

Measure測量一個View的大小

  • 1、MeasureSpe描述了父View對子View大小的期望。裏面包含了測量模式和大小。
  • 2、MeasureSpe類把測量模式和大小組合到一個32位的int型的數值中,其中高2位表示模式,低30位表示大小而在計算中使用位運算的原因是爲了提高並優化效率。
  • 3、Android中提供了三種測量模式,EXACTLY 精確模式,AT_MOST最大值模式,UNSPECIFIED不確定模式。默認的是去定的模式。
  • 4、系統最終會調用setMeasureDimension(int measuredWidth, int measuredHeight)方法將測量後的寬高設置進去,從而完成測量工作。
  • 5、當我們需要自定義的時候,需要自定義的measureWidth()方法和measureHeight()方法對寬高進行了重新定義。從MeasureSpec對象中獲取到測量模式和測量大小值,通過判斷測量模式,返回不同的測量值。

Layout擺放一個View的位置

  • 1、首先是layout函數, Layout方法中接受四個參數,是由父View提供,指定了子View在父View中的左、上、右、下的位置。父View在指定子View的位置時通常會根據子View在measure中測量的大小來決定。
  • 2、子View的位置通常還受有其他屬性左右,例如父View的orientation,gravity,自身的margin等等,特別是RelativeLayout,影響佈局的因素非常多。
  • 3、layout和onLayout之間有一個setFrame方法。setFrame方法是一個隱藏方法,所以作爲應用層程序員來說,無法重寫該方法。該方法體內部通過比對本次的l、t、r、b四個值與上次是否相同來判斷自身的位置和大小是否發生了改變。如果發生了改變,將會調用invalidate請求重繪。
  • 4、onLayout是ViewGroup用來決定子View擺放位置的,各種佈局的差異都在該方法中得到了體現。

Draw畫出View的顯示內容

View的繪製也是遵循一定的順序:

  • 1、畫背景
  • 2、畫邊緣
  • 3、畫自身: ondraw方法
  • 4、畫子View: dispatchDraw方法
  • 5、畫滾動條

draw()->onDraw()->dispatchDraw()

  • 1、draw是由ViewRoot的performTraversals方法發起,它將調用DecorView的draw方法,並把成員變量canvas傳給給draw方法。而在後面draw遍歷中,傳遞的都是同一個canvas。所以android的繪製是同一個window中的所有View都繪製在同一個畫布上。
  • 2、onDraw()方法的使用,因爲我們的目的就是自定義View,所以當我們測量好了一個View之後,我們就可以間的重寫onDraw()這個方法,並在Canvas對象上來繪製所需要的圖形。在onDraw()中就有一個參數,該參數就是Canvas canvas對象,使用這個對象即可進行繪圖操作。
  • 3、之所以要傳入一個bitmap,是因爲傳進來的bitmap與通過這個bitmap創建的Canvas畫布是緊緊聯繫在一起的,這個過程稱之爲裝載畫布。
  • 4、dispatchDraw
    先根據自身的padding剪裁畫布,所有的子View都將在畫布剪裁後的區域繪製。遍歷所有子View,調用子View的computeScroll對子View的滾動值進行計算。根據滾動值和子View在父View中的座標進行畫布原點座標的移動,根據子在父View中的座標計算出子View的視圖大小,然後對畫布進行剪裁。

View框架的measure機制

1.mesure幹了什麼

Android中View有自使用的機制,把各種尺寸值,經過計算,得到具體的像素值。measure過程會遍歷整棵View樹,然後依次測量每個View真實的尺寸。具體是每個ViewGroup會向它內部的每個子View發送measure命令,然後由具體子View的onMeasure()來測量自己的尺寸。最後測量的結果保存在View的mMeasuredWidth和mMeasuredHeight中,保存的數據單位是像素。

2.如何合理的測量一顆View樹

一個View需要把它內部的match_parent或者wrap_content轉換成具體的像素值。
在measure過程中,ViewGroup會根據自己當前的狀況,結合子View的尺寸數據,進行一個綜合評定,然後把相關信息告訴子View,然後子View在onMeasure自己的時候,一邊需要考慮到自己的content大小,一邊還要考慮的父佈局的限制信息,然後綜合評定,測量出一個最優的結果。

3.ViewGroup是如何向子View傳遞限制信息

談到傳遞限制信息,那就是MeasureSpec類了,該類貫穿於整個measure過程,用來傳遞父佈局對子View尺寸測量的約束信息。簡單來說,該類就保存兩類數據。
1、子View當前所在父佈局的具體尺寸。
2、父佈局對子View的限制類型。
還是包括那三種類型精確、最大和適應

4.源代碼的分析

我們知道,整棵View樹的根節點是DecorView,它是一個FrameLayout,所以它是一個ViewGroup,所以整棵View樹的測量是從一個ViewGroup對象的measure方法開始的。

View的測量過程

measure->onMeasure->setMeasuredDimension

(1)measure
該方法會調用onMeasure()方法,所以只有onMeasure能被也必須要被override。public final void measure(int widthMeasureSpec, int heightMeasureSpec);
父佈局會在自己的onMeasure方法中,調用child.measure ,這就把measure過程轉移到了子View中。

(2)onMeasure
具體測量過程,測量view和它的內容,來決定測量的寬高(mMeasuredWidth mMeasuredHeight )。該方法中必須要調用setMeasuredDimension(int, int)來保存該view測量的寬高。

(3)setMeasuredDimension
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight);
當View測量結束後,把測量結果保存起來,具體保存在mMeasuredWidth和mMeasuredHeight中。

ViewGroup的測量過程

measureChildren->measureChild->measureChildWithMargins->getChildMeasureSpec

(1)measureChildren
讓所有子view測量自己的尺寸,需要考慮當前ViewGroup的MeasureSpec和Padding。跳過狀態爲gone的子view

(2)measureChild
測量單個View,需要考慮當前ViewGroup的MeasureSpec和Padding。

(3)measureChildWithMargins
測量單個View,需要考慮當前ViewGroup的MeasureSpec和Padding、margins。

(4)getChildMeasureSpec
measureChildren過程中最困難的一部分,爲child計算MeasureSpec。該方法爲每個child的每個維度(寬、高)計算正確的MeasureSpec。目標就是把當前viewgroup的MeasureSpec和child的LayoutParams結合起來,生成最合理的結果。

getChildMeasureSpec的過程分析

根據當前自身的狀況,以及特定子View的尺寸參數,爲特定子View計算一個合理的限制信息

  • 首先判斷限定信息的模式
  • 如果是精確的模式,如果子容器申請的是固定尺寸,就用這個固定尺寸,如果子容器希望和父容器一樣大,就使用父容器的尺寸,還有就殺包裹內容啦。
  • 最大尺寸模式
  • 當前容器尺寸不限定模式

自定義View控件

需要覆寫onMeasure來正確測量自己。最後都需要調用setMeasuredDimension來保存測量結果
一般來說,自定義View的measure過程僞代碼爲:

  • 首先根據measureSpc來獲得mode和size
  • 根據不同的模式來設置不同的值,如果當前是精確模式直接設置大小即可,如果是最大值模式,就設置爲內容尺寸和父佈局尺寸的最小值。
    如果是不限定模式,那麼當前值有多大就設置爲多大。
  • 然後通過setMeasureDimension(viewSize)來設置

自定義ViewGroup控件

不但需要覆寫onMeasure來正確測量自己,可能還要覆寫一系列measureChild方法,來正確的測量子view,比如ScrollView。或者乾脆放棄父類實現的measureChild規則,自己重新實現一套測量子view的規則,比如RelativeLayout。最後都需要調用setMeasuredDimension來保存測量結果。

  • ViewGroup開始測量自己的尺寸
  • ViewGroup爲每個Child計算限定信息
  • 將上一步的限定信息傳遞給子View,然後子View需要measure自己的尺寸
  • 子View測量完成後,ViewGroup就可以獲取每個子View測量後的尺寸
  • ViewGroup會根據自己的狀況計算自己的尺寸
  • ViewGroup保存自己的尺寸

View框架的layout機制

什麼是layout過程

就是給View找到合適的位置
該位置是View相對於父佈局座標系的相對位置,而不是以屏幕座標系爲準的絕對位置。這樣更容易保持樹型結構的遞歸性和內部自治性。而View的位置,可以無限大,超出當前ViewGroup的可視範圍,這也是通過改變View位置而實現滑動效果的原理。

layout過程幹了什麼

  • 由於View是以樹結構進行存儲,所以典型的數據操作就是遞歸操作,所以,View框架中,採用了內部自治的layout過程。

  • 每個葉子節點根據父節點傳遞過來的位置信息,設置自己的位置數據,每個非葉子節點,除了負責根據父節點傳遞過來的位置信息,設置自己的位置數據外(如果有父節點的話),還需要根據自己內部的layout規則(比如垂直排布等),計算出每一個子節點的位置信息,然後向子節點傳遞layout過程。

  • 對於ViewGroup,除了根據自己的parent傳遞的位置信息,來設置自己的位置之外,還需要根據自己的layout規則,爲每一個子View計算出準確的位置(相對於子View的父佈局的位置)。

  • View對象的位置信息,在內部是以4個成員變量的保存的,分別是mLeft、mRight、mTop、mBottom。他們的含義如圖所示。

源代碼

protected void onLayout(boolean changed, int left, int top, int right, int bottom);

ViewGroup中,只需要覆寫onLayout方法,來計算出每一個子View的位置,並且把layout流程傳遞給子View。

  • 在onLayout中首先遍歷子View
  • 然後在遍歷的for循環中計算每一個子View的位置信息
  • 計算的規則包括當前佈局規則/子View 的測量尺寸/子View所在的位置索引
  • 通過child.layout來設置上面的位置信息

結論

一般來說,自定義View,如果該View不包含子View,類似於TextView這種的,是不需要覆寫onLayout方法的。而含有子View的,比如LinearLayout這種,就需要根據自己的佈局規則,來計算每一個子View的位置。


View框架的draw

什麼是layout過程

View框架中,draw過程主要是繪製View的外觀。ViewGroup除了負責繪製自己之外,還需要負責繪製所有的子View。而不含子View的View對象,就負責繪製自己就可以了。

draw過程的主要流程

  • 繪製 backgroud(drawBackground)
  • 如果需要的話,保存canvas的layer,來準備fading(不是必要的步驟)
  • 繪製view的content(onDraw方法)
  • 繪製children(dispatchDraw方法)
  • 如果需要的話,繪製fading edges,然後還原layer(不是必要的步驟)
  • 繪製裝飾器、比如scrollBar(onDrawForeground)

源代碼

我們知道,整棵View樹的根節點是DecorView,它是一個FrameLayout,所以它是一個ViewGroup,所以整棵View樹的測量是從一個ViewGroup對象的draw方法開始的。

下面是draw方法的流程

  • 繪製background
  • 繪製View的content
  • 繪製children(dispatchView)
  • 繪製裝飾器
發佈了228 篇原創文章 · 獲贊 45 · 訪問量 27萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章