第一行代碼讀書筆記 4 -- 探究碎片

本篇文章主要介紹以下幾個知識點:

  • 碎片 fragment 的用法;
  • 碎片 fragment 的生命周期;
  • 動態加載布局的技巧,限定符的使用;
  • 實戰:簡易版的新聞應用。
圖片來源于網絡

4.1 碎片是什么

碎片(Fragment)是一種可以嵌入在活動當中的 UI 片段,它能讓程序更加合理和充分地利用大屏幕的空間,因而在平板上應用的非常廣泛。

如開發一個新聞應用,其中一個界面使用 RecyclerView 展示了一組新聞的標題,當點擊了其中一個標題,就打開另一個界面顯示新聞的詳細內容。若是在手機中設計,可以將新聞標題列表放在一個活動中,將新聞的詳細內容放在另一個活動中,如圖所示:

手機的設計方案

但顯示在平板上,那么新聞標題列表將會被拉長至填充滿整個平板的屏幕,而新聞的標題一般都不會太長,這樣將會導致界面上有大量的空白區域,如圖所示:

平板的新聞列表

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

平板的雙頁設計

4.2 碎片的使用方式

4.2.1 碎片的簡單用法

在一個活動當中添加兩個碎片,并讓這兩個碎片平分活動空間。

新建一個左側碎片布局 fragment_left.xml 如下:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    
    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:text="按鈕"/>

</LinearLayout>

這個布局只放置了一個按鈕,并讓它水平居中顯示。然后新建右側碎片布局 fragment_right.xml如下:

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

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:textSize="20sp"
        android:text="這是右邊的fragment"/>

</LinearLayout>

將這個布局的背景色設置成綠色,并放置了一個 TextView 用于顯示一段文本。

接著新建一個 LeftFragment 類,繼承自 Fragment。注意,這里可能會有兩個不同包下的 Fragment 供你選擇,一個是系統內置的 android.app.Fragment,一個是 support-v4 庫中的 android.support.v4.app.Fragment

這里強烈建議使用 support-v4 庫中的 Fragment,因為它可以讓碎片在所有 Android 系統版本中保持功能一致性。

代碼如下:

public class LeftFragment extends Fragment {

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

這里僅僅是重寫了 FragmentonCreateView() 方法,然后在這個方法中通過 LayoutInflaterinflate() 方法將剛定義的 fragment_left 布局動態加載進來,整個方法簡單明了。

接著用同樣的方法再新建一個 RightFragment

public class RightFragment extends Fragment {

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

接下來修改 activity_fragment.xml 中的代碼如下:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/activity_fragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal">

    <fragment
        android:id="@+id/fragment_left"
        android:name="com.wonderful.myfirstcode.inquiry_fragment.LeftFragment"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1"/>

    <fragment
        android:id="@+id/fragment_right"
        android:name="com.wonderful.myfirstcode.inquiry_fragment.RightFragment"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1"/>
    
</LinearLayout>

上面使用了 <fragment> 標簽在布局中添加碎片,通過 android:name 屬性來顯式指明要添加的碎片類名,注意一定要將類的包名也加上。

這樣簡單的碎片示例就已經寫好了,運行程序,(平板上)效果如圖:

碎片的簡單運行效果

4.2.2 動態添加碎片

碎片真正的強大之處在于,它可以在程序運行時動態地添加到活動當中。根據具體情況來動態地添加碎片,你就可以將程序界面定制得更加多樣化。

在上一節代碼的基礎上繼續完善,新建 fragment_another_right.xml 如下:

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

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:textSize="20sp"
        android:text="這是另外一個右邊的fragment"/>

</LinearLayout>

然后新建 AnotherRightFragment 作為另一個右側碎片如下:

public class AnotherRightFragment extends Fragment {

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

接下來看一下如何將它動態地添加到活動當中。修改 activity_fragment.xml 如下:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/activity_fragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal">

    <fragment
        android:id="@+id/fragment_left"
        android:name="com.wonderful.myfirstcode.inquiry_fragment.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"/>
    
    <!--
    <fragment
        android:id="@+id/fragment_right"
        android:name="com.wonderful.myfirstcode.inquiry_fragment.RightFragment"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1"/>
         -->

</LinearLayout>

上面將右側碎片放在了一個 FrameLayout 中,下面在代碼中向 FrameLayout 里添加內容,從而實現動態添加碎片的功能。修改 Activity 中的代碼如下:

public class FragmentActivity extends AppCompatActivity implements View.OnClickListener {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_fragment);

        Button button = (Button) findViewById(R.id.button);
        button.setOnClickListener(this);
        replaceFragment(new RightFragment());
    }

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

            default:
                break;
        }
    }

    private void replaceFragment(Fragment fragment){
        // 獲取FragmentManager
        FragmentManager fragmentManager = getSupportFragmentManager();
        // 開啟事務
        FragmentTransaction transaction = fragmentManager.beginTransaction();
        // 添加或替換碎片
        transaction.replace(R.id.right_layout,fragment);
        // 提交事務
        transaction.commit();
    }
}

上述代碼,給左側碎片中的按鈕注冊了一個點擊事件,調用 replaceFragment() 方法動態添加碎片。結合代碼可看出,動態添加碎片主要分為 5 步。

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

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

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

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

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

重新運行程序,效果如圖:

動態添加碎片的效果

4.2.3 在碎片中模擬返回棧

在上一小節中,實現了向活動中動態添加碎片的功能,但通過點擊按鈕添加了一個碎片之后,按下 Back 鍵程序就會直接退出。該如何實現按下 Back 鍵可以回到上一個碎片呢?

FragmentTransaction 中提供了一個 addToBackStack() 方法,可以用于將一個事務添加到返回棧中,修改 Activity 中的代碼如下:

public class FragmentActivity extends AppCompatActivity implements View.OnClickListener {

    . . .

    private void replaceFragment(Fragment fragment){
        // 獲取FragmentManager
        FragmentManager fragmentManager = getSupportFragmentManager();
        // 開啟事務
        FragmentTransaction transaction = fragmentManager.beginTransaction();
        // 添加或替換碎片
        transaction.replace(R.id.right_layout,fragment);
        // 用于描述返回棧的狀態
        transaction.addToBackStack(null);
        // 提交事務
        transaction.commit();
    }
}

在事務提交之前調用了 FragmentTransactionaddToBackStack() 方法,它可以接收一個名字用于描述返回棧的狀態,一般傳入 null 即可。這樣問題就解決了。

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

為了方便碎片和活動之間進行通信,FragmentManager 提供了一個類似于 findViewById() 的方法,專門用于從布局文件中獲取碎片的實例。在活動中調用碎片里的方法:

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

在碎片中調用活動里的方法:通過調用 getActivity()方法來得到和當前碎片相關聯的活動實例,代碼如下:

MainActivity activity = (MainActivity) getActivity();

4.3 碎片的生命周期

每個活動在其生命周期內可能會有四種狀態:運行狀態、暫停 狀態、停止狀態和銷毀狀態。類似地,每個碎片在其生命周期內也可能會經歷這幾種狀態,只不過在一些細小的地方會有部分區別。

  • 運行狀態:
    當一個碎片是可見的,并且它所關聯的活動正處于運行狀態時,該碎片也處于運行狀態。

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

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

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

和活動 Acitvity 相似,Fragment 類中也提供了一系列的回調方法,以覆蓋碎片生命周期的每個環節。其中,活動中有的回調方法,碎片中幾乎都有,不過碎片還提供了一些附加的回調方法,重點來看下這幾個回調:

  • onAttach() 當碎片和活動建立關聯的時候調用。
  • onCreateView() 為碎片創建視圖(加載布局)時調用。
  • onActivityCreated() 確保與碎片相關聯的活動一定已經創建完畢的時候調用。
  • onDestroyView() 當與碎片關聯的視圖被移除的時候調用。
  • onDetach() 當碎片和活動解除關聯的時候調用。

碎片完整的生命周期可參考源自 Android 官網圖的示意圖:

碎片的生命周期

另外,在碎片中你也可以通過 onSaveInstanceState() 方法來保存數據, 因為進入停止狀態的碎片有可能在系統內存不足的時候被回收。保存下來的數據在 onCreate()onCreateView()onActivityCreated() 這三個方法中你都可以重新得到,它們都含有一個 Bundle 類型的 savedInstanceState 參數。

4.4 動態加載布局的技巧

4.4.1 使用限定符

現有個需求:在平板上使用雙頁模式,在手機上顯示單頁模式。那么怎樣才能在運行時判斷程序應該是使用雙頁模式還是單頁模式呢?這就需要借助限定符(Qualifiers)來實現了。

通過一個例子來學習一下它的用法,修改項目中 activity_fragment.xml 的代碼:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/activity_fragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal">

    <fragment
        android:id="@+id/fragment_left"
        android:name="com.wonderful.myfirstcode.inquiry_fragment.LeftFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>

這里將多余的代碼都刪掉,只留下一個左側碎片,并讓它充滿整個父布局。接著在 res 目錄下新建 layout-large 文件夾,在這個文件夾下新建一個布局,也叫做 activity_fragment.xml, 代碼如下:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/activity_fragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal">

    <fragment
        android:id="@+id/fragment_left"
        android:name="com.wonderful.myfirstcode.inquiry_fragment.LeftFragment"
        android:layout_width="0dp"
        android:layout_height="match_parent" 
        android:layout_weight="1"/>
    
    <fragment
        android:id="@+id/fragment_right"
        android:name="com.wonderful.myfirstcode.inquiry_fragment.RightFragment"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="3"/>
    
</LinearLayout>

上面 layout/activity_fragment 布局只包含了一個碎片,即單頁模式,而 layout-large/activity_fragment 布局包含了兩個碎片,即雙頁模式。其中 large 就是一個限定符,那些屏幕被認為是 large 的設備就會自動加載 layout-large 文件夾下的布局,而小屏幕的設備則還是會加載 layout 文件夾下的布局。

然后將 ActivityreplaceFragment() 方法注釋掉,并在平板模擬器上重新運行程序, 效果如圖:

雙頁模式運行效果

再啟動一個手機模擬器,并在這個模擬器上重新運行程序,效果如圖:

單頁模式運行效果

Android 中一些常見的限定符可以參考下表:

Android 中一些常見的限定符

4.4.2 使用最小寬度限定符

有時候希望可以更加靈活地為不同設備加載布局,不管它們是不是被系統認定為 “ large ”,這時就可以使用最小寬度限定符(Smallest-width Qualifier)了。

最小寬度限定符允許我們對屏幕的寬度指定一個最小指(以 dp 為單位),然后以這個最小值為臨界點,屏幕寬度大于這個值的設備就加載一個布局,屏幕寬度小于這個值的設備就加載另一個布局。

在 res 目錄下新建 layout-sw600dp 文件夾,然后在這個文件夾下新建 activity_fragment .xml 布局,代碼與上面 layout-large/activity_fragment 中的一樣。

...

這就意味著,當程序運行在屏幕寬度大于 600dp 的設備上時,會加載 layout-sw600dp/ activity_fragment 布局,當程序運行在屏幕寬度小于 600dp 的設備上時,則仍然加載默認的 layout/activity_fragment 布局。

4.5 碎片的最佳實踐——一個簡易版的新聞應用

需求:一個簡易的新聞應用,可以同時兼容手機和平板。

首先,在 app/build.gradle 中添加后面需要用到的 RecyclerView 依賴庫:

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:24.2.1'
    compile 'com.android.support:recyclerview-v7:24.2.1'
    testCompile 'junit:junit:4.12'
}

接下來,準備好一個新聞的實體類,新建類 News,代碼如下:

/**
 * 新聞實體類
 * Created by KXwon on 2016/12/12.
 */

public class News {

    private String title;   // 新聞標題

    private String content; // 新聞內容

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }
}

接著新建一個 news_content_frag.xml 布局,作為新聞內容的布局:

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

    <LinearLayout
        android:id="@+id/visibility_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:visibility="invisible">

        <TextView
            android:id="@+id/news_title"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:textSize="20sp"
            android:padding="10dp"/>
        
        <View
            android:layout_width="match_parent"
            android:layout_height="1dp"
            android:background="#000"/>

        <TextView
            android:id="@+id/news_content"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1"
            android:textSize="18sp"
            android:padding="15dp"/>
        
    </LinearLayout>

    <View
        android:layout_width="1dp"
        android:layout_height="match_parent"
        android:layout_alignParentLeft="true"
        android:background="#000"/>

</RelativeLayout>

新聞內容的布局主要分為兩個部分,頭部顯示新聞標題,正文顯示新聞內容,中間使用一條細線分隔開。

然后再新建一個 NewsContentFragment 類,如下:

/**
 * 新聞內容fragment
 * Created by KXwon on 2016/12/12.
 */

public class NewsContentFragment extends Fragment {

    private View view;

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

    /**
     * 將新聞標題和新聞內容顯示在界面上
     * @param newsTitle   標題
     * @param newsContent 內容
     */
    public void refresh(String newsTitle, String newsContent) {
        View visibilityLayout = view.findViewById(R.id.visibility_layout);
        visibilityLayout.setVisibility(View.VISIBLE);
        TextView newsTitleText = (TextView) view.findViewById (R.id.news_title);
        TextView newsContentText = (TextView) view.findViewById(R.id.news_content);
        newsTitleText.setText(newsTitle); // 刷新新聞的標題
        newsContentText.setText(newsContent); // 刷新新聞的內容
    }
}

這樣就把新聞內容的碎片和布局創建好了,但它們都是在雙頁模式下使用的,若要在單頁模式中使用,還需創建一個活動 NewsContentActivity,其布局 news_content.xml 中的代碼如下:

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

    <fragment
        android:id="@+id/news_content_fragment"
        android:name="com.wonderful.myfirstcode.chapter4.simple_news.NewsContentFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        />

</LinearLayout>

這里直接在布局中引入了 NewsContentFragment,相當于把 news_content_frag 布局的內容自動加了進來。

然后編寫 NewsContentActivity 的代碼如下:

public class NewsContentActivity extends AppCompatActivity {

    /**
     * 構建Intent,傳遞所需數據
     * @param context
     * @param newsTitle
     * @param newsContent
     */
    public static void actionStart(Context context, String newsTitle, String newsContent) {
        Intent intent = new Intent(context, NewsContentActivity.class);
        intent.putExtra("news_title", newsTitle);
        intent.putExtra("news_content", newsContent);
        context.startActivity(intent);
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.news_content);
        // 獲取傳入的新聞標題、新聞內容
        String newsTitle = getIntent().getStringExtra("news_title");
        String newsContent = getIntent().getStringExtra("news_content");
        // 獲取 NewsContentFragment 實例
        NewsContentFragment newsContentFragment = (NewsContentFragment) getSupportFragmentManager()
                .findFragmentById(R.id.news_content_fragment);
        // 刷新 NewsContentFragment 界面
        newsContentFragment.refresh(newsTitle, newsContent); 
    }
}

上述代碼,在 onCreate() 方法中通過 Intent 獲取傳入的新聞標題和內容,然后調用 FragmentManagerfindFragmentById() 方法得到 NewsContentFragment 的實例,接著調用它的 refresh() 方法,并將新聞的標題和內容傳入,顯示數據。(關于 actionStart() 方法可以閱讀前面的探究活動2.5.2相關筆記。)

接下來還需再創建顯示新聞列表的布局 news_title_frag.xml,如下:

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

    <android.support.v7.widget.RecyclerView
        android:id="@+id/news_title_recycler_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>

新建 news_item.xml 作為 上述 RecyclerView 子項的布局:

<TextView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/news_title"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:singleLine="true"
    android:ellipsize="end"
    android:textSize="18sp"
    android:padding="10dp"/>

子項的布局就只有一個 TextView

新聞列表和子項布局都創建好了,接下來就需要一個用于展示新聞列表的地方。這里新建 NewsTitleFragment 作為展示新聞列表的碎片:

/**
 * 新聞列表fragment
 * Created by KXwon on 2016/12/12.
 */

public class NewsTitleFragment extends Fragment{

    private boolean isTowPane;

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

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        if (getActivity().findViewById(R.id.news_content_layout)!= null){
            // 可以找到 news_content_layout 布局時,為雙頁模式
            isTowPane = true;
        }else {
            // 找不到 news_content_layout 布局時,為單頁模式
            isTowPane = false;
        }
    }
}

為實現上述 onActivityCreated() 方法中判斷當前時雙頁還是單頁模式,需要借助限定符,首先修改主布局 activity_news.xml 中的代碼:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/news_title_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <fragment
        android:id="@+id/news_title_fragment"
        android:name="com.wonderful.myfirstcode.chapter4.simple_news.NewsTitleFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</FrameLayout>

上述代碼表示,在單頁模式下只會加載一個新聞標題的碎片。

然后在 res 目錄下新建 layout-sw600dp 文件夾,在這個文件夾下再新建一個 activity_news.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/news_title_fragment"
        android:name="com.wonderful.myfirstcode.chapter4.simple_news.NewsTitleFragment"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1" />

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

        <fragment
            android:id="@+id/news_content_fragment"
            android:name="com.wonderful.myfirstcode.chapter4.simple_news.NewsContentFragment"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    </FrameLayout>

</LinearLayout>

上述代碼表示,在雙頁模式下會同時加載兩個碎片,并將新聞內容碎片放在 FrameLayout 布局下,這個布局 id 正是 news_content_layout。因此能找到這個 id 的時候就是雙頁模式,否則就是單頁模式。

現在已經將絕大多數工作完成了,剩下至關重要的一點,就是在 NewsTitleFragemt 中通過 RecyclerView 將新聞列表展示出來。接下來在 NewsTitleFragemt 中新建一個內部類 NewsAdapter 來作為 RecyclerView 的適配器,如下:

public class NewsTitleFragment extends Fragment{

    private boolean isTowPane;

    . . .
    
    class NewsAdapter extends RecyclerView.Adapter<NewsAdapter.ViewHolder> {

        private List<News> mNewsList;

        class ViewHolder extends RecyclerView.ViewHolder {

            TextView newsTitleText;

            public ViewHolder(View view) {
                super(view);
                newsTitleText = (TextView) view.findViewById(R.id.news_title);
            }
        }

        public NewsAdapter(List<News> newsList) {
            mNewsList = newsList;
        }

        @Override
        public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.news_item, parent, false);
            final ViewHolder holder = new ViewHolder(view);
            view.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    News news = mNewsList.get(holder.getAdapterPosition());
                    if (isTwoPane) {
                        // 若是雙頁模式,則刷新 NewsContentFragment 中的內容
                        NewsContentFragment newsContentFragment = (NewsContentFragment)
                                getFragmentManager().findFragmentById(R.id.news_content_fragment);
                        newsContentFragment.refresh(news.getTitle(), news.getContent());
                    } else {
                        // 若是單頁模式,則直接啟動 NewsContentActivity
                        NewsContentActivity.actionStart(getActivity(), news.getTitle(), news.getContent());
                    }
                }
            });
            return holder;
        }

        @Override
        public void onBindViewHolder(ViewHolder holder, int position) {
            News news = mNewsList.get(position);
            holder.newsTitleText.setText(news.getTitle());
        }

        @Override
        public int getItemCount() {
            return mNewsList.size();
        }

    }

需要注意的是,這里把適配器寫成內部類是為了直接訪問 NewsTitleFragment 的變量,比如 isTowPane

現在還剩最后一步收尾工作,就是向 RecyclerView 中填充數據了。修改 NewsTitleFragment 中的代碼如下:

public class NewsTitleFragment extends Fragment{

    . . .

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.news_title_frag, container, false);
        
        RecyclerView newsTitleRecyclerView = (RecyclerView) view.findViewById(R.id.news_title_recycler_view);
        LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity());
        newsTitleRecyclerView.setLayoutManager(layoutManager);
        NewsAdapter adapter = new NewsAdapter(getNews());
        newsTitleRecyclerView.setAdapter(adapter);
        
        return view;
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        if (getActivity().findViewById(R.id.news_content_layout) != null) {
            // 可以找到news_content_layout布局時,為雙頁模式
            isTwoPane = true;
        } else {
            // 找不到news_content_layout布局時,為單頁模式
            isTwoPane = false;
        }
    }

    /**
     * 初始化50條模擬新聞數據
     * @return
     */
    private List<News> getNews() {
        List<News> newsList = new ArrayList<>();
        for (int i = 1; i <= 50; i++) {
            News news = new News();
            news.setTitle("This is news title " + i);
            news.setContent(getRandomLengthContent("新聞內容吼吼吼" + i + "!"));
            newsList.add(news);
        }
        return newsList;
    }

    /**
     * 隨機生成不同長度的新聞內容
     * @param content
     * @return
     */
    private String getRandomLengthContent(String content) {
        Random random = new Random();
        int length = random.nextInt(20) + 1;
        StringBuilder builder = new StringBuilder();
        for (int i = 0; i < length; i++) {
            builder.append(content);
        }
        return builder.toString();
    }

    . . . 
}

到這里,所有的代碼編寫工作就完成了,運行程序,效果如下:

單頁模式的新聞列表界面

點擊一條新聞,會啟動一個新的活動來顯示新聞內容:

單頁模式的新聞內容界面

接下來把程序在平板上運行,同樣點擊一條新聞,效果如下:

雙頁模式的新聞標題和內容界面

好了,關于碎片的內容學習到這,下篇文章將學習廣播機制...

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容