學自定義View前必備知識

一、layoutInflater

概述:主要用于加載布局,其實setContentView()方法的內部也是使用LayoutInflater來加載布局的,只不過這部分源碼是internal的,不太容易查看到。


1.獲取實例 ?,首先需要獲取到LayoutInflater的實例,有兩種方法可以獲取到,

第一種寫法如下:(是第二種的封裝寫法)

LayoutInflater layoutInflater = LayoutInflater.from(context);

第二種:

LayoutInflater layoutInflater =(LayoutInflater) context

.getSystemService(Context.LAYOUT_INFLATER_SERVICE);


2.加載布局 ?然后得到了LayoutInflater的實例之后就可以調用它的inflate()方法來加載布局了 ?

layoutInflater.inflate(resourceId, root);

inflate()方法一般接收兩個參數,第一個參數就是要加載的布局id第二個參數是指給該布局的外部再嵌套一層父布局,如果不需要就直接傳null。這樣就成功成功創建了一個布局的實例,之后再將它添加到指定的位置就可以顯示出來了。


3.用法1(添加)?下面我們就通過一個非常簡單的小例子,來更加直觀地看一下LayoutInflater的用法。比如說當前有一個項目,其中MainActivity對應的布局文件叫做activity_main.xml,代碼如下所示:

3.1

這個布局文件的內容非常簡單,只有一個空的LinearLayout,里面什么控件都沒有,因此界面上應該不會顯示任何東西。

那么接下來我們再定義一個布局文件,給它取名為button_layout.xml,代碼如下所示:

3.2

這個布局文件也非常簡單,只有一個Button按鈕而已。現在我們要想辦法,如何通過LayoutInflater來將button_layout這個布局添加到主布局文件的LinearLayout中。根據剛剛介紹的用法,修改MainActivity中的代碼,如下所示:

public class MainActivity extends Activity?

{

private LinearLayout mainLayout;

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

mainLayout=(LinearLayout) findViewById(R.id.main_layout);

LayoutInflater layoutInflater= LayoutInflater.from(this);//獲取實例

View buttonLayout= layoutInflater.inflate(R.layout.button_layout,null);//加載布局

mainLayout.addView(buttonLayout);//添加布局

}

}

可以看到,這里先是獲取到了LayoutInflater的實例,然后調用它的inflate()方法來加載button_layout這個布局,最后調用LinearLayout的addView()方法將它添加到LinearLayout中。


3.3

Button在界面上顯示出來了!說明我們確實是借助LayoutInflater成功將button_layout這個布局添加到LinearLayout中了。LayoutInflater技術廣泛應用于需要動態添加View的時候,比如在ScrollView和ListView中,經常都可以看到LayoutInflater的身影。


4.設置子布局大小

這里我們將按鈕的寬度改成300dp,高度改成80dp,這樣夠大了吧?現在重新運行一下程序來觀察效果。咦?怎么按鈕還是原來的大小,沒有任何變化!是不是按鈕仍然不夠大,再改大一點呢?還是沒有用!

其實這里不管你將Button的layout_width和layout_height的值修改成多少,都不會有任何效果的,因為這兩個值現在已經完全失去了作用。平時我們經常使用layout_width和layout_height來設置View的大小,并且一直都能正常工作,就好像這兩個屬性確實是用于設置View的大小的。而實際上則不然,它們其實是用于設置View在布局中的大小的,也就是說,首先View必須存在于一個布局中,之后如果將layout_width設置成match_parent表示讓View的寬度填充滿布局,如果設置成wrap_content表示讓View的寬度剛好可以包含其內容,如果設置成具體的數值則View的寬度會變成相應的數值。這也是為什么這兩個屬性叫作layout_width和layout_height,而不是width和height。

再來看一下我們的button_layout.xml吧,很明顯Button這個控件目前不存在于任何布局當中,所以layout_width和layout_height這兩個屬性理所當然沒有任何作用。那么怎樣修改才能讓按鈕的大小改變呢?解決方法其實有很多種,最簡單的方式就是在Button的外面再嵌套一層布局,如下所示:


