文章出處

Fragment概述

在Fragment出現之前,Activity是app中界面的基本組成單位,值得一提的是,作為四大組件之一,它是需要“注冊”的。組件的特性使得一個Activity可以在整個app甚至是不同app間被復用。
隨著android 3.0中安卓平板的新增,app對不同尺寸屏幕的適配需求更加突出,Fragment大概也因為這樣的需要被引入。雖然可以為Activity動態指定不同的layout,但也僅僅是解決一些簡單的適配。像手機和平板這樣的顯著不同的尺寸下,是需要完全不同的界面設計的。此時,界面需要是不同幾個部分的組成,根據實際的屏幕大小,它們動態的組成一個界面或者是分離到不同界面中,經典的案例說明就是“列表-詳情”界面。雖然Activity可以相互嵌套,但在支持上(設計初衷)顯得很笨重,因此,sdk中引入了Fragment,它作為對Activity的一個模塊化拆分,類似Activity那樣的包含邏輯和View布局的“sub activity”,具有Activity那樣的生命周期和回退棧,可以接收事件等。通過Fragment來合理地分解一個復雜界面為多個模塊化的子界面,可以滿足一些動態的界面組合適配需求,同時也讓界面代碼更好的復用,并因為更加細分而設計清晰。此外,Fragment是無需注冊的,這樣它比Activity更加具備動態創建的可能性,基于此甚至出現了一些單一Activity這樣的app框架設計。更多它的特性,接下來就一起來探索吧。

基本使用

Fragment是API 11(android 3.0)中引入的,而support v4庫中提供了向前兼容的實現。下面為了突出重點,不使用對應的兼容實現,假設app的最低版本為api 11以上。
從設計上,Fragment應該是一個獨立完整的模塊化界面組件,包含自身的layout和界面交互邏輯。但在使用上,它離不開Activity,必須內嵌到一個Activity實例中。因為它就是設計來作為部分化的Activity,它的生命周期就是基于所在Activity的生命周期,沒必要獨立存在。
一個Fragment需要依附到Activity來進行顯示。可以通過在Activity的layout中使用

定義Fragment子類

類似定義Activity那樣,通過繼承Fragment類來定義一個fragment類型。特殊的是,“通常”唯一需要定義的方法是onCreateView(),它用來提供fragment關聯的layout,除非是一個無界面的Fragment。

下面是一個極簡單的Fragment定義:

import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.ViewGroup;

public class ArticleListFragment extends Fragment {
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
        Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.article_list, container, false);
    }
}

方法onCreateView()返回的View就是Fragment對應layout(ViewHierarchy)的root。
參數container是來自所在Activity的layout的一個ViewGroup,最終Fragment的布局被添加作為此
container的childView。inflate()方法最后一個參數表示是否將加載的view添加到container中,因為onCreateView()返回的view默認就會被Activity添加到container中,這里就傳遞false后方法inflate()返回布局的根View,否則inflate()會返回container參數,具體細節見方法inflate()
的說明。

方法inflate()原型:

/**
 * Inflate a new view hierarchy from the specified xml resource. Throws
 * {@link InflateException} if there is an error.
 *
 * @param resource ID for an XML layout resource to load (e.g.,
 *        <code>R.layout.main_page</code>)
 * @param root Optional view to be the parent of the generated hierarchy (if
 *        <em>attachToRoot</em> is true), or else simply an object that
 *        provides a set of LayoutParams values for root of the returned
 *        hierarchy (if <em>attachToRoot</em> is false.)
 * @param attachToRoot Whether the inflated hierarchy should be attached to
 *        the root parameter? If false, root is only used to create the
 *        correct subclass of LayoutParams for the root view in the XML.
 * @return The root View of the inflated hierarchy. If root was supplied and
 *         attachToRoot is true, this is root; otherwise it is the root of
 *         the inflated XML file.
 */
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)

一般的,定義一個Fragment時需要重寫Fragment的幾個生命周期中的回調方法包括:

  • onCreateView()
    系統調用此方法用來在第一次創建Fragment時,獲得其layout。
    方法原型:
/**
 * Called to have the fragment instantiate its user interface view.
 * This is optional, and non-graphical fragments can return null (which
 * is the default implementation).  This will be called between
 * {@link #onCreate(Bundle)} and {@link #onActivityCreated(Bundle)}.
 *
 * <p>If you return a View from here, you will later be called in
 * {@link #onDestroyView} when the view is being released.
 *
 * @param inflater The LayoutInflater object that can be used to inflate
 * any views in the fragment,
 * @param container If non-null, this is the parent view that the fragment's
 * UI should be attached to.  The fragment should not add the view itself,
 * but this can be used to generate the LayoutParams of the view.
 * @param savedInstanceState If non-null, this fragment is being re-constructed
 * from a previous saved state as given here.
 *
 * @return Return the View for the fragment's UI, or null.
 */
@Nullable
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
        @Nullable Bundle savedInstanceState) {
    return null;
}
  • onCreate()
    系統在開始創建Fragment時調用,這里做一些和Fragment相關狀態的初始化。

  • onPause()
    指示用戶離開Fragment所在Activity的使用調用,在這里做一些狀態保存的操作。

上面幾個方法基本就是Activity對應生命周期回調方法的一個調用傳遞,后面會在“Fragment生命周期”中詳細介紹各個回調方法的用途,接下來就看看如何在Activity中使用Fragment。

在layout中添加Fragment

接下來的示例是一個文章列表和文章詳情頁面的場景。app會在不同屏幕尺寸時動態選擇在同一個Activity中同時顯示文章列表和對應選擇的文章的詳情信息,或者單獨的一個列表界面,選擇一個文章后打開新Activity來顯示文章詳情。
下面是ArticleListActivity對應的布局文件,它里面同時聲明了2個Fragment作為其界面組成:

<?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.ArticleDetailFragment"
            android:id="@+id/viewer"
            android:layout_weight="2"
            android:layout_width="0dp"
            android:layout_height="match_parent" />
</LinearLayout>

通過上面這種在layout中使用<fragment>標簽的方式來為Activity添加Fragment的話,在Fragment中調用方法isInLayout()返回true,該方法用來指示當前Fragment實例是否處在一個View布局中。此外,可以重寫方法onInflate()來獲得標簽中定義的attr。

標簽<fragment>指定的Fragment在Activity的布局加載中會被實例化,onCreateView()返回的View將替換<fragment>元素在layout中的位置。
對應有View的Fragment,需要為它提供一個標識來訪問它,供系統在Activity的重建過程中使用,在對Fragment執行刪除,替換等操作時也要用到。提供標識的方式有:

  • 提供 android:id 屬性來指定一個唯一的整數ID,類似其它layout中的View那樣。
  • 提供 android:tag 來指定一個唯一的字符串標識。
  • 如果以上2者均未提供,默認會使用<fragment>標簽所在容器View的ID。

代碼添加Fragment

在Activity中通過代碼來添加Fragment時需要用到FragmentManager和FragmentTransaction。在Activity運行期間,對Fragment的操作是以事務的形式進行的,可以在一次事務中執行多個Fragment相關操作,最后提交事務。

示例如下:

FragmentManager fragmentManager = getFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();

ArticleFragment fragment = new ArticleFragment();
fragmentTransaction.add(R.id.layout_article_detail, fragment);
fragmentTransaction.commit();

上面的代碼完成了向布局中id為R.id.layout_article_detail的ViewGroup中添加一個fragment的操作。總之,帶界面的Fragment添加到Activity時,就需要指定其View所在的ViewGroup。

無界面Fragment

一些情況下,可以定義一個沒有界面的Fragment,此時它擁有關聯的Activity的各種回掉,狀態保存和恢復等。可以完成一些Activity運行期間的后臺操作。
無界面Fragment沒用關聯的View,不需要重寫onCreateView()方法。此時只會以代碼的方式添加它到Activity,并且使用的是FragmentTransaction.add(Fragment fragment, String tag)方法,此時fragment實例不需要ID來標識它,因為它不存在于layout中,使用一個String類型的tag來標記它,之后可以通過FragmentManager.findFragmentByTag(String tag)來訪問它。

Fragment的管理

上面簡單的在代碼中添加了一個Fragment到當前Activity。這里對可以針對Fragment執行的各種操作進行總結。

FragmentManager

首先通過getFragmentManager() 得到一個FragmentManager對象。使用它可以完成以下操作:

  • 獲得activity中的fragment實例。findFragmentById()方法用來獲取提供UI到Activity的layout中的fragment。findFragmentByTag()用來獲取無界面的fragment。
  • 使用方法popBackStack()對Activity管理的Fragment的回退棧執行出棧操作。
  • 提供方法addOnBackStackChangedListener()來添加一個監聽器來響應回退棧的變化。

FragmentTransaction和回退棧

在Activity運行期間,可以動態地添加,移除fragment來改變界面顯示和行為。這些關于Fragment的修改操作,需要通過FragmentTransaction 來完成,像下面這樣獲得它:

FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();

FragmentTransaction提供的有關Fragment的操作有:

  • add()
    add系列方法用來添加一個Fragment到Activity。如add(int containerViewId, Fragment fragment, String tag)用來將參數fragment對象添加到containerViewId表示的layout中的ViewGroup的位置。可選的tag字符串用來對fragment進行標識。這樣以后可以通過FragmentManager.findFragmentByTag(String tag)來獲取它(因為fragment也會發生restore這樣的過程,不能像普通對象那樣簡單的保持其引用來保持它)。

  • hide(Fragment fragment)
    當參數fragment的View已經添加到布局容器中時,可以通過此方法來隱藏對應的View。
    Hides an existing fragment. This is only relevant for fragments whose views have been added to a container, as this will cause the view to be hidden.

  • remove(Fragment fragment)
    Remove an existing fragment. If it was added to a container, its view is also removed from that container.

  • replace (int containerViewId, Fragment fragment, String tag)
    Replace an existing fragment that was added to a container. This is essentially the same as calling remove(Fragment) for all currently added fragments that were added with the same containerViewId and then add(int, Fragment, String) with the same arguments given here.

  • show (Fragment fragment)
    Shows a previously hidden fragment. This is only relevant for fragments whose views have been added to a container, as this will cause the view to be shown.

每一次事務包含一或多個相關Fragment的修改,如add(), remove(), replace()。事務可以記錄到activity關聯的Fragment的回退棧中。之后用戶按下返回鍵時,本次事務將“回滾”。

// Create new fragment and transaction
Fragment newFragment = new ExampleFragment();
FragmentTransaction transaction = getFragmentManager().beginTransaction();

// Replace whatever is in the fragment_container view with this fragment,
// and add the transaction to the back stack
transaction.replace(R.id.fragment_container, newFragment);
transaction.addToBackStack(null);

// Commit the transaction
transaction.commit();

上面的代碼,使用newFragment替換掉了對應ViewGroup處的fragment,并將事務添加到了回退棧中,這使得以前的fragment不會被銷毀,不執行onDestroyView()而僅僅執行了onStop()方法。之后若用戶按下返回鍵,那么newFragment關閉后先前對應的fragment會繼續恢復顯示。

在一次fragment事務中,最后必須調用commit()來提交事務,若添加多個fragment到一個布局容器中,那么添加順序決定了它們對應View的展示順序,類似ViewGroup.addView(View child)那樣。

Fragment切換動畫

為了讓fragment切換時具有動畫效果,可以調用setTransition()、setCustomAnimations()等來指定切換動畫。

commit()

commit()的調用不會立即引起fragment變化操作的執行,而是將事務安排到UI線程中——UI線程準備好就會執行它。可以通過調用FragmentManager.executePendingTransactions()讓提交的事務立即執行,如果其它異步任務有這樣的需求的話。

commitAllowingStateLoss().

當用戶離開Activity時(打開其它界面,返回桌面等),Activity的生命周期回調會執行狀態保存操作,此時如果執行fragment事務的commit()將引發異常,原因是當Activity之后發生重建過程,事務提交后的狀態可能丟失,如果這在設計上不是問題的話,可以選擇執行commitAllowingStateLoss()。

Fragment的生命周期

Fragment的設計目標就是表現得像一個Activity的一部分,實現上它必須添加到Activity中運行。重要的一點,Fragment的View是插入到Activity的layout的一部分存在的。
因為界面組件的屬性,Fragment具備像Activity那樣的生命周期回調方法,大多數方法本身就是Activity對應方法的一個調用傳遞,另一些方法是和Fragment的界面生成相關,或和宿主Activity進行交互的。

有關Fragment的完整生命周期還是蠻復雜的,這個github項目是關于它的一個完整的演示。通常app只用到一些常見的生命周期方法,也是api文檔中著重說明的,下面就學習下這些常見的回調。

Lifecycle圖解

下圖是Activity運行時期(resumed狀態),Fragment從添加到移除過程中各個生命周期回調的執行狀況:

fragment_lifecycle

作為對比,下圖表明了宿主Activity生命周期對Fragment的影響:

activity_fragment_lifecycle

如圖所示,Activity生命周期的變化會直接影響其包含的Fragment。假設以layout文件中

在Activity的onCreate階段

  • onAttach()
    原型:public void onAttach (Activity activity)
    在Fragment和它寄宿的Activity關聯時調用,方法參數就是關聯的Activity。此時Activity可能處在onCreate階段或resumed階段。

  • onCreate()
    原型:public void onCreate (Bundle savedInstanceState)
    創建Fragment時執行,在此作一些狀態初始化操作。方法在onAttach(Activity) 之后在 onCreateView(LayoutInflater, ViewGroup, Bundle)之前執行。
    此時宿主Activity可能處于創建過程(onCreate執行期間,或者是出于resumed階段),對于ViewHierarchy還未創建完成,所以不要做和View相關的操作,稍后的onActivityCreated()方法是Activity.onCreate()執行完畢后調用,可以在那里做一些Activity創建完成后的初始化操作。

類似Activity那樣,如果Fragment是從之前的狀態恢復重建,則參數savedInstanceState攜帶了之前保存的狀態數據。

  • onCreateView()
    原型:public View onCreateView (LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
    Activity獲取Fragment提供的View時執行。

  • onViewCreated()
    原型:public void onViewCreated(View view, Bundle savedInstanceState)
    onCreateView()返回的view hierarchy創建完成,但還未被添加到Activity布局中的ViewGroup中。可以在此訪問Fragment的各個View,進行設置。

  • onActivityCreated()
    原型:public void onActivityCreated (Bundle savedInstanceState)
    宿主Activity已完成創建,其onCreate()執行完畢。Fragment的View準備就緒,可以在此執行創建過程的最后初始化操作,如獲得View對象,恢復狀態等。

經過上面幾個方法的執行,宿主Activity及Fragment的創建過程已經完成。顯然,這些創建過程的回調方法僅執行一次
如果是在Activity創建時添加Fragment,那么上述方法會嚴格受到宿主Activity的onCreate()執行的影響。
大多數情況下,對Fragment的使用正是通過

onStart,onResume,onPause,onStop

在用戶離開和返回宿主Activity,或對Fragment執行replace,hide等操作時,它對應執行以下回調。

  • onStart()
    當fragment對用戶可見時調用,前提是宿主Activity已處于started狀態。同時也是宿主Activity.onStart的一個綁定調用。

  • onResume()
    fragment的界面可見且可以交互時調用,前提是宿主Activity已處于resumed狀態。同時也是宿主Activity.onResume()的一個綁定調用。

  • onPause()
    Fragment不再處于resumed狀態,一般是宿主Activity轉為paused,或是正被通過fragment事務所修改。

  • onStop()
    fragment不再可見,由于宿主Activity轉為stopped,或它正被執行fragment相關操作。

在Activity的onDestroy階段

當Fragment被移除,或者宿主Activity銷毀時,它將依次經歷下面的回調。

  • onDestroyView()
    通知Fragment其關聯的View將被移除宿主Activity,下次顯示是重新執行onCreateView()創建View。如果fragment提供了View的話,可以在此完成一些有關View的清理操作,此時View的狀態已保存還未從其parent中移除。

  • onDestroy()
    fragment不再被使用,執行最后的清理。

  • onDetach ()
    fragment不再和Activity關聯。

Fragment的狀態

類似Activity那樣,一個Fragment實例處在運行中時,只能是以下狀態之一。

  • Resumed
    The fragment is visible in the running activity.

  • Paused
    Another activity is in the foreground and has focus, but the activity in which this fragment lives is still visible (the foreground activity is partially transparent or doesn't cover the entire screen).

  • Stopped
    The fragment is not visible. Either the host activity has been stopped or the fragment has been removed from the activity but added to the back stack. A stopped fragment is still alive (all state and member information is retained by the system). However, it is no longer visible to the user and will be killed if the activity is killed.

回調方法中需要注意的

由于Fragment對象是一個具有生命周期的特殊對象,所以在它的代碼中時刻注意一些操作的調用時機,下面列舉一些。

  • getActivity()
    一般在Fragment中會調用getActivity()來獲得關聯的Avtivity當作Context來使用。而這個方法在Fragment還未attached或已經detached后 期間均返回null。

  • resumed狀態下fragment的操作
    當Activity處于resumed 狀態后,此時可以通過FragmentTransaction來執行各種fragment操作。此時fragment實例的生命周期回調是獨立與宿主Activity的。比如點擊一個按鈕來為Activity添加Fragment,此時fragment實例直接依次執行onAttach、onCreate...onResume等一系列方法,而Activity則一直處于resumed狀態。
    一旦Activity執行onPause離開resumed狀態時,其包含的各個Fragment的相應生命周期回調繼續被綁定執行。

狀態保持和回退棧

作為一個“模塊化的界面組件”,Fragment有類似Activity那樣的狀態保持和恢復機制:當一個未被顯式結束的Activity處在后臺時,由于內存問題它臨時被回收掉,之后若用戶再次回來時,對應任務棧中的Activity會被重新創建,而框架提供了和此Activity關聯的一些狀態保存和恢復的方法。

在Fragment中,重寫onSaveInstanceState()來保存一些狀態。
之后在 onCreate(), onCreateView(),或 onActivityCreated()中獲取保存的狀態,進行恢復設置。

另一個Fragment的特性就是“回退棧”。
在通過FragmentTransaction來執行有關fragment的事務時,可以通過addToBackStack()來添加此次事務的操作到回退棧中,這樣以后,用戶按下返回鍵后Activity的Fragment狀態會恢復為上一次的情形,當回退棧中沒有任何Fragment時,才執行Activity本身的onBackPressed()邏輯。

更多關于回退棧的用法,參見FragmentManager的API。

其它方法

  • isInLayout()和onInflate()
    若使用<fragment>標簽添加一個Fragment實例,那么在Fragment的代碼中可以通過方法isInLayout()來驗證這一點,通常上述方式時方法會返回true。而且可以重寫 onInflate (Activity activity, AttributeSet attrs, Bundle savedInstanceState)方法來獲得布局文件中標簽定義的一些attrs。
    例外的情況是,當Activity重建時采用了不同的layout,之前layout中的fragment還會被重新實例化,但此時此fragment對象的View已經不再使用了,此時它的onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)調用時參數container為null,此時方法無需返回任何View。而此時isInLayout()返回false。

和Activity通信

Fragment難免需要和宿主Activity進行交互,從設計原則上看,因為Fragment表示一個模塊化的可復用界面組件,所以它應該可以被放置在不同的Activity中。出于這樣的原因,它和其宿主Activity的交互只能是通過接口來進行抽象。

  • 首先在Fragment中定義一個接口來抽象需要交互的Activity。
  • 之后宿主Activity實現此接口,在onAttach()回調中(或者其它創建階段的回調方法中調用getActivity)可以將得到的Activity實例保存到字段,作為接口實例。
  • 其它地方通過此接口實例來完成和Activity的交互。

如果沒有特殊的抽象需要,Activity本身是完全可以直接調用Fragment的公開方法的。可以獲得FragmentManager,然后通過findFragmentById() 或 findFragmentByTag()得到目標Fragment。
當然,在Fragment中也可以getActivity()得到Activity示例然后強轉為已知的Activity類型去使用。

Fragment也可以為宿主Activity提供Options Menu、Context Menu和Action Bar菜單,并處理這些動作響應。

和其它Activity通信

不同Fragment之間不能直接進行交互,因為它們應該可以進行各種不同的界面動態組合。最好的情況是,這些Fragment之間是相互不知道的,它們各自具備其獨立的界面和行為。
各個Fragment都定義自己的接口來和Activity交互,而它們之間的交互邏輯是Activity本身負責的。
例如,ArticleListFragment表示的文章列表中一個文章被選中后,它通知Activity,然后Activity在根據此Article數據來操作ArticleFragment去顯示對應文章內容。

Fragment的參數

雖然可以像普通對象那樣,在創建Fragment后直接通過property來設置一些狀態。但是,Fragment具有像Activity那樣的,在intent中攜帶一些數據的能力。

可以通過setArguments()來為Fragment設置一些額外數據,像下面這樣:

ArticleFragment f = new ArticleFragment();

// Supply index input as an argument.
Bundle args = new Bundle();
args.putSerializable("article", article);
f.setArguments(args);

return f;

之后,Fragment中使用下面的方式獲得這些數據:

Article article = (Article) getArguments().getSerializable ("article");

通常,Activity在添加一個fragment時,可以直接把它的intent的Bundle數據設置給fragment,這在啟動Activity的intent中攜帶的數據是給目標fragment時非常方便:

fragment.setArguments(getIntent().getExtras());

通過setArguments設置的數據,在fragment銷毀后重建時也可以獲取到。不像普通對象那樣,只存在于內存中。
這點類似于Activity的intent的數據,起到類似狀態保持那樣的效果——構建參數(construction arguments )的保持。

列表-詳情案例

接下來就給出顯示“文章列表,文章詳情界面”示例的完整代碼,列表和詳情界面分別由兩個Fragment表示,在大尺寸平板這樣的屏幕時它們處在同一個Activity中。小尺寸手機設備上時,分別處在兩個Activity中。界面效果如下:

fragments_for_screens

Article

數據實體類Article的定義:

public class Article implements Serializable {
    public String title;
    public String content;
}

一個文章包含標題和內容。

列表:ArticleListFragment

定義ArticleListFragment來顯示文章列表,列表項是文章標題。
使用RecyclerView來顯示列表,列表項用TextView顯示。

ArticleListFragment對應布局文件fragment_article_list.xml:

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/list"
    android:name="com.example.android.ItemFragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

ArticleListFragment定義:

public class ArticleFragment extends Fragment {
    private OnArticleCheckedListener mListener;
    private ArrayList<Article> mArticles;

    public ArticleFragment() {
        mArticles = new ArrayList<>();

        for (int i = 0; i < 24; i++) {
            Article article = new Article();
            article.title = "標題-" + i;
            article.content = "文章內容-" + i;

            mArticles.add(article);
        }
    }

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

        if (view instanceof RecyclerView) {
            RecyclerView recyclerView = (RecyclerView) view;
            recyclerView.setLayoutManager(new LinearLayoutManager(getActivity(), LinearLayoutManager.VERTICAL, false));
            recyclerView.setAdapter(new ArticleListViewAdapter(mArticles, mListener));
        }
        return view;
    }

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        if (context instanceof OnArticleCheckedListener) {
            mListener = (OnArticleCheckedListener) context;
        }
    }

    @Override
    public void onDetach() {
        super.onDetach();
        mListener = null;
    }

    public interface OnArticleCheckedListener {
        void onArticleChecked(Article article);
    }
}

接口OnArticleCheckedListener定義了某個文章標題被點擊查看時的回調。
onAttach()得到的context正是宿主Activity,按照設計,宿主Activity應該實現了OnArticleCheckedListener,這樣ArticleListFragment列表項的點擊事件就傳遞給了宿主Activity。

適配器ArticleListViewAdapter的定義:

public class ArticleListViewAdapter extends RecyclerView.Adapter<ArticleListViewAdapter.ViewHolder> {

    private final List<Article> mArticles;
    private final ArticleFragment.OnArticleCheckedListener mListener;

    public ArticleListViewAdapter(List<Article> items, ArticleFragment.OnArticleCheckedListener listener) {
        mArticles = items;
        mListener = listener;
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.fragment_article_title, parent, false);
        return new ViewHolder(view);
    }

    @Override
    public void onBindViewHolder(final ViewHolder holder, int position) {
        holder.mArticle = mArticles.get(position);
        holder.mContentView.setText(mArticles.get(position).content);

        holder.mView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (null != mListener) {
                    mListener.onArticleChecked(holder.mArticle);
                }
            }
        });
    }

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

    public class ViewHolder extends RecyclerView.ViewHolder {
        public final View mView;
        public final TextView mContentView;
        public Article mArticle;

        public ViewHolder(View view) {
            super(view);
            mView = view;
            mContentView = (TextView) view.findViewById(R.id.content);
        }
    }
}

布局文件fragment_article_title.xml簡單的定義了一個TextView用來顯示標題:

<?xml version="1.0" encoding="utf-8"?>
<TextView   xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/content"
    android:textSize="32sp"
    android:gravity="center"
    android:layout_width="match_parent"
    android:layout_height="40dp"/>

以上代碼完成了ArticleListFragment的布局和類型定義。

ArticleDetailFragment

ArticleDetailFragment用來顯示具體文章的內容,它的布局文件fragment_article_detail.xml:

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/article_content"
    android:name="com.example.android.ItemFragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

布局中使用一個TextView簡單都顯示文章內容。

ArticleDetailFragment類型定義:

public class ArticleDetailFragment extends Fragment {
    public static final String ARG_ARTICLE = "ARG_ARTICLE";
    private Article mArticle;

    public ArticleDetailFragment() {
    }

    public static ArticleDetailFragment newInstance(Article article) {
        ArticleDetailFragment fragment = new ArticleDetailFragment();
        Bundle bundle = new Bundle();
        bundle.putSerializable(ARG_ARTICLE, article);
        fragment.setArguments(bundle);

        return fragment;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mArticle = (Article) getArguments().getSerializable(ARG_ARTICLE);
    }

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

        if (mArticle != null) {
            TextView textView = (TextView) view.findViewById(R.id.article_content);
            textView.setText(mArticle.content);
        }

        return view;
    }
}

方法newInstance()方便創建一個攜帶參數的實例。

ArticleListActivity

對應宿主ArticleListActivity,它在大尺寸屏幕下可以同時顯示列表和文章詳情,在小尺寸屏幕下只顯示標題列表。所以在res/layout-large/和
res/layout/兩個目錄下分別為它定義不同的布局文件。

  • res/layout/activity_articles.xml

在小尺寸屏幕時,Activity只顯示ArticleListFragment,上面利用

  • res/layout-large/activity_articles.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/layout_root"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal"
    tools:context="com.example.android.ArticleListActivity">

    <fragment
        android:id="@+id/list_fragment"
        android:layout_weight="2"
        android:name="com.example.android.ArticleListFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

    <FrameLayout
        android:id="@+id/detail_container"
        android:layout_weight="2"
        android:layout_width="match_parent"
        android:layout_height="match_parent"></FrameLayout>

</LinearLayout>

在大尺寸屏幕下,布局中同時提供了R.id.detail_container作為ArticleDetailFragment的容器,這樣在選擇查看某個標題時,
就會在同一個界面使用ArticleDetailFragment來顯示文章內容。

下面是ArticleListActivity的定義,它的onCreate()中根據布局文件是否包含R.id.detail_container來判斷當前屏幕是處于大屏幕——顯示兩個內容面板(towPanel)還是小屏幕(singlePanel)。

public class ArticleListActivity extends Activity implements ArticleListFragment.OnArticleCheckedListener {
    private boolean mIsTwoPanel = false;

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

        View container = findViewById(R.id.detail_container);
        mIsTwoPanel = container != null;
    }

    @Override
    public void onArticleChecked(Article article) {
        if (mIsTwoPanel) {
            Fragment fragment = ArticleDetailFragment.newInstance(article);
            getFragmentManager().beginTransaction().replace(R.id.detail_container, fragment).commit();
        } else {
            Intent intent = new Intent(this, ArticleDetailActivity.class);
            intent.putExtra(ArticleDetailFragment.ARG_ARTICLE, article);
            startActivity(intent);
        }
    }
}

ArticleListActivity實現了OnArticleCheckedListener接口,在方法onArticleChecked()中,若當前Activity是大屏幕對應的布局,
則直接替換R.id.detail_container處的fragment用來顯示文章內容。否則,啟動一個新的ArticleDetailActivity來顯示文章內容。
ArticleDetailActivity的定義非常簡單,它作為ArticleDetailFragment的宿主,將article參數設置給它。

(本文使用Atom編寫)


文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()