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

一:自定義View繪制流程函數調用鏈


二.幾個重要的函數

1.構造函數

構造函數是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();

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

來自GcsSloop

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


參考文檔:

公共技術點之 View 繪制流程

自定義View分類與流程

自定義View總結

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,363評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,497評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,305評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,962評論 1 311
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,727評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,193評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,257評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,411評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,945評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,777評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,978評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,519評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,216評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,657評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,960評論 2 373

推薦閱讀更多精彩內容