Android中的View繪製原理

該文章參考了
伯努力不努力(https://blog.csdn.net/u012124438/article/details/71435787)
Android_韋魯斯(https://blog.csdn.net/sinat_27154507/article/details/79748010)
黑暗終將過去(https://www.jianshu.com/p/3654ab59b908)
博主的文章,如有侵權請通知刪除

在介紹VIew繪製原理之前,簡單介紹一下Window,ViewRootImpl,DecorView之間的聯繫。

一個 Activity 包含一個Window,Window是一個抽象基類,是 Activity 和整個 View 系統交互的接口,只有一個實現子類PhoneWindow,提供了一系列窗口的方法,比如設置背景,標題等。一個PhoneWindow 對應一個DecorView 跟 一個 ViewRootImpl,DecorView 是ViewTree 裏面的頂層佈局,是繼承於FrameLayout,包含兩個子View,一個id=statusBarBackground 的 View 和 LineaLayout,LineaLayout 裏面包含 title 跟content,title就是平時用的TitleBar或者ActionBar, contenty也是FrameLayout,activity通過 setContent()加載佈局的時候加載到這個View上。ViewRootImpl就是建立 DecorView 和 Window 之間的聯繫。
在這裏插入圖片描述

那麼針對於View 繪製中measure,layout, draw 三個階段他們的入口是什麼呢?

measure :根據父 view 傳遞的 MeasureSpec 進行計算大小。
layout :根據 measure 子 View 所得到的佈局大小和佈局參數,將子View放在合適的位置上。
draw :把 View 對象繪製到屏幕上。

這三個階段的核心入口是在 ViewRootImpl 類的 performTraversals() 方法中。
performTraversals會依次調用performMeasure、performLayout和performDraw三個方法,這三個方法分別完成頂級View的measure、layout和draw這三大流程。其中performMeasure中會調用measure方法,在measure方法中又會調用onMeasure方法,在onMeasure方法中則會對所有子元素進行measure過程,這樣就完成了一次measure過程;子元素會重複父容器的measure過程,如此反覆完成了整個View數的遍歷。

private void performTraversals() {
    ......
    int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
    int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
    ......
    mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    ......
    mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
    ......
    mView.draw(canvas);
    ......
 }

在這裏插入圖片描述
在源碼中這個方法賊長,但是核心還是這三個步驟,就是判斷根據之前的狀態判斷是否需要重新 measure,是否需要重新 layout ,是否需要重新 draw。

measure過程決定了View的寬/高,完成後可通過getMeasuredWidth/getMeasureHeight方法來獲取View測量後的寬/高。
Layout過程決定了View的四個頂點的座標和實際View的寬高,完成後可通過getTop、getBotton、getLeft和getRight拿到View的四個定點座標。
Draw過程決定了View的顯示,完成後View的內容才能呈現到屏幕上。

下面我們就逐個進行分析,先從measure開始
measurespeac
在介紹 measure 方法之前,需要了解一個很核心的概念:measureSpeac 。在 Google 官方文檔中是這麼定義 measureSpeac 的

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.

大概意思是:MeasureSpec 封裝了從父View 傳遞給到子View的佈局需求。每個MeasureSpec代表寬度或高度的要求。每個MeasureSpec都包含了size(大小)和mode(模式)。
首先我們根據翻譯一句一句進行解析:
MeasureSpec 封裝了從父View 傳遞給到子View的佈局需求。

View 的 MeasureSpec 並不是父 View 獨自決定,它是根據父 view 的MeasureSpec加上子 View 的自己的LayoutParams,通過相應的規則轉化。
View 測量流程是父 View 先測量子 View,等子 View 測量完了,再來測量自己。在ViewGroup 測量子 View 的入口就是 measureChildWithMargins

總而言之,第一句話的含義就是父View的大小寬高不是隻由自身決定的,也需要根據子View來決定,這也就引出了上面第二三句話的,看懂了上面第二三句話,再結合自己平時的佈局文件就能明白了。
MeasureSpec代表寬度或高度的要求。每個MeasureSpec都包含了size(大小)和mode(模式)。

MeasureSpec 一個32位二進制的整數型,前面2位代表的是mode,後面30位代表的是size。mode 主要分爲3類,分別是

EXACTLY:父容器已經測量出子View的大小。對應是 View 的LayoutParams的match_parent 或者精確數值。
AT_MOST:父容器已經限制子view的大小,View 最終大小不可超過這個值。對應是 View 的LayoutParams的wrap_content
UNSPECIFIED:父容器不對View有任何限制,要多大給多大,這種情況一般用於系統內部,表示一種測量的狀態。(這種不怎麼常用,下面分析也會直接忽略這種情況)

onMeasure()
由於measure是final類型的,所以子類不能覆蓋,但是onMeasure方法可以被重寫,所以我們可以在onMeasure方法中重寫測量設置View尺寸。onMeasure也是測量View的核心代碼。
在這個方法中測量流程是會判斷如果父類傳遞過來的模式是否是MeasureSpec.UNSPECIFIED,如果是就會獲取到最小建議值,如果不是有返回值AT_MOST或者EXACTLY模式,則設置父類傳遞過來的大小。 然後調用setMeasuredDimension 方法進行存儲大小。

其次是layout方法
對 View 進行排版佈局,還是要看父 View,也就是 ViewGroup。
大致流程是判斷 View 是否在執行動畫,如果是在執行動畫,則等待動畫執行完調用 requestLayout (),如果沒有添加動畫或者動畫已經執行完了,則調用 layout(),也就是調用View的 layout()。
接下來就是跳入了View的layout方法中,設置 View 的在父 View 的位置,然後判斷位置是否發生變化,是否需要重新調用排版佈局,如果是需要重新佈局則用了 onLayout()方法。
onLayout
在OnLayout 方法中,View 裏面是一個空實現,而 ViewGroup 則是一個抽象方法。爲什麼這麼設計呢?因爲onLayout中主要就是爲了給遍歷View然後進行排版佈局,分別設置View在父View中的位置。既然如此,那麼View的意義就不大了,而ViewGruop 必須實現,不然沒法對子View進行佈局。那麼如何對 View 進行排版呢?
其實很簡單,就是ViewGruop會遍歷它裏面的所有View然後調用每個view 的layout(l,t,r,b)方法進行位置設置。

到了最後也是最重要的draw方法了!
在這裏插入圖片描述

draw 過程中一共分成7步,其中兩步我們直接直接跳過不分析了。

第一步:drawBackground(canvas): 作用就是繪製 View 的背景。

第三步:onDraw(canvas) :繪製 View 的內容。View 的內容是根據自己需求自己繪製的,所以方法是一個空方法,View的繼承類自己複寫實現繪製內容。

第三步:dispatchDraw(canvas):遍歷子View進行繪製內容。在 View 裏面是一個空實現,ViewGroup 裏面纔會有實現。在自定義 ViewGroup 一般不用複寫這個方法,因爲它在裏面的實現幫我們實現了子 View 的繪製過程,基本滿足需求。

第四步:onDrawForeground(canvas):對前景色跟滾動條進行繪製。

第五步:drawDefaultFocusHighlight(canvas):繪製默認焦點高亮
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章