解讀Android之Fragment
本文翻譯自android官方文件,結合自己測試,整理如下。
概述
Fragment在activity中表示一個行為或者UI的一部分。我們可以在一個activity中使用多個fragments或者在多個activities複用一個fragment。我們可以把fragment作為activity模組化的一部分,fragment有自己的生命週期,UI佈局。我們可以在activity執行時動態新增或刪除fragment。
fragment必須要嵌入到activity中,fragment的生命週期受到宿主activity的影響。例如,當activity處於暫停狀態時,在activity中的所有fragments也進入暫停狀態,當activity銷燬時,activity中所有的fragments也全部銷燬。但是,當activity處於執行狀態時,我們可以單獨處理每個fragment,例如我們可以新增或者刪除它們。當我們執行完一個fragment事務時,我們也可以將fragment新增到Back棧中,該Back棧由宿主activity管理,棧中的實體記錄的是fragment發生的事務操作。有了Back棧,我們就可以通過按BACK鍵回到fragment事務發生之前的狀態。
當我們向activity佈局中新增fragment時,它儲存在ViewGroup中,該ViewGroup將新增到activity檢視的層次結構中,fragment可以定義自己的佈局檔案。可以在佈局檔案中通過<fragment>
標籤新增fragment,或者通過程式碼新增到存在的ViewGroup中。但是fragment不是必須作為activity佈局的一部分;我們仍然可以使用沒有佈局的fragment處理activity不用顯示的任務。
下面將具體介紹fragment的詳細用法。
設計哲學
Android從3.0(API11)開始引入fragments,主要是為了在大螢幕上支援更加動態和靈活的UI設計,例如平板。因為平板的螢幕遠大於手持裝置(手機等),有更大的空間來存放和互動UI元件。Fragments能夠自動處理view層次變化的設計,不用我們來管理。通過將activity佈局分成fragments,我們能夠在activity執行時改變activity的呈現狀態,並且可以在Back棧中管理這些改變,該Back棧由宿主activity管理。
例如,新聞應用程式可以在左邊使用一個fragment顯示標題列表,而在右邊使用另一個fragment用於顯示具體新聞內容。這兩個fragments都在同一個activity中,一左一右,每個fragment有自己的生命週期方法並且處理自己的輸入事件。這樣的話我們可以避免使用兩個activities達到這種效果。如下圖所示:
我們應該把fragment設計成模組化可重用的activity元件。這是因為每個fragment可以定義自己的佈局,有自己的生命週期,我們可以在多個activities中使用同一個fragment,因此我們應該設計成可重用的避免在fragment中直接操作另一個fragment。模組化的fragment可以允許我們根據不同的螢幕大小改變fragments之間的組合。當設計程式同時支援平板和手機時,我們可以在不同的佈局配置中重用我們的fragments,以此來適應不同的螢幕空間,提高使用者體檢質量。例如上面的新聞客戶端的例子。對於大屏的平板來說可以在一個activity中同時顯示新聞標題和新聞內容這兩個fragment,而對於手機來說,一個fragment只能放在一個activity中。
建立fragment
為了建立一個fragment,我們必須建立一個Fragment類(或者它的子類)的子類。Fragment中的生命週期方法和Activity有很多相同的地方。例如fragment也包含:onCreate()
,onStart()
,onPause()
,onStop()
等。實際上,若我們想要把現有的應用程式使用fragment的話,我們只需要將activity的回撥方法中的程式碼移到對應的fragment的回撥方法中即可。
通常情況下,我們至少要實現一下方法:
-onCreate()
當建立fragment時系統會呼叫該方法。我們應該在這裡初始化必要的元件,這些元件當fragment進入暫停或停止之後重新啟動後仍被保留。
- onCreateView()
當首次為frgment準備UI佈局時,系統呼叫該方法。為了給fragment繪畫UI佈局,該方法必須返回一個View物件作為fragment佈局的根。若fragment不提供佈局的話,該方法返回null。
- onPause()
當用戶將要離開該fragment時(離開但不一定銷燬該fragment),系統呼叫該方法。通常我們需要在這裡儲存那些需要永久儲存的資訊,這是因為使用者可能不在回來。
關於生命週期中其它的方法我們將在後續處理fragment生命週期中詳細介紹。
除了基類Fragment之外,系統還有我們提供了幾個繼承Fragment的子類:
- DialogFrament
顯示為對話方塊形式。 - ListFragment
顯示為專案列表。 - PreferenceFragment
作為一個列表顯示Preference物件的層次結構,類似PreferenceActivity。當在程式中建立設定這樣的activity這是非常有用的。
新增UI
fragment通常被作為activity UI的一部分,fragment將自己的佈局貢獻給activity。
為了提供fragment的佈局,我們必須實現onCreateView()
,該方法會在系統開始建立該fragment的佈局時由系統呼叫。同時該方法必須返回一個View作為我們frament的佈局的根檢視。
注意:若我們的fragment是ListFragment的子類,則該方法預設會返回一個ListView,因此不用我們來實現該方法。
為了從onCreateView()
返回一個佈局,我們利用xml中定義的佈局檔案,為了達到這個目的,在onCreateView()
提供了LayoutInflater物件引數。例如:
public static class ExampleFragment extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.example_fragment, container, false);
}
}
其中,引數ViewGroup物件表示父檢視(來自activity佈局),我們的fragment佈局需要插入到該佈局中。Bundle物件用於儲存之前執行的fragment狀態,若之前不存在則返回null。
LayoutInflater物件的inflate()
方法傳遞三個引數:
- 載入的佈局檔案ID。
- 載入的佈局的父檢視,即該父檢視作為fragment佈局的根佈局。
- boolean表示在載入的過程中載入的佈局是否依賴父佈局(第二個引數)。這裡需要傳遞false,這是因為系統已經將載入的佈局插入到ViewGroup物件中;若傳遞true,則在最終的佈局中會建立一個冗餘的檢視。
通過以上介紹我們知道了如何建立一個帶有佈局的fragment,下面,我們介紹如何將fragment新增到activity中。
將fragment新增到activity
通常,fragment作為宿主activity佈局的一部分,有兩種方法將fragment新增到activity中:
靜態載入
該方法通過在activity佈局檔案中宣告fragment控制元件。這種情況下我們可以像指定檢視一樣指定fragment的佈局屬性。例如下面聲明瞭帶有兩個fragments的activity佈局:<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="match_parent"> <fragment android:name="com.example.news.ArticleListFragment" android:id="@+id/list" android:layout_weight="1" android:layout_width="0dp" android:layout_height="match_parent" /> <fragment android:name="com.example.news.ArticleReaderFragment" android:id="@+id/viewer" android:layout_weight="2" android:layout_width="0dp" android:layout_height="match_parent" /> </LinearLayout>
在
<fragment>
標籤中,android:name
屬性指定需要例項化的Fragment類。當系統建立activity佈局時,它例項化在activity佈局中指定的每個fragment,通過
onCreateView()
檢索相應的佈局,並將這些佈局插入到對應<fragement>
標籤所在的位置。注意:每個fragment需要提供一個唯一識別符號,通過該識別符號系統可以儲存這些fragment(在重新啟動activity時)。有下列三種方式指定:
1. 在<fragment>
標籤中通過android:id
指定一個ID;
2. 在<fragment>
標籤中通過android:tag
指定一個字串;
3. 若以上兩種都不指定的話,系統使用父控制元件(inflate()
方法的第二個引數)的ID。動態載入
該方法是指將fragment動態地新增到一個存在的ViewGroup中。在activity執行的任何時候,我們能夠將fragment新增到activity中。我們只需要指定一個被fragment替換的ViewGroup物件即可。
若想在activity中進行fragment事務操作(事務是指原子操作,一次事務要麼都執行要麼都不執行。fragment事務操作如:新增,刪除,替換fragment),我們能夠使用FragmentTransaction類,可以在activity如下操作:FragmentManager fragmentManager = getFragmentManager(); FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
我們可以使用
add()
方法新增fragment,add()
方法接收兩個引數,分別為:插入到的父佈局和要新增的fragment物件。如下:ExampleFragment fragment = new ExampleFragment(); fragmentTransaction.add(R.id.fragment_container, fragment); fragmentTransaction.commit();
每次使用FragmentTransaction物件執行事務之後都需要呼叫
commit()
提交事務執行。
新增無UI佈局的fragment
上面介紹瞭如何通過fragment向activity中提供UI佈局。然而,我們也可以使用不帶佈局的fragment,該fragment可以提供後臺服務。
為了新增無UI佈局的fragment,我們可以在activity中使用add(Fragment,String)
(String物件為fragment的tag屬性)新增fragment。由於該fragment不需要UI佈局(不與activity佈局發生關聯),因此我們可以不用實現onCreateView()
。
使用tag字串不是嚴格意義上的無佈局fragment,我們也可以為有佈局的fragment提供tag字串。但是若fragment沒有相關佈局的話,那麼tag字串是唯一標識該fragment的方法。若想在之後使用該fragment的話,可以通過findFragmentByTag()
。
管理Fragments
為了在activity中管理fragments,我們可以使用FragmentManager物件,可以在activity中通過`getFragmentManager()獲取該物件。
我們可以通過FragmentManager完成以下事宜:
- 可以通過
findFragmentById()
(獲取有UI佈局的fragments)或findFragmentByTag()
(獲得有/無UI佈局的fragments)獲取activity中存在的fragments。 - 通過
popBackStack()
將fragment從Back棧中彈出(模擬使用者BACK命令)。 - 通過
addOnBackStackChangedListener()
為Back棧註冊事件監聽。
關於FragmentManager類更多內容可以參考官方文件中的介紹。
上一小節介紹了可以通過FragmentTransaction開啟一個事務操作,下面詳細介紹。
執行Fragment事務
通過Fragment事務,我們能夠完成新增/刪除/替換操作。每次操作的集合都可稱為一次事務。我們也可以通過Back棧對事務進行管理,可以通過activity管理Back棧,該Back棧允許使用者通過導航回到變化之前的狀態(類似activity的Back棧)。
事務由一組同一時刻的操作組成。我們可以使用add()
,remove()
,replace()
中的1個或多個完成一次事務操作。最後需要呼叫commit()
方法完成事務提交。
在呼叫commit()
之前,我們可能需要呼叫addToBackStack(String)
(引數為Back棧名),該方法能夠實現將操作的事務新增到fragment事務的Back棧中。該Back棧由activity管理,通過BACK鍵可以回到改變之前的fragment狀態。
例如:
// 建立fragment和事務
Fragment newFragment = new ExampleFragment();
FragmentTransaction transaction = getFragmentManager().beginTransaction();
// 使用fragment替換id為fragment_container的檢視
// 將本次事務新增到back棧中
transaction.replace(R.id.fragment_container, newFragment);
transaction.addToBackStack(null);
// 提交事務
transaction.commit();
例子中,使用ExampleFragment物件替換 R.id.fragment_container。然後呼叫addToBackStack()
,將替換事務儲存到Back棧中。使用者可以通過BACK鍵返回到替換之前的狀態。
若在一次事務中執行了多次操作,然後再呼叫addToBackStack()
,則所有在commit()
之前的操作都會儲存在Back棧中,並作為一次事務。
需要注意一下兩點:
- 在一次事務操作完之後必須呼叫
commit()
; - 若在一個佈局中新增多個fragment的話,則顯示的順序和新增的順序一致。
當我們移除一個fragment時,若不呼叫addToBackStack()
,則當移除事務執行時,該fragment將會銷燬,使用者無法返回移除前的狀態。而若呼叫的話,當移除一個fragment時,該fragment進入停止狀態,若使用者通過導航返回時,該fragment又會重新啟動。
tip:
- 對於每個事務,我們可以通過
setTransaction()
設定事務的動畫,該方法在commit()
之前呼叫。 - 可以呼叫
hide(Fragment)
隱藏當前的Fragment,僅僅是設為不可見,並不會銷燬;呼叫show(Fragment)
顯示之前隱藏的Fragment。當然這兩個操作也是事務,須在呼叫commit()
之前執行。
呼叫commit()
不會立刻執行事務,而是將該事務新增到主執行緒的任務表中,一旦主執行緒能夠處理時就會執行該事務。我們可以通過呼叫FragmentManager的executePendingTransactions()
方法立即執行commit()
方法提交的事務,該方法必須在主執行緒中執行。通常來說,沒有必要這樣做,只有在其它執行緒需要依賴該事務時,才這樣做。
注意:commit()
方法需要在activity儲存狀態之前呼叫,否則的話會丟擲異常。這是因為若activity恢復資料的話,在該方法提交之後的狀態可能丟失。若允許丟失資料的話,我們可以呼叫commitAllowingStateLoss()
而不是呼叫commit()
。
和Activity通訊
儘管fragment作為一個獨立於activity的物件,並且能夠在多個activities中使用同一個fragment,但是我們仍然可以在activity中直接和它包含的fragments進行通訊。
在fragment中可以通過getActivity()
方法獲取activity,然後就可以進行通訊,例如獲得activity佈局中的控制元件:
View listView = getActivity().findViewById(R.id.list);
同樣,activity可以通過FragmentManager使用findFragmentById()
或者findFragmentByTag()
獲取fragment,例如:
ExampleFragment fragment = (ExampleFragment) getFragmentManager()
.findFragmentById(R.id.example_fragment);
fragment中有兩個方法可以設定和獲取儲存的Bundle物件:setArgument()
和getArgument()
。
建立事件回撥方法
有些情況下,fragment可能需要和宿主activity共享事件。一個好的辦法是在fragment中定義一個回撥介面,而宿主activity實現該介面。當activity通過介面接收回調時,若有必要的話,他就能夠和其它fragments共享資訊。
例如新聞應用程式,它包含兩個fragments,一個用於顯示文章標題列表(fragment A),另一個用於顯示文章內容(fragment B)。則當點選A中標題列表的某個文章標題時,就需要在B中顯示該文章的具體內容。因此則需要在A中定義一個OnArticleSelectedListener
介面:
public static class FragmentA extends ListFragment {
...
// Container Activity must implement this interface
public interface OnArticleSelectedListener {
public void onArticleSelected(Uri articleUri);
}
...
}
然後,宿主activity需要實現該介面,並實現onArticleSelected(Uri articleUri)
,該方法用於通知B顯示具體內容事件。為了確保在宿主activity中實現該介面,可以在fragment中的onAttach()
中強制轉換宿主activity來例項化介面。例如:
public static class FragmentA extends ListFragment {
OnArticleSelectedListener mListener;
...
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
try {
mListener = (OnArticleSelectedListener) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString() + " must implement OnArticleSelectedListener");
}
}
...
}
可以看到若宿主activity沒有實現該介面的話會丟擲ClassCastException異常。若實現的話,則mListener變數就持有宿主activity的引用,則A就可以通過介面方法實現共享點選事件。例如,假設A繼承自ListFragment,每次使用者點選列表選項時,都會呼叫onListItemClick()
,我們可以在這裡面回撥介面方法:
public static class FragmentA extends ListFragment {
OnArticleSelectedListener mListener;
...
@Override
public void onListItemClick(ListView l, View v, int position, long id) {
// Append the clicked item's row ID with the content provider Uri
Uri noteUri = ContentUris.withAppendedId(ArticleColumns.CONTENT_URI, id);
// Send the event and Uri to the host activity
mListener.onArticleSelected(noteUri);
}
...
}
onListItemClick()
中傳遞的id引數為被點選的選項的行號(來自ContentProvider的內容)。
新增到ActionBar選項
fragments可以作為activity的Options選單(因此也是ActionBar)中的選單選項,通過實現fragment的onCreateOptionsMenu()
。想要回調該方法,必須在onCreate()
中呼叫setHasOptionsMenu()
,表明該fragment作為選單選項。
任何通過fragments新增的選項都會附加在已存在的選項選單上。當點選某個選項時會回撥onOptionsItemSelected()
。
我們也可以通過registerForContextMenu()
在fragment佈局中註冊一個檢視提供context選單。當用戶開啟context選單時,就會呼叫onCreateContextMenu()
。當用戶選擇某個選項時,就會呼叫onContextItemSelected()
。
注意:儘管當用戶選擇某個選項時,fragment接收點選選項回撥方法,但是activity會首先回調相應的方法。若activity中沒有實現處理選中的選項,則事件會傳遞給fragment的回撥方法。對於context選單和Options選單都是如此。
處理fragment生命週期
fragment的生命週期類似於activity的生命週期,也存在以下三種狀態:
- Resumed
執行狀態。fragment在處於執行狀態的activity中可見。 - Paused
另外一個activity來到前臺並獲得焦點,但是此時宿主activity仍然可見(被透明的或dialog樣式的activity覆蓋)。 - Stopped
fragment不再可見。原因要麼是宿主activity處於停止狀態,要麼該fragment被移除但是新增到Back棧了。處於停止狀態的fragment依然是存活的(所有狀態資訊和成員都被系統儲存)。若宿主activity被殺死的話,它也不再可以獲得並且也將被殺死。
同activity一樣,我們也可以在宿主activity所在的程序被殺死時,通過Bundle物件儲存fragment狀態,然後再activity重建時恢復fragment狀態。我們可以在fragment的onSaveInstanceState()
中儲存狀態,在onCreate()
,或onCreateView()
,或onActivityCreated()
中恢復資料。
關於activity和fragment生命週期一個重要的區別在於如何在Back棧中進行儲存。預設情況下,activity被放在activities的Back棧中,Back棧由系統管理。然而,fragment的Back棧由宿主activity管理,並且當fragment被移除時只有在我們通過addToBackStack()
明確將fragment新增到Back棧才行。也就是說fragment若要進入Back棧必須滿足兩個條件:
1. 該fragment將要被移除;
2. 在commit()
之前必須呼叫addToBackStack()
。
fragment的生命週期和activity的生命週期類似,我們需要特別注意的是activity的生命週期如何影響fragment的生命週期。
注意:若我們需要在fragment獲得一個Context物件,可以通過getActivity()
。然而只有當fragment關聯acitivity時才能使用該方法。當fragment還沒有關聯activity或者解除關聯時,該方法返回null。
與activity生命週期相協調
宿主activity的生命週期直接影響fragment的生命週期,因此,activity的每個週期方法都會導致一個類似的fragment週期方法。
除此之外,fragment還有一些其他的週期方法,如下:
onAttach()
當fragment和activity進行關聯時呼叫,此時會傳遞activity引數。onCreateView()
當建立fragment檢視層次時呼叫。onActivityCreated()
當activity方法onCreate()
返回時呼叫。onDestroyView()
當移除fragment檢視時被呼叫。onDetach()
當fragment和activity解除關聯時被呼叫。
下圖描述了宿主activity如何影響fragment:
只有當宿主activity處於執行狀態時,我們才能獨立處理fragment生命週期了。否則,都會按照上圖所示,受到宿主activity的影響。