4.1

可以看到,這里我們又加入了一個RelativeLayout,此時的Button存在與RelativeLayout之中,layout_width和layout_height屬性也就有作用了。當然,處于最外層的RelativeLayout,它的layout_width和layout_height則會失去作用。現在重新運行一下程序,結果如下圖所示:


4.2

看到這里,也許有些朋友心中會有一個巨大的疑惑。不對呀!平時在Activity中指定布局文件的時候,最外層的那個布局是可以指定大小的呀,layout_width和layout_height都是有作用的。確實,這主要是因為,在setContentView()方法中,Android會自動在布局文件的最外層再嵌套一個FrameLayout,所以layout_width和layout_height屬性才會有效果。

說到這里,雖然setContentView()方法大家都會用,但實際上Android界面顯示的原理要比我們所看到的東西復雜得多。任何一個Activity中顯示的界面其實主要都由兩部分組成,標題欄和內容布局。標題欄就是在很多界面頂部顯示的那部分內容,比如剛剛我們的那個例子當中就有標題欄,可以在代碼中控制讓它是否顯示。而內容布局就是一個FrameLayout,這個布局的id叫作content,我們調用setContentView()方法時所傳入的布局其實就是放到這個FrameLayout中的,這也是為什么這個方法名叫作setContentView(),而不是叫setView()。

最后再附上一張Activity窗口的組成圖吧,以便于大家更加直觀地理解:


4.3


二、視圖繪制流程

相信每個Android程序員都知道,我們每天的開發工作當中都在不停地跟View打交道,Android中的任何一個布局、任何一個控件其實都是直接或間接繼承自View的,如TextView、Button、ImageView、ListView等。這些控件雖然是Android系統本身就提供好的,我們只需要拿過來使用就可以了,但你知道它們是怎樣被繪制到屏幕上的嗎?多知道一些總是沒有壞處的,那么我們趕快進入到本篇文章的正題內容吧。

要知道,任何一個視圖都不可能憑空突然出現在屏幕上,它們都是要經過非常科學的繪制流程后才能顯示出來的。每一個視圖的繪制過程都必須經歷三個最主要的階段,即onMeasure()(測量)onLayout()onDraw(),下面我們逐個對這三個階段展開進行探討。


1. onMeasure() 測量方法

measure是測量的意思,那么onMeasure()方法顧名思義就是用于測量視圖的大小的。

一個界面的展示可能會涉及到很多次的measure,因為一個布局中一般都會包含多個子視圖,每個視圖都需要經歷一次measure過程。ViewGroup中定義了一個measureChildren()方法來去測量子視圖的大小

當然,onMeasure()方法是可以重寫的,也就是說,如果你不想使用系統默認的測量方式,可以按照自己的意愿進行定制,比如:

public class MyView extends View {

......

@Override

protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec) {

setMeasuredDimension(200, 200);//設置測量尺寸

}

}

需要注意的是,在setMeasuredDimension()方法調用之后,我們才能使用getMeasuredWidth()getMeasuredHeight()獲取視圖測量出的寬高,以此之前調用這兩個方法得到的值都會是0。

由此可見,視圖大小的控制是由父視圖、布局文件、以及視圖本身共同完成的,父視圖會提供給子視圖參考的大小,而開發人員可以在XML文件中指定視圖的大小,然后視圖本身會對最終的大小進行拍板。


2、onLayout() 進行布局方法

measure過程結束后,視圖的大小就已經測量好了,接下來就是layout的過程了。正如其名字所描述的一樣,這個方法是用于給視圖進行布局的,也就是確定視圖的位置

View中的onLayout()方法就是一個空方法,因為onLayout()過程是為了確定視圖在布局中所在的位置,而這個操作應該是由布局來完成的,即父視圖決定子視圖的顯示位置。既然如此,我們來看下ViewGroup中的onLayout()方法是怎么寫的吧,代碼如下:

