安卓-碎片的使用入門

第四章-Android開發中的碎片

4.1 碎片是什麼

碎片(Fragment)是一種可以嵌入在活動當中的UI片段,它能讓程序更加合理和充分地利用大屏幕的空間,因而在平板上應用得非常廣泛。雖然碎片對你來說應該是個全新的概念,但我相信你學習起來應該毫不費力,因爲它和活動實在是太像了,同樣都能包含佈局,同樣都有自己的生命週期。你甚至可以將碎片理解成一個迷你型的活動,雖然這個迷你型的活動有可能和普通的活動是一樣大的。

 那麼究竟要如何使用碎片才能充分地利用平板屏幕的空間呢?想象我們正在開發一個新聞應用,其中一個界面使用RecyclerView展示了一組新聞的標題,當點擊了其中一個標題時,就打開另一個界面顯示新聞的詳細內容。如果是在手機中設計,我們可以將新聞標題列表放在一個活動中,將新聞的詳細內容放在另一個活動中,如圖4.1所示。
在這裏插入圖片描述
圖 4.1 手機的設計方案

 可是如果在平板上也這麼設計,那麼新聞標題列表將會被拉長至填充滿整個平板的屏幕,而新聞的標題一般都不會太長,這樣將會導致界面上有大量的空白區域,如圖4.2所示。
在這裏插入圖片描述
圖 4.2 平板的新聞列表

 因此,更好的設計方案是將新聞標題列表界面和新聞詳細內容界面分別放在兩個碎片中,然後在同一個活動裏引入這兩個碎片,這樣就可以將屏幕空間充分地利用起來了,如圖4.3所示。
在這裏插入圖片描述
圖 4.3 平板的雙頁設計

4.2 碎片的使用方式

 介紹了這麼多抽象的東西,也是時候學習一下碎片的具體用法了。你已經知道,碎片通常都是在平板開發中使用的,因此我們首先要做的就是創建一個平板模擬器。創建模擬器的方法我們在第1章已經學過了,創建完成後啓動平板模擬器,效果如圖4.4所示。
在這裏插入圖片描述
圖 4.4 平板模擬器的運行效果

4.2.1 碎片的簡單用法

 這裏我們準備先寫一個最簡單的碎片示例來練練手,在一個活動當中添加兩個碎片,並讓這兩個碎片平分活動空間。

 新建一個左側碎片佈局left_fragment.xml,代碼如下所示:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:text="Button"
        />

</LinearLayout>

 這個佈局非常簡單,只放置了一個按鈕,並讓它水平居中顯示。然後新建右側碎片佈局right_fragment.xml,代碼如下所示:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:background="#00ff00"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:textSize="20sp"
        android:text="This is right fragment"
        />

</LinearLayout>

 可以看到,我們將這個佈局的背景色設置成了綠色,並放置了一個TextView用於顯示一段文本。

 接着新建一個LeftFragment 類,並讓它繼承自Fragment。注意,這裏可能會有兩個不同包下的Fragment供你選擇,一個是系統內置的android.app.Fragment,一個是support-v4庫中的android.support.v4.app.Fragment。這裏我強烈建議你使用support-v4庫中的Fragment,因爲它可以讓碎片在所有Android系統版本中保持功能一致性。比如說在Fragment中嵌套使用Fragment,這個功能是在Android 4.2系統中才開始支持的,如果你使用的是系統內置的Fragment,那麼很遺憾,4.2系統之前的設備運行你的程序就會崩潰。而使用support-v4庫中的Fragment就不會出現這個問題,只要你保證使用的是最新的support-v4庫就可以了。另外,我們並不需要在build.gradle文件中添加support-v4庫的依賴,因爲build.gradle文件中已經添加了appcompat-v7庫的依賴,而這個庫會將support-v4庫也一起引入進來。

 現在編寫一下LeftFragment 中的代碼,如下所示:

public class LeftFragment extends Fragment {

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.left_fragment, container, false);
        return view;
    }

}

 這裏僅僅是重寫了Fragment的onCreateView()方法,然後在這個方法中通過LayoutInflater的inflate()方法將剛纔定義的left_fragment佈局動態加載進活動中來,整個方法簡單明瞭。

**問題來了,上面所提到的,將自己對應的佈局文件left_fragment.xml以及right_fragment.xml加載進來,那什麼時候加載進來呢?**這個問題在碎片佈局的引入執行邏輯一章中再進行回答。

接着我們用同樣的方法再新建一個RightFragment ,代碼如下所示:

public class RightFragment extends Fragment {

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.right_fragment, container, false);
        return view;
    }

}

 基本上代碼都是相同的,相信已經沒有必要再做什麼解釋了。接下來修改activity_main.xml中的代碼,如下所示:

<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:id="@+id/left_fragment"
        android:name="com.example.fragmenttest.LeftFragment"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1" />

    <fragment
        android:id="@+id/right_fragment"
        android:name="com.example.fragmenttest.RightFragment"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1" />

</LinearLayout>

 可以看到,我們使用了<fragment>標籤在佈局中添加碎片,其中指定的大多數屬性都是你熟悉的,只不過這裏還需要通過android:name 屬性來顯式指明要添加的碎片類名,注意一定要將類的包名也加上(因爲不加上就不知道此fragment標籤是由哪一個類實現的)。

 這樣最簡單的碎片示例就已經寫好了,現在運行一下程序,效果如圖4.5所示。
在這裏插入圖片描述
圖 4.5 碎片的簡單運行效果

 正如我們所期待的一樣,兩個碎片平分了整個活動的佈局。不過這個例子實在是太簡單了,在真正的項目中很難有什麼實際的作用,因此我們馬上來看一看,關於碎片更加高級的使用技巧。

4.2.2 碎片佈局引入活動的程序執行邏輯

 現在可以回答上述問題了,究竟何時何地加載了兩個碎片佈局。由於我們在MainActivity方法中調用了方法:setContentView(R.layout.activity_main);所以只會加載佈局文件activity_main.xml,而我們在此佈局文件中添加了兩個fragment控件,而實際上其通過:android:name="com.example.fragmenttest.LeftFragment"指向了類文件:LeftFragment.java,(我們不是通過android:id="@+id/left_fragment"知道這個碎片控件實現類是誰,而是android:name來控制的),而類文件LeftFragment.java則重寫了方法onCreateView(),使其返回一個我們所指定的的佈局View對象,而這個對象是由R.layout.left_fragment指向了:left_fragment.xml

所以執行邏輯可以認爲是大致如下:

MainActivity#onCreate -> activity_main.xml -> <fragment>-> <fragment>標籤中的android:name -> LeftFragment類 ->LayoutInflater#inflate(int, android.view.ViewGroup, boolean)方法 -> left_fragment.xml -> right_fragment同理。

 可以發現實際上上述代碼執行順序和我們寫代碼的順序是完全相反的,我們首先要寫一個關於fragment的佈局xml文件,接着創建一個碎片類去引用這個佈局文件,最後第二步是在activity_main文件中通過android:name來引用這個碎片類,最後纔是在MainActivity中加載activity_main佈局。可以說這樣寫代碼的好處是不會IDE是不會報錯引用錯誤,壞處是和程序的執行順序正好相反,但是如果你深諳代碼的執行邏輯,首先就是在activity_main文件中通過android:name來引用這個碎片類,一步步你想思維,我想可能也是一個寫Android代碼的好思維方式。

 所以如果你知道如果引用控件的話,那麼碎片的加入佈局文件與引用控件則有非常大的區別。其不能通過inclue來實現,比如說:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal">
    <include layout="@layout/right_fragment"/>

    <include layout="@layout/left_fragment"/>

</LinearLayout>

此佈局對於app主活動的影響就是:
在這裏插入圖片描述
如果你以下兩句代碼交換順序,

    <include layout="@layout/right_fragment"/>

    <include layout="@layout/left_fragment"/>

那麼就會達到以下的情況:(由於兩個佈局都是全屏幕的,所以第二個引入完全沒有起到效果)
在這裏插入圖片描述
所以說這樣一來完全沒有能夠得到想要的碎片佈局的效果。

4.3 動態添加碎片

 在上一節當中,你已經學會了在佈局文件中添加碎片的方法,不過碎片真正的強大之處在於,它可以在程序運行時動態地添加到活動當中。根據具體情況來動態地添加碎片,你就可以將程序界面定製得更加多樣化

 我們還是在上一節代碼的基礎上繼續完善,新建another_right_fragment.xml,代碼如下所示:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:background="#ffff00"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:textSize="20sp"
        android:text="This is another right fragment"
        />

</LinearLayout>

 這個佈局文件的代碼和right_fragment.xml中的代碼基本相同,只是將背景色改成了黃色,並將顯示的文字改了改。然後新建AnotherRightFragment 作爲另一個右側碎片,代碼如下所示:

public class AnotherRightFragment extends Fragment {

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.another_right_fragment, container,
            false);
        return view;
    }

}

 代碼同樣非常簡單,在onCreateView() 方法中加載了剛剛創建的another_right_fragment佈局。這樣我們就準備好了另一個碎片,接下來看一下如何將它動態地添加到活動當中。修改activity_main.xml,代碼如下所示:

<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:id="@+id/left_fragment"
        android:name="com.example.fragmenttest.LeftFragment"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1" />

    <FrameLayout
        android:id="@+id/right_layout"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1" >
    </FrameLayout>

</LinearLayout>

 可以看到,現在將右側碎片替換成了一個FrameLayout中,還記得這個佈局嗎?在上一章中我們學過,這是Android中最簡單的一種佈局,所有的控件默認都會擺放在佈局的左上角**。由於這裏僅需要在佈局裏放入一個碎片,不需要任何定位,因此非常適合使用FrameLayout**。

 下面我們將在代碼中向FrameLayout裏添加內容,從而實現動態添加碎片的功能。修改MainActivity中的代碼,如下所示:

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button button = (Button) findViewById(R.id.button);
        button.setOnClickListener(this);
        replaceFragment(new RightFragment());
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.button:
                replaceFragment(new AnotherRightFragment());
                break;
            default:
                break;
        }
    }

    private void replaceFragment(Fragment fragment) {
        FragmentManager fragmentManager = getSupportFragmentManager();
        FragmentTransaction transaction = fragmentManager.beginTransaction();
        transaction.replace(R.id.right_layout, fragment);
        transaction.commit();
    }
}

 可以看到,首先我們給左側碎片中的按鈕註冊了一個點擊事件,然後調用replaceFragment() 方法動態添加了RightFragment這個碎片。當點擊左側碎片中的按鈕時,又會調用replaceFragment() 方法將右側碎片替換成AnotherRightFragment。結合replaceFragment() 方法中的代碼可以看出,動態添加碎片主要分爲5步。

(1) 創建待添加的碎片實例。

(2) 獲取FragmentManager,在活動中可以直接通過調用getSupportFragmentManager() 方法得到。

(3) 開啓一個事務,通過調用beginTransaction() 方法開啓。

(4) 向容器內添加或替換碎片,一般使用replace() 方法實現,需要傳入容器的id和待添加的碎片實例。

(5) 提交事務,調用commit() 方法來完成。

 這樣就完成了在活動中動態添加碎片的功能,重新運行程序,可以看到和之前相同的界面,然後點擊一下按鈕,效果如圖4.6所示。
在這裏插入圖片描述
圖 4.6 動態添加碎片的效果

 如果想要得到一種效果:按BUTTON一下就會使右邊的兩個佈局切換,只要將MainActivity.java的onCreate()方法改成以下邏輯即可:

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.button:
                if (flag) {
                    replaceFragment(new AnotherRightFragment());
                    flag = false;
                    break;
                }
                replaceFragment(new RightFragment());
                flag=true;
                break;

            default:
                break;

        }
    }

4.4 在碎片中模擬返回棧

 在上一小節中,我們成功實現了向活動中動態添加碎片的功能,不過你嘗試一下就會發現,通過點擊按鈕添加了一個碎片之後,這時按下Back鍵程序就會直接退出。如果這裏我們想模仿類似於返回棧的效果,按下Back鍵可以回到上一個碎片,該如何實現呢?

 其實很簡單,FragmentTransaction中提供了一個addToBackStack() 方法,可以用於將一個事務添加到返回棧中,修改MainActivity中的代碼,如下所示:

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    ...

    private void replaceFragment(Fragment fragment) {
        FragmentManager fragmentManager = getSupportFragmentManager();
        FragmentTransaction transaction = fragmentManager.beginTransaction();
        transaction.replace(R.id.right_layout, fragment);
        transaction.addToBackStack(null);
        transaction.commit();
    }

}

 這裏我們在事務提交之前調用了FragmentTransaction的addToBackStack() 方法,它可以接收一個名字用於描述返回棧的狀態,一般傳入null 即可。現在重新運行程序,並點擊按鈕將AnotherRightFragment添加到活動中,然後按下Back鍵,你會發現程序並沒有退出,而是回到了RightFragment界面,繼續按下Back鍵,RightFragment界面也會消失,再次按下Back鍵,程序纔會退出。

