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