1. 程式人生 > >解讀Android之Fragment

解讀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達到這種效果。如下圖所示:
fragments
我們應該把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()方法傳遞三個引數:

  1. 載入的佈局檔案ID。
  2. 載入的佈局的父檢視,即該父檢視作為fragment佈局的根佈局。
  3. boolean表示在載入的過程中載入的佈局是否依賴父佈局(第二個引數)。這裡需要傳遞false,這是因為系統已經將載入的佈局插入到ViewGroup物件中;若傳遞true,則在最終的佈局中會建立一個冗餘的檢視。

通過以上介紹我們知道了如何建立一個帶有佈局的fragment,下面,我們介紹如何將fragment新增到activity中。

將fragment新增到activity

通常,fragment作為宿主activity佈局的一部分,有兩種方法將fragment新增到activity中:

  1. 靜態載入
    該方法通過在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。

  2. 動態載入
    該方法是指將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棧中,並作為一次事務。

需要注意一下兩點:

  1. 在一次事務操作完之後必須呼叫commit()
  2. 若在一個佈局中新增多個fragment的話,則顯示的順序和新增的順序一致。

當我們移除一個fragment時,若不呼叫addToBackStack(),則當移除事務執行時,該fragment將會銷燬,使用者無法返回移除前的狀態。而若呼叫的話,當移除一個fragment時,該fragment進入停止狀態,若使用者通過導航返回時,該fragment又會重新啟動。

tip:

  1. 對於每個事務,我們可以通過setTransaction()設定事務的動畫,該方法在commit()之前呼叫。
  2. 可以呼叫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:
activityAndfragment
只有當宿主activity處於執行狀態時,我們才能獨立處理fragment生命週期了。否則,都會按照上圖所示,受到宿主activity的影響。