Android自定義View(一)--基礎知識

一:自定義View繪制流程函數(shù)調(diào)用鏈


二.幾個重要的函數(shù)

1.構(gòu)造函數(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();

具體可以參考坐標系這篇文章。

來自GcsSloop

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()方法,然后設為背景。


參考文檔:

公共技術(shù)點之 View 繪制流程

自定義View分類與流程

自定義View總結(jié)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內(nèi)容