一:自定義View繪制流程函數調用鏈
二.幾個重要的函數
構造函數是View的入口,可以用于初始化一些的內容,和獲取自定義屬性。
View的構造函數有四種重載分別如下:
public void SloopView(Context context) {}
public void SloopView(Context context,AttributeSet attrs) {}
public void SloopView(Context context,AttributeSet attrs,int defStyleAttr) {}
public void SloopView(Context context,AttributeSet attrs,int defStyleAttr,int defStyleRes) {}
可以看出,關于View構造函數的參數有多有少,先排除幾個不常用的,留下常用的再研究。
有四個參數的構造函數在API21的時候才添加上,暫不考慮。
有三個參數的構造函數中第三個參數是默認的Style,這里的默認的Style是指它在當前Application或Activity所用的Theme中的默認Style,且只有在明確調用的時候才會生效,以系統中的ImageButton為例說明:
public ImageButton(Context context,AttributeSet attrs) {//調用了三個參數的構造函數,明確指定第三個參數this(context, attrs,com.android.internal.R.attr.imageButtonStyle);? ? }
publicImageButton(Context context,AttributeSet attrs, int defStyleAttr) {//此處調了四個參數的構造函數,無視即可this(context, attrs, defStyleAttr,0);? ? }
注意:即使你在View中使用了Style這個屬性也不會調用三個參數的構造函數,所調用的依舊是兩個參數的構造函數。
由于三個參數的構造函數第三個參數一般不用,暫不考慮,第三個參數的具體用法會在以后用到的時候詳細介紹。
排除了兩個之后,只剩下一個參數和兩個參數的構造函數,他們的詳情如下:
//一般在直接New一個View的時候調用。
public void SloopView(Context context) {}
//一般在layout文件中使用的時候會調用,關于它的所有屬性(包括自定義屬性)都會包含在attrs中傳遞進來。
public void SloopView(Context context,AttributeSet attrs) {}
以下方法調用的是一個參數的構造函數:
//在Avtivity中SloopViewview=newSloopView(this);
以下方法調用的是兩個參數的構造函數:
//在layout文件中 - 格式為: <包名.View名 />
關于構造函數先講這么多,關于如何自定義屬性和使用attrs中的內容,在后面會詳細講解,目前只需要知道這兩個構造函數在何時調用即可。
2.onAttachedToWindow():
運行在onResume()之后;此函數會調用Activity的onResume()生命周期,所以在onResume之后可以設置窗體尺寸。
3.Measure
具體分析
measure 過程由measure(int, int)方法發起,從上到下有序的測量 View,在 measure 過程的最后,每個視圖存儲了自己的尺寸大小和測量規格。 layout 過程由layout(int, int, int, int)方法發起,也是自上而下進行遍歷。在該過程中,每個父視圖會根據 measure 過程得到的尺寸來擺放自己的子視圖。
measure 過程會為一個 View 及所有子節點的 mMeasuredWidth 和 mMeasuredHeight 變量賦值,該值可以通過getMeasuredWidth()和getMeasuredHeight()方法獲得。而且這兩個值必須在父視圖約束范圍之內,這樣才可以保證所有的父視圖都接收所有子視圖的測量。如果子視圖對于 Measure 得到的大小不滿意的時候,父視圖會介入并設置測量規則進行第二次 measure。比如,父視圖可以先根據未給定的 dimension 去測量每一個子視圖,如果最終子視圖的未約束尺寸太大或者太小的時候,父視圖就會使用一個確切的大小再次對子視圖進行 measure。
measure 過程傳遞尺寸的兩個類
ViewGroup.LayoutParams (View 自身的布局參數)
MeasureSpecs 類(父視圖對子視圖的測量要求)
ViewGroup.LayoutParams
這個類我們很常見,就是用來指定視圖的高度和寬度等參數。對于每個視圖的 height 和 width,你有以下選擇:
具體值
MATCH_PARENT 表示子視圖希望和父視圖一樣大(不包含 padding 值)
WRAP_CONTENT 表示視圖為正好能包裹其內容大小(包含 padding 值)
ViewGroup 的子類有其對應的 ViewGroup.LayoutParams 的子類。比如 RelativeLayout 擁有的 ViewGroup.LayoutParams 的子類 RelativeLayoutParams。
有時我們需要使用 view.getLayoutParams() 方法獲取一個視圖 LayoutParams,然后進行強轉,但由于不知道其具體類型,可能會導致強轉錯誤。其實該方法得到的就是其所在父視圖類型的 LayoutParams,比如 View 的父控件為 RelativeLayout,那么得到的 LayoutParams 類型就為 RelativeLayoutParams。
measure 核心方法
measure(int widthMeasureSpec, int heightMeasureSpec)
該方法定義在View.java類中,為 final 類型,不可被復寫,但 measure 調用鏈最終會回調 View/ViewGroup 對象的onMeasure()方法,因此自定義視圖時,只需要復寫onMeasure()方法即可。
4.onMeasure(int widthMeasureSpec, int heightMeasureSpec)
該方法就是我們自定義視圖中實現測量邏輯的方法,該方法的參數是父視圖對子視圖的 width 和 height 的測量要求。在我們自身的自定義視圖中,要做的就是根據該 widthMeasureSpec 和 heightMeasureSpec 計算視圖的 width 和 height,不同的模式處理方式不同。
Q: 為什么要測量View大小?
A: View的大小不僅由自身所決定,同時也會受到父控件的影響,為了我們的控件能更好的適應各種情況,一般會自己進行測量。
測量View大小使用的是onMeasure函數,我們可以從onMeasure的兩個參數中取出寬高的相關數據:
@OverrideprotectedvoidonMeasure(intwidthMeasureSpec,intheightMeasureSpec{
int widthsize=MeasureSpec.getSize(widthMeasureSpec);//取出寬度的確切數值
int widthmode=MeasureSpec.getMode(widthMeasureSpec);//取出寬度的測量模式
int heightsize=MeasureSpec.getSize(heightMeasureSpec);//取出高度的確切數值
int heightmode=MeasureSpec.getMode(heightMeasureSpec);//取出高度的測量模式
}
setMeasuredDimension()
測量階段終極方法,在onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法中調用,將計算得到的尺寸,傳遞給該方法,測量階段即結束。該方法也是必須要調用的方法,否則會報異常。在我們在自定義視圖的時候,不需要關心系統復雜的 Measure 過程的,只需調用setMeasuredDimension()設置根據 MeasureSpec 計算得到的尺寸即可,你可以參考ViewPagerIndicator的 onMeasure 方法。
View.MeasureSpec中:
模式二進制數值描述
UNSPECIFIED00默認值,父控件沒有給子view任何限制,子View可以設置為任意大小。
EXACTLY01表示父控件已經確切的指定了子View的大小。
AT_MOST10表示子View具體大小沒有尺寸限制,但是存在上限,上限一般為父View大小。
源碼解析:ANDROID自定義視圖——onMeasure,MeasureSpec源碼 流程 思路詳解
5.確定View大小(onSizeChanged)
這個函數在視圖大小發生改變時調用。
Q: 在測量完View并使用setMeasuredDimension函數之后View的大小基本上已經確定了,那么為什么還要再次確定View的大小呢?
A: 這是因為View的大小不僅由View本身控制,而且受父控件的影響,所以我們在確定View大小的時候最好使用系統提供的onSizeChanged回調函數。
onSizeChanged如下:
@OverrideprotectedvoidonSizeChanged(intw,inth,intoldw,intoldh) {super.onSizeChanged(w, h, oldw, oldh);? ? }
可以看出,它又四個參數,分別為 寬度,高度,上一次寬度,上一次高度。
這個函數比較簡單,我們只需關注 寬度(w), 高度(h) 即可,這兩個參數就是View最終的大小。
6.確定子View布局位置(onLayout)
首先要明確的是,子視圖的具體位置都是相對于父視圖而言的。View 的 onLayout 方法為空實現,而 ViewGroup 的 onLayout 為 abstract 的,因此,如果自定義的 View 要繼承 ViewGroup 時,必須實現 onLayout 函數。
在 layout 過程中,子視圖會調用getMeasuredWidth()和getMeasuredHeight()方法獲取到 measure 過程得到的 mMeasuredWidth 和 mMeasuredHeight,作為自己的 width 和 height。然后調用每一個子視圖的layout(l, t, r, b)函數,來確定每個子視圖在父視圖中的位置。
確定布局的函數是onLayout,它用于確定子View的位置,在自定義ViewGroup中會用到,他調用的是子View的layout函數。
在自定義ViewGroup中,onLayout一般是循環取出子View,然后經過計算得出各個子View位置的坐標值,然后用以下函數設置子View位置。
child.layout(l, t, r, b);
四個參數分別為:
名稱說明對應的函數
lView左側距父View左側的距離getLeft();
tView頂部距父View頂部的距離getTop();
rView右側距父View左側的距離getRight();
bView底部距父View頂部的距離getBottom();
具體可以參考坐標系這篇文章。
7.draw繪制流程相關概念及核心方法
View.draw(Canvas canvas): 由于 ViewGroup 并沒有復寫此方法,因此,所有的視圖最終都是調用 View 的 draw 方法進行繪制的。在自定義的視圖中,也不應該復寫該方法,而是復寫onDraw(Canvas)方法進行繪制,如果自定義的視圖確實要復寫該方法,那么請先調用super.draw(canvas)完成系統的繪制,然后再進行自定義的繪制。
View.onDraw():
View 的onDraw(Canvas)默認是空實現,自定義繪制過程需要復寫的方法,繪制自身的內容。
drawChild(canvas, this, drawingTime)
直接調用了 View 的child.draw(canvas, this,drawingTime)方法,文檔中也說明了,除了被ViewGroup.drawChild()方法外,你不應該在其它任何地方去復寫或調用該方法,它屬于 ViewGroup。而View.draw(Canvas)方法是我們自定義控件中可以復寫的方法,具體可以參考上述對view.draw(Canvas)的說明。從參數中可以看到,child.draw(canvas, this, drawingTime)肯定是處理了和父視圖相關的邏輯,但 View 的最終繪制,還是View.draw(Canvas)方法。
invalidate()
請求重繪 View 樹,即 draw 過程,假如視圖發生大小沒有變化就不會調用layout()過程,并且只繪制那些調用了invalidate()方法的 View。
requestLayout()
當布局變化的時候,比如方向變化,尺寸的變化,會調用該方法,在自定義的視圖中,如果某些情況下希望重新測量尺寸大小,應該手動去調用該方法,它會觸發measure()和layout()過程,但不會進行 draw。
dispatchDraw() 發起對子視圖的繪制。View 中默認是空實現,ViewGroup 復寫了dispatchDraw()來對其子視圖進行繪制。該方法我們不用去管,自定義的 ViewGroup 不應該對dispatchDraw()進行復寫。
Android的view組件顯示主要經過mesure, layout和draw這三個過程。在mesure階段里調用mesure(int widthSpec, int heightSpec)方法,這個方法是final不能被重寫,在這個過程里會調用onMesure(int widthSpec, int heightSpec)方法。當組件設置好大小后,調用final layout(int l, int t, int r, int b)方法進行布局,在這個過程里會調用onLayout(boolean changed, int l, int t, int r, int b)方法,所以處理組件的布局通常要重寫onMesure和onLayout這兩個方法。
View組件的繪制會調用draw(Canvas canvas)方法,這個方法在源代碼里看不到在哪里調用...draw過程中主要是先畫Drawable背景,對drawable調用setBounds()然后是draw(Canvas c)方法.有點注意的是背景drawable的實際大小會影響view組件的大小,drawable的實際大小通過getIntrinsicWidth()和getIntrinsicHeight()獲取,當背景比較大時view組件大小等于背景drawable的大小,不過俺沒有在源代碼里找到布局時調用過getIntrinsicWidth()和getIntrinsicHeight()方法...
畫完背景后,draw過程會調用onDraw(Canvas canvas)方法,然后就是dispatchDraw(Canvas canvas)方法, dispatchDraw()主要是分發給子組件進行繪制,我們通常定制組件的時候重寫的是onDraw()方法。值得注意的是ViewGroup容器組件的繪制,當它沒有背景時直接調用的是dispatchDraw()方法, 而繞過了draw()方法,當它有背景的時候就調用draw()方法,而draw()方法里包含了dispatchDraw()方法的調用。因此要在ViewGroup上繪制東西的時候往往重寫的是dispatchDraw()方法而不是onDraw()方法,或者自定制一個Drawable,重寫它的draw(Canvas c)和getIntrinsicWidth(),
getIntrinsicHeight()方法,然后設為背景。
參考文檔: