一、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,代碼如下所示:
這個布局文件的內容非常簡單,只有一個空的LinearLayout,里面什么控件都沒有,因此界面上應該不會顯示任何東西。
那么接下來我們再定義一個布局文件,給它取名為button_layout.xml,代碼如下所示:
這個布局文件也非常簡單,只有一個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中。
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的外面再嵌套一層布局,如下所示:
可以看到,這里我們又加入了一個RelativeLayout,此時的Button存在與RelativeLayout之中,layout_width和layout_height屬性也就有作用了。當然,處于最外層的RelativeLayout,它的layout_width和layout_height則會失去作用。現在重新運行一下程序,結果如下圖所示:
看到這里,也許有些朋友心中會有一個巨大的疑惑。不對呀!平時在Activity中指定布局文件的時候,最外層的那個布局是可以指定大小的呀,layout_width和layout_height都是有作用的。確實,這主要是因為,在setContentView()方法中,Android會自動在布局文件的最外層再嵌套一個FrameLayout,所以layout_width和layout_height屬性才會有效果。
說到這里,雖然setContentView()方法大家都會用,但實際上Android界面顯示的原理要比我們所看到的東西復雜得多。任何一個Activity中顯示的界面其實主要都由兩部分組成,標題欄和內容布局。標題欄就是在很多界面頂部顯示的那部分內容,比如剛剛我們的那個例子當中就有標題欄,可以在代碼中控制讓它是否顯示。而內容布局就是一個FrameLayout,這個布局的id叫作content,我們調用setContentView()方法時所傳入的布局其實就是放到這個FrameLayout中的,這也是為什么這個方法名叫作setContentView(),而不是叫setView()。
最后再附上一張Activity窗口的組成圖吧,以便于大家更加直觀地理解:
二、視圖繪制流程
相信每個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吧,代碼如下所示:
代碼非常的簡單,我們來看下具體的邏輯吧。你已經知道,onMeasure()方法會在onLayout()方法之前調用,因此這里在onMeasure()方法中判斷SimpleLayout中是否有包含一個子視圖,如果有的話就調用measureChild()方法來測量出子視圖的大小。
接著在onLayout()方法中同樣判斷SimpleLayout是否有包含一個子視圖,然后調用這個子視圖的layout()方法來確定它在SimpleLayout布局中的位置,這里傳入的四個參數依次是0、0、childView.getMeasuredWidth()和childView.getMeasuredHeight(),分別代表著子視圖在SimpleLayout中左上右下四個點的坐標。其中,調用childView.getMeasuredWidth()和childView.getMeasuredHeight()方法得到的值就是在onMeasure()方法中測量出的寬和高。
這樣就已經把SimpleLayout這個布局定義好了,下面就是在XML文件中使用它了,如下所示:
可以看到,我們能夠像使用普通的布局文件一樣使用SimpleLayout,只是注意它只能包含一個子視圖,多余的子視圖會被舍棄掉。這里SimpleLayout中包含了一個ImageView,并且ImageView的寬高都是wrap_content。現在運行一下程序,結果如下圖所示:
OK!ImageView成功已經顯示出來了,并且顯示的位置也正是我們所期望的。如果你想改變ImageView顯示的位置,只需要改變childView.layout()方法的四個參數就行了。
這里注意:4個參數 可以這樣理解 ?離某一邊(left top bottom right)的距離?
3、onDraw() ?了解畫筆類和畫布類
measure和layout的過程都結束后,接下來就進入到draw的過程了。同樣,根據名字你就能夠判斷出,在這里才真正地開始對視圖進行繪制。ViewRoot中的代碼會繼續執行并創建出一個Canvas對象,然后調用View的draw()方法來執行具體的繪制工作。draw()方法內部的繪制過程總共可以分為六步,其中第二步和第五步在一般情況下很少用到,因此這里我們只分析簡化后的繪制過程。代碼如下所示:
可以看到,我們創建了一個自定義的MyView繼承自View,并在MyView的構造函數中創建了一個Paint對象。Paint就像是一個畫筆一樣,配合著Canvas就可以進行繪制了。這里我們的繪制邏輯比較簡單,在onDraw()方法中先是把畫筆設置成黃色,然后調用Canvas的drawRect()方法繪制一個矩形。然后在把畫筆設置成藍色,并調整了一下文字的大小(在分辨率高的手機上顯得小),然后調用drawText()方法繪制了一段文字。
就這么簡單,一個自定義的視圖就已經寫好了,現在可以在XML中加入這個視圖,如下所示:
將MyView的寬度設置成200dp,高度設置成100dp,然后運行一下程序,結果如下圖所示:
三、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文件,在里面編寫如下代碼:
這段代碼就表示,當視圖處于正常狀態的時候就顯示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