@Override

protected abstract void onLayout(boolean changed,int l,int t,int r,int b);

可以看到,ViewGroup中的onLayout()方法竟然是一個抽象方法,這就意味著所有ViewGroup的子類都必須重寫這個方法。沒錯,像LinearLayout、RelativeLayout等布局,都是重寫了這個方法,然后在內部按照各自的規則對子視圖進行布局的。由于LinearLayout和RelativeLayout的布局規則都比較復雜,就不單獨拿出來進行分析了,這里我們嘗試自定義一個布局,借此來更深刻地理解onLayout()的過程。

自定義的這個布局目標很簡單,只要能夠包含一個子視圖,并且讓子視圖正常顯示出來就可以了。那么就給這個布局起名叫做SimpleLayout吧,代碼如下所示:

2.1

代碼非常的簡單,我們來看下具體的邏輯吧。你已經知道,onMeasure()方法會在onLayout()方法之前調用,因此這里在onMeasure()方法中判斷SimpleLayout中是否有包含一個子視圖,如果有的話就調用measureChild()方法來測量出子視圖的大小。

接著在onLayout()方法中同樣判斷SimpleLayout是否有包含一個子視圖,然后調用這個子視圖的layout()方法來確定它在SimpleLayout布局中的位置,這里傳入的四個參數依次是0、0、childView.getMeasuredWidth()和childView.getMeasuredHeight(),分別代表著子視圖在SimpleLayout中左上右下四個點的坐標。其中,調用childView.getMeasuredWidth()和childView.getMeasuredHeight()方法得到的值就是在onMeasure()方法中測量出的寬和高。

這樣就已經把SimpleLayout這個布局定義好了,下面就是在XML文件中使用它了,如下所示:

2.2

可以看到,我們能夠像使用普通的布局文件一樣使用SimpleLayout,只是注意它只能包含一個子視圖,多余的子視圖會被舍棄掉。這里SimpleLayout中包含了一個ImageView,并且ImageView的寬高都是wrap_content。現在運行一下程序,結果如下圖所示:


2.3

OK!ImageView成功已經顯示出來了,并且顯示的位置也正是我們所期望的。如果你想改變ImageView顯示的位置,只需要改變childView.layout()方法的四個參數就行了。

這里注意:4個參數 可以這樣理解 ?離某一邊(left top bottom right)的距離?


3、onDraw() ?了解畫筆類和畫布類

measure和layout的過程都結束后,接下來就進入到draw的過程了。同樣,根據名字你就能夠判斷出,在這里才真正地開始對視圖進行繪制。ViewRoot中的代碼會繼續執行并創建出一個Canvas對象,然后調用View的draw()方法來執行具體的繪制工作。draw()方法內部的繪制過程總共可以分為六步,其中第二步和第五步在一般情況下很少用到,因此這里我們只分析簡化后的繪制過程。代碼如下所示:


3.1

可以看到,我們創建了一個自定義的MyView繼承自View,并在MyView的構造函數中創建了一個Paint對象。Paint就像是一個畫筆一樣,配合著Canvas就可以進行繪制了。這里我們的繪制邏輯比較簡單,在onDraw()方法中先是把畫筆設置成黃色,然后調用Canvas的drawRect()方法繪制一個矩形。然后在把畫筆設置成藍色,并調整了一下文字的大小(在分辨率高的手機上顯得小),然后調用drawText()方法繪制了一段文字。

就這么簡單,一個自定義的視圖就已經寫好了,現在可以在XML中加入這個視圖,如下所示:


3.2

將MyView的寬度設置成200dp,高度設置成100dp,然后運行一下程序,結果如下圖所示:


3.3


三、Android 視圖狀態及重繪

相信大家在平時使用View的時候都會發現它是有狀態的,比如說有一個按鈕,普通狀態下是一種效果,但是當手指按下的時候就會變成另外一種效果,這樣才會給人產生一種點擊了按鈕的感覺。當然了,這種效果相信幾乎所有的Android程序員都知道該如何實現,但是我們既然是深入了解View,那么自然也應該知道它背后的實現原理應該是什么樣的,今天就讓我們來一起探究一下吧。


1、視圖狀態

視圖狀態的種類非常多,一共有十幾種類型,不過多數情況下我們只會使用到其中的幾種,因此這里我們也就只去分析最常用的幾種視圖狀態。

(1). enabled

表示當前視圖是否可用。可以調用setEnable()方法來改變視圖的可用狀態,傳入true表示可用,傳入false表示不可用。它們之間最大的區別在于,不可用的視圖是無法響應onTouch事件的。

(2). focused

表示當前視圖是否獲得到焦點。通常情況下有兩種方法可以讓視圖獲得焦點,即通過鍵盤的上下左右鍵切換視圖,以及調用requestFocus()方法。而現在的Android手機幾乎都沒有鍵盤了,因此基本上只可以使用requestFocus()這個辦法來讓視圖獲得焦點了。而requestFocus()方法也不能保證一定可以讓視圖獲得焦點,它會有一個布爾值的返回值,如果返回true說明獲得焦點成功,返回false說明獲得焦點失敗。一般只有視圖在focusable和focusable in touch mode同時成立的情況下才能成功獲取焦點,比如說EditText。

(3). window_focused

表示當前視圖是否處于正在交互的窗口中,這個值由系統自動決定,應用程序不能進行改變。

(4). selected

表示當前視圖是否處于選中狀態。一個界面當中可以有多個視圖處于選中狀態,調用setSelected()方法能夠改變視圖的選中狀態,傳入true表示選中,傳入false表示未選中。

(5). pressed

表示當前視圖是否處于按下狀態。可以調用setPressed()方法來對這一狀態進行改變,傳入true表示按下,傳入false表示未按下。通常情況下這個狀態都是由系統自動賦值的,但開發者也可以自己調用這個方法來進行改變。

我們可以在項目的drawable目錄下創建一個selector文件,在這里配置每種狀態下視圖對應的背景圖片。比如創建一個compose_bg.xml文件,在里面編寫如下代碼:


1.1

這段代碼就表示,當視圖處于正常狀態的時候就顯示compose_normal這張背景圖,當視圖獲得到焦點或者被按下的時候就顯示compose_pressed這張背景圖。


2、視圖重繪

雖然視圖會在Activity加載完成之后自動繪制到屏幕上,但是我們完全有理由在與Activity進行交互的時候要求動態更新視圖,比如改變視圖的狀態、以及顯示或隱藏某個控件等。那在這個時候,之前繪制出的視圖其實就已經過期了,此時我們就應該對視圖進行重繪。

調用視圖的setVisibility()、setEnabled()、setSelected()等方法時都會導致視圖重繪,而如果我們想要手動地強制讓視圖進行重繪,可以調用invalidate()方法來實現。當然了,setVisibility()、setEnabled()、setSelected()等方法的內部其實也是通過調用invalidate()方法來實現的,那么就讓我們來看一看invalidate()方法的代碼是什么樣的吧。invalidate實際上調用了視圖繪制的入口函數:performTraversals()方法。

另外需要注意的是,invalidate()方法雖然最終會調用到performTraversals()方法中,但這時measure和layout流程是不會重新執行的,因為視圖沒有強制重新測量的標志位,而且大小也沒有發生過變化,所以這時只有draw流程可以得到執行。而如果你希望視圖的繪制流程可以完完整整地重新走一遍,就不能使用invalidate()方法,而應該調用requestLayout()了。這個方法中的流程比invalidate()方法要簡單一些,但中心思想是差不多的,這里也就不再詳細進行分析了。



整理自:http://www.cnblogs.com/yukino/p/4438919.html

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

推薦閱讀更多精彩內容