學(xué)自定義View前必備知識(shí)

一、layoutInflater

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


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

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

LayoutInflater layoutInflater = LayoutInflater.from(context);

第二種:

LayoutInflater layoutInflater =(LayoutInflater) context

.getSystemService(Context.LAYOUT_INFLATER_SERVICE);


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

layoutInflater.inflate(resourceId, root);

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


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

3.1

這個(gè)布局文件的內(nèi)容非常簡單,只有一個(gè)空的LinearLayout,里面什么控件都沒有,因此界面上應(yīng)該不會(huì)顯示任何東西。

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

3.2

這個(gè)布局文件也非常簡單,只有一個(gè)Button按鈕而已。現(xiàn)在我們要想辦法,如何通過LayoutInflater來將button_layout這個(gè)布局添加到主布局文件的LinearLayout中。根據(jù)剛剛介紹的用法,修改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);//獲取實(shí)例

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

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

}

}

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


3.3

Button在界面上顯示出來了!說明我們確實(shí)是借助LayoutInflater成功將button_layout這個(gè)布局添加到LinearLayout中了。LayoutInflater技術(shù)廣泛應(yīng)用于需要?jiǎng)討B(tài)添加View的時(shí)候,比如在ScrollView和ListView中,經(jīng)常都可以看到LayoutInflater的身影。


4.設(shè)置子布局大小

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

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

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


4.1

可以看到,這里我們又加入了一個(gè)RelativeLayout,此時(shí)的Button存在與RelativeLayout之中,layout_width和layout_height屬性也就有作用了。當(dāng)然,處于最外層的RelativeLayout,它的layout_width和layout_height則會(huì)失去作用?,F(xiàn)在重新運(yùn)行一下程序,結(jié)果如下圖所示:


4.2

看到這里,也許有些朋友心中會(huì)有一個(gè)巨大的疑惑。不對(duì)呀!平時(shí)在Activity中指定布局文件的時(shí)候,最外層的那個(gè)布局是可以指定大小的呀,layout_width和layout_height都是有作用的。確實(shí),這主要是因?yàn)椋趕etContentView()方法中,Android會(huì)自動(dòng)在布局文件的最外層再嵌套一個(gè)FrameLayout,所以layout_width和layout_height屬性才會(huì)有效果。

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

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


4.3


二、視圖繪制流程

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

要知道,任何一個(gè)視圖都不可能憑空突然出現(xiàn)在屏幕上,它們都是要經(jīng)過非??茖W(xué)的繪制流程后才能顯示出來的。每一個(gè)視圖的繪制過程都必須經(jīng)歷三個(gè)最主要的階段,即onMeasure()(測量)、onLayout()onDraw(),下面我們逐個(gè)對(duì)這三個(gè)階段展開進(jìn)行探討。


1. onMeasure() 測量方法

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

一個(gè)界面的展示可能會(huì)涉及到很多次的measure,因?yàn)橐粋€(gè)布局中一般都會(huì)包含多個(gè)子視圖,每個(gè)視圖都需要經(jīng)歷一次measure過程。ViewGroup中定義了一個(gè)measureChildren()方法來去測量子視圖的大小

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

public class MyView extends View {

......

@Override

protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec) {

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

}

}

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

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


2、onLayout() 進(jìn)行布局方法

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

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

@Override

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

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

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

2.1

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

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

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

2.2

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


2.3

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

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


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

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


3.1

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

就這么簡單,一個(gè)自定義的視圖就已經(jīng)寫好了,現(xiàn)在可以在XML中加入這個(gè)視圖,如下所示:


3.2

將MyView的寬度設(shè)置成200dp,高度設(shè)置成100dp,然后運(yùn)行一下程序,結(jié)果如下圖所示:


3.3


三、Android 視圖狀態(tài)及重繪

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


1、視圖狀態(tài)

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

(1). enabled

表示當(dāng)前視圖是否可用??梢哉{(diào)用setEnable()方法來改變視圖的可用狀態(tài),傳入true表示可用,傳入false表示不可用。它們之間最大的區(qū)別在于,不可用的視圖是無法響應(yīng)onTouch事件的。

(2). focused

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

(3). window_focused

表示當(dāng)前視圖是否處于正在交互的窗口中,這個(gè)值由系統(tǒng)自動(dòng)決定,應(yīng)用程序不能進(jìn)行改變。

(4). selected

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

(5). pressed

表示當(dāng)前視圖是否處于按下狀態(tài)??梢哉{(diào)用setPressed()方法來對(duì)這一狀態(tài)進(jìn)行改變,傳入true表示按下,傳入false表示未按下。通常情況下這個(gè)狀態(tài)都是由系統(tǒng)自動(dòng)賦值的,但開發(fā)者也可以自己調(diào)用這個(gè)方法來進(jìn)行改變。

我們可以在項(xiàng)目的drawable目錄下創(chuàng)建一個(gè)selector文件,在這里配置每種狀態(tài)下視圖對(duì)應(yīng)的背景圖片。比如創(chuàng)建一個(gè)compose_bg.xml文件,在里面編寫如下代碼:


1.1

這段代碼就表示,當(dāng)視圖處于正常狀態(tài)的時(shí)候就顯示compose_normal這張背景圖,當(dāng)視圖獲得到焦點(diǎn)或者被按下的時(shí)候就顯示compose_pressed這張背景圖。


2、視圖重繪

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

調(diào)用視圖的setVisibility()、setEnabled()、setSelected()等方法時(shí)都會(huì)導(dǎo)致視圖重繪,而如果我們想要手動(dòng)地強(qiáng)制讓視圖進(jìn)行重繪,可以調(diào)用invalidate()方法來實(shí)現(xiàn)。當(dāng)然了,setVisibility()、setEnabled()、setSelected()等方法的內(nèi)部其實(shí)也是通過調(diào)用invalidate()方法來實(shí)現(xiàn)的,那么就讓我們來看一看invalidate()方法的代碼是什么樣的吧。invalidate實(shí)際上調(diào)用了視圖繪制的入口函數(shù):performTraversals()方法。

另外需要注意的是,invalidate()方法雖然最終會(huì)調(diào)用到performTraversals()方法中,但這時(shí)measure和layout流程是不會(huì)重新執(zhí)行的,因?yàn)橐晥D沒有強(qiáng)制重新測量的標(biāo)志位,而且大小也沒有發(fā)生過變化,所以這時(shí)只有draw流程可以得到執(zhí)行。而如果你希望視圖的繪制流程可以完完整整地重新走一遍,就不能使用invalidate()方法,而應(yīng)該調(diào)用requestLayout()了。這個(gè)方法中的流程比invalidate()方法要簡單一些,但中心思想是差不多的,這里也就不再詳細(xì)進(jìn)行分析了。



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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容