4.2.4 碎片和活動之間進行通信

 雖然碎片都是嵌入在活動中顯示的,可是實際上它們的關係並沒有那麼親密。你可以看出,碎片和活動都是各自存在於一個獨立的類當中的,它們之間並沒有那麼明顯的方式來直接進行通信。如果想要在活動中調用碎片裏的方法,或者在碎片中調用活動裏的方法,應該如何實現呢?

 爲了方便碎片和活動之間進行通信,FragmentManager提供了一個類似於findViewById() 的方法,專門用於從佈局文件中獲取碎片的實例,代碼如下所示:

RightFragment rightFragment = getSupportFragmentManager()
    .findFragmentById(R.id.right_fragment);

 調用FragmentManager的findFragmentById() 方法,可以在活動中得到相應碎片的實例,然後就能輕鬆地調用碎片裏的方法了。

 掌握瞭如何在活動中調用碎片裏的方法,那在碎片中又該怎樣調用活動裏的方法呢?其實這就更簡單了,在每個碎片中都可以通過調用getActivity() 方法來得到和當前碎片相關聯的活動實例,代碼如下所示:

MainActivity activity =  getActivity();

 有了活動實例之後,在碎片中調用活動裏的方法就變得輕而易舉了。另外當碎片中需要使用Context 對象時,也可以使用getActivity() 方法,因爲獲取到的活動本身就是一個Context 對象。

 這時不知道你心中會不會產生一個疑問:既然碎片和活動之間的通信問題已經解決了,那麼碎片和碎片之間可不可以進行通信呢?

 說實在的,這個問題並沒有看上去那麼複雜,它的基本思路非常簡單,首先在一個碎片中可以得到與它相關聯的活動,然後再通過這個活動去獲取另外一個碎片的實例,這樣也就實現了不同碎片之間的通信功能,因此這裏我們的答案是肯定的。

4.3 碎片的生命週期

 和活動一樣,碎片也有自己的生命週期,並且它和活動的生命週期實在是太像了,我相信你很快就能學會,下面我們馬上就來看一下。

4.3.1 碎片的狀態和回調

 還記得每個活動在其生命週期內可能會有哪幾種狀態嗎?沒錯,一共有運行狀態、暫停狀態、停止狀態和銷燬狀態這4種。類似地,每個碎片在其生命週期內也可能會經歷這幾種狀態,只不過在一些細小的地方會有部分區別。

  1. 運行狀態

    ​ 當一個碎片是可見的,並且它所關聯的活動正處於運行狀態時,該碎片也處於運行狀態。

  2. 暫停狀態

    ​ 當一個活動進入暫停狀態時(由於另一個未佔滿屏幕的活動被添加到了棧頂),與它相關聯的可見碎片就會進入到暫停狀態。

  3. 停止狀態

    ​ 當一個活動進入停止狀態時,與它相關聯的碎片就會進入到停止狀態,或者通過調用FragmentTransaction的remove() 、replace() 方法將碎片從活動中移除,但如果在事務提交之前調用addToBackStack() 方法,這時的碎片也會進入到停止狀態。總的來說,進入停止狀態的碎片對用戶來說是完全不可見的,有可能會被系統回收。

  4. 銷燬狀態

    ​ 碎片總是依附於活動而存在的,因此當活動被銷燬時,與它相關聯的碎片就會進入到銷燬狀態。或者通過調用FragmentTransaction的remove() 、replace() 方法將碎片從活動中移除,但在事務提交之前並沒有調用addToBackStack() 方法,這時的碎片也會進入到銷燬狀態。

 結合之前的活動狀態,相信你理解起來應該毫不費力吧。同樣地,Fragment 類中也提供了一系列的回調方法,以覆蓋碎片生命週期的每個環節。其中,活動中有的回調方法,碎片中幾乎都有,不過碎片還提供了一些附加的回調方法,那我們就重點看一下這幾個回調。

onAttach() 。當碎片和活動建立關聯的時候調用。

onCreateView() 。爲碎片創建視圖(加載佈局)時調用。

onActivityCreated() 。確保與碎片相關聯的活動一定已經創建完畢的時候調用。

onDestroyView() 。當與碎片關聯的視圖被移除的時候調用。

onDetach() 。當碎片和活動解除關聯的時候調用。

 碎片完整的生命週期示意圖可參考圖4.7,圖片源自Android官網。
在這裏插入圖片描述
圖 4.7 碎片的生命週期

4.3.2 體驗碎片的生命週期

 爲了讓你能夠更加直觀地體驗碎片的生命週期,我們還是通過一個例子來實踐一下。例子很簡單,仍然是在FragmentTest項目的基礎上改動的。

 修改RightFragment中的代碼,如下所示:

public class RightFragment extends Fragment {

    public static final String TAG = "RightFragment";

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        Log.d(TAG, "onAttach");
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.d(TAG, "onCreate");
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        Log.d(TAG, "onCreateView");
        View view = inflater.inflate(R.layout.right_fragment, container, false);
        return view;
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        Log.d(TAG, "onActivityCreated");
    }

    @Override
    public void onStart() {
        super.onStart();
        Log.d(TAG, "onStart");
    }

    @Override
    public void onResume() {
        super.onResume();
        Log.d(TAG, "onResume");
    }

    @Override
    public void onPause() {
        super.onPause();
        Log.d(TAG, "onPause");
    }

    @Override
    public void onStop() {
        super.onStop();
        Log.d(TAG, "onStop");
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        Log.d(TAG, "onDestroyView");
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.d(TAG, "onDestroy");
    }

    @Override
    public void onDetach() {
        super.onDetach();
        Log.d(TAG, "onDetach");
    }

}

 我們在RightFragment中的每一個回調方法裏都加入了打印日誌的代碼,然後重新運行程序,這時觀察logcat中的打印信息,如圖4.8所示。

在這裏插入圖片描述
圖 4.8 啓動程序時的打印日誌

 可以看到,當RightFragment第一次被加載到屏幕上時,會依次執行onAttach() 、onCreate() 、onCreateView() 、onActivityCreated() 、onStart() 和onResume() 方法。然後點擊LeftFragment中的按鈕,此時打印信息如圖4.9所示。
在這裏插入圖片描述
圖 4.9 替換成AnotherRightFragment時的打印日誌

 由於AnotherRightFragment替換了RightFragment,此時的RightFragment進入了停止狀態,因此onPause() 、onStop() 和onDestroyView() 方法會得到執行。當然如果在替換的時候沒有調用addToBackStack() 方法,此時的RightFragment就會進入銷燬狀態,onDestroy() 和onDetach() 方法就會得到執行。

 接着按下Back鍵,RightFragment會重新回到屏幕,打印信息如圖4.10所示。

在這裏插入圖片描述
圖 4.10 返回RightFragment時的打印日誌

 由於RightFragment重新回到了運行狀態,因此onCreateView() 、onActivityCreated() 、onStart() 和onResume() 方法會得到執行。注意此時onCreate() 方法並不會執行,因爲我們藉助了addToBackStack() 方法使得RightFragment並沒有被銷燬。

 現在再次按下Back鍵,打印信息如圖4.11所示。
在這裏插入圖片描述
圖 4.11 退出程序時的打印日誌

 依次會執行onPause() 、onStop() 、onDestroyView() 、onDestroy() 和onDetach() 方法,最終將碎片銷燬掉。這樣碎片完整的生命週期你也體驗了一遍,是不是理解得更加深刻了?

 另外值得一提的是,在碎片中你也是可以通過onSaveInstanceState() 方法來保存數據的,因爲進入停止狀態的碎片有可能在系統內存不足的時候被回收。保存下來的數據在onCreate() 、onCreateView() 和onActivityCreated() 這3個方法中你都可以重新得到,它們都含有一個Bundle類型的savedInstanceState 參數。具體的代碼我就不在這裏給出了,如果你忘記了該如何編寫,可以參考2.4.5小節。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章