Fragment的全面解析

                                         Fragment

1. Fragment概要介紹

Fragment是activity的界面中的一部分或一種行爲。你能夠把多個Fragment們組合到一個activity中來建立一個多面界面而且你能夠在多個activity中重用一個Fragment。你能夠把Fragment認爲模塊化的一段activity,它具備本身的生命週期,接收它本身的事件,並能夠在activity運行時被添加或刪除。java

Fragment不能獨立存在,它必須嵌入到activity中,並且Fragment的生命週期直接受所在的activity的影響。例如:當activity暫停時,它擁有的全部的Fragment們都暫停了,當activity銷燬時,它擁有的全部Fragment們都被銷燬。然而,當activity運行時(在onResume()以後,onPause()以前),你能夠單獨地操做每一個Fragment,好比添加或刪除它們。當你在執行上述針對Fragment的事務時,你能夠將事務添加到一個棧中,這個棧被activity管理,棧中的每一條都是一個Fragment的一次事務。有了這個棧,就能夠反向執行Fragment的事務,這樣就能夠在Fragment級支持「返回」鍵(向後導航)。android

當向activity中添加一個Fragment時,它須置於ViewGroup控件中,而且需定義Fragment本身的界面。你能夠在layout.xml文件中聲明Fragment,元素爲:<fragment>;也能夠在代碼中建立Fragment,而後把它加入到ViewGroup控件中。然而,Fragment不必定非要放在activity的界面中,它能夠隱藏在後臺爲actvitiy工做。編程

本章描述如何使用fragment,包括fragment在加入activity的後退棧中時如何保持本身的狀態,如何與activity以及其它fragment們共享事件,如何顯示在activity的動做欄,等等。api

2. 設計哲學

Android從3.0開始引入fragment,主要是爲了支持更動態更靈活的界面設計,好比在平板上的應用。平板機上擁有比手機更大的屏幕空間來組合和交互界面組件們。Fragment使你在作那樣的設計時,不需應付view樹中複雜的變化。經過把activity的layout分紅fragment,你能夠在activity運行時改變它的樣子,而且能夠在activity的後退棧中保存這些改變。app

例如:寫一個讀新聞的程序,能夠用一個fragment顯示標題列表,另外一個fragment顯示選中標題的內容,這兩個fragment都在一個activity上,並排顯示。那麼這兩個fragment都有本身的生命週期並響應本身感興趣的事件。因而,不需再像手機上那樣用一個activity顯示標題列表,用另外一個activity顯示新聞內容;如今能夠把二者放在一個activity上同時顯示出來。以下圖:ide

Fragment必須被寫成可重用的模塊。由於fragment有本身的layout,本身進行事件響應,擁有本身的生命週期和行爲,因此你能夠在多個activity中包含同一個Fragment的不一樣實例。這對於讓你的界面在不一樣的屏幕尺寸下都能給用戶完美的體驗尤爲重要。好比你能夠在程序運行於大屏幕中時啓動包含不少fragment的activity,而在運行於小屏幕時啓動一個包含少許fragment的activity。模塊化

舉個例子--仍是剛纔那個讀新聞的程序-當你檢測到程序運行於大屏幕時,啓動activityA,你將標題列表和新聞內容這兩個fragment都放在activityA中;當檢測到程序運行於小屏幕時,仍是啓動activityA,但此時A中只有標題列表fragment,當選中一個標題時,activityA啓動activityB,B中含有新聞內容fragment。函數

2.建立Fragmentoop

要建立fragment,必須從Fragment或Fragment的派生類派生出一個類。Fragment的代碼寫起來有些像activity。它具備跟activity同樣的回調方法,好比 onCreate(),onStart(),onPause()和onStop()。實際上,若是你想把老的程序改成使用fragment,基本上只須要把activity的回調方法的代碼移到fragment中對應的方法便可。佈局

一般須要實現以上生命週期函數:

onCreate()

當建立Fragment時系統調用這個方法。在你的實現中,你應該初始化哪些在Fragment暫停態、終止態、和恢復態時想要保持狀態的的Fragment組件。

onCreateView()

當第一次用Fragment來描畫用戶界面時,系統調用這個方法。要用你的Fragment來描畫一個UI界面,你必須從這個方法中返回一個View,這個View是Fragment佈局的根。若是Fragment沒有提供UI界面,那麼它返回一個null。

onPause()

在用戶要離開Fragment的第一時刻系統會調用這個方法(但這並意味着Fragment要被銷燬)。一般應該在這兒提交本次用戶會話以外的要持久化的改變(由於用戶可能再也不回來)。

對於每一個Fragment,大多數應用程序應該至少實現這三個方法,可是你也應該使用其它的回調方法來處理Fragment生命週期的各類狀態。

下圖爲fragment的生命週期(它所在的activity處於運行狀態)。

Fragment is added--->onAttach()---.onCreate()

還有幾個現成的fragemtn的派生類,你可能須要從它們派生,以下所列:

DialogFragment

顯示一個浮動的對話框。使用這個類建立對話框是替代activity建立對話框的最佳選擇.由於你能夠把fragmentdialog放入到activity的返回棧中,使用戶能再返回到這個對話框。

ListFragment

顯示一個列表控件,就像ListActivity類,它提供了不少管理列表的方法,好比onListItemClick()方法響應click事件。

PreferenceFragment

顯示一個由Preference對象組成的列表,與PreferenceActivity相同。它用於爲程序建立「設置」activity。

2.1爲fragment添加用戶界面

一般,Fragment是做爲Activity用戶界面的一部分來使用的,而且它會給Activity提供本身的佈局。

要給Fragment提供一個佈局,你必須實現onCreateView()回調方法,系統在給Fragment描畫布局的時候會調用這個方法。這個方法的實現必須返回一個View,它是Fragment佈局的根。

注:若是你的的Fragment是ListFragment的子類,默認的實現是從onCreateView()方法中返回一個ListView(),所以你不須要實現它。

爲了從onCreateView()方法中返回一個佈局,你可以經過XML文件中的一個佈局資源的定義來填充它。爲了幫助你作這件事,onCreateView()方法提供了一個LayoutInflater對象。

例如,Fragment的一個子類經過example_fragment.xml文件加載一個佈局:

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);
    }
}

onCreateView()參數中的container是存放fragment的layout的ViewGroup對象。savedInstanceState參數是一個Bundle,跟activity的onCreate()中Bundle差很少,用於狀態恢復。可是fragment的onCreate()中也有Bundle參數,因此此處的Bundle中存放的數據與onCreate()中存放的數據仍是不一樣的。

Inflate()方法有三個參數:

1.layout的資源ID。

2.存放fragment的layout的ViewGroup。

3.布爾型數據表示是否在建立fragment的layout期間,把layout附加到container上(在這個例子中,由於系統已經把layout插入到container中了,因此值爲false,若是爲true會導至在最終的layout中建立多餘的ViewGroup)。

如今你看到如何爲fragment建立layout了,下面講述如何把它添加到activity中。

2.2把fragment添加到activity

通常狀況下,fragment把它的layout做爲activitiy的loyout的一部分合併到activity中,有兩種方法將一個fragment添加到activity中:

1.在activity的layout.xml文件中聲明fragment

以下代碼,一個activity中包含兩個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.ArticleReaderFragment"
            android:id="@+id/viewer"
            android:layout_weight="2"
            android:layout_width="0dp"
            android:layout_height="match_parent" />
</LinearLayout>

在<fragment>元素中的android:name屬性指定了在佈局中要實例化的Fragment。

當系統建立這個Activity佈局時,它實例化在佈局中指定的每個Fragment,而且分別調用onCreateView(),來獲取每一個Fragment的佈局。而後系統會在Activity佈局中插入經過<fragment>元素中聲明直接返回的視圖。

注:每一個Fragment須要一個惟一的標識,這樣可以在Activity被重啓時系統使用這個ID來恢復Fragment(而且你可以使用這個ID獲取執行事務的Fragment,如刪除)。有三種給Fragment提供ID的方法:

. 使用android:id屬性來設置惟一ID;

. 使用android:tag屬性來設置惟一的字符串;

. 若是沒有設置前面兩個屬性,系統會使用容器視圖的ID。

靜態添加Fragment到Activity示例:

title_fragment_layout.java

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="45dp"
    android:background="#00aa00">

    <ImageButton
        android:id="@+id/id_title_left_btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:layout_marginLeft="3dp"
        android:background="@mipmap/ic_launcher" />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:text="使用TitleFragment作標題欄"
        android:textColor="#fff"
        android:textSize="20sp"
        android:textStyle="bold" />

</RelativeLayout>

TitleFragment.java

public class TitleFragment extends Fragment
{
    private ImageButton mLeftMenu;
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState)
    {
        View view = inflater.inflate(R.layout.title_fragment_layout, container, false);
        mLeftMenu = (ImageButton) view.findViewById(R.id.id_title_left_btn);
        mLeftMenu.setOnClickListener(new View.OnClickListener()
        {
            @Override
            public void onClick(View v)
            {
                Toast.makeText(getActivity(),
                        "i am an ImageButton in TitleFragment ! ",
                        Toast.LENGTH_SHORT).show();
            }
        });
        return view;
    }
}

content_fragment_layout.java

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:text="使用ContentFragment作主面板"
        android:textSize="20sp"
        android:textStyle="bold" />

</LinearLayout>

ContentFragment.java

public class ContentFragment extends Fragment {
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        return inflater.inflate(R.layout.content_fragment_layout, container, false);
    }
}

activity_main.xml

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

    <fragment
        android:id="@+id/id_fragment_title"
        android:name="com.smart.myapplication.TitleFragment"
        android:layout_width="match_parent"
        android:layout_height="45dp" />

    <fragment
        android:layout_below="@id/id_fragment_title"
        android:id="@+id/id_fragment_content"
        android:name="com.smart.myapplication.ContentFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</RelativeLayout>

MainActivity.java

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        setContentView(R.layout.activity_main);
    }
}

2.編程給一個既存的ViewGroup添加Fragment

在Activity運行的任什麼時候候,均可以把Fragment添加到Activity佈局中。你只須要指定一個放置Fragment的ViewGroup。要在Activity中使用Fragment事務(如添加、刪除、或替換Fragment),必須使用來自FragmentTransaction的APIs。你可以向下面例子那樣從Activity中獲取一個FragmentTransaction實例:

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

而後,你可以使用add()方法把Fragment添加到指定的視圖中,如:

ExampleFragment fragment = new ExampleFragment();
fragmentTransaction.add(R.id.fragment_container, fragment);
fragmentTransaction.commit();

傳遞給add()方法的第一個參數是Fragment應該被放入的ViewGroup,經過資源ID來指定這個ViewGroup,第二個參數是要添加的Fragment。

一旦FragmentTransaction對象發生了改變,就必須調用commit方法來提交改變的影響。

動態添加Fragment到Activity示例:

friend_fragment_layout.java

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >
    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:text="使用FriendFragment作主面板"
        android:textSize="20sp"
        android:textStyle="bold" />
</LinearLayout>

FriendFragment.java

public class FriendFragment extends Fragment {

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

activity_main.xml

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

    <fragment
        android:id="@+id/id_fragment_title"
        android:name="com.smart.myapplication.TitleFragment"
        android:layout_width="match_parent"
        android:layout_height="45dp" />
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/id_fragment_title"
        android:onClick="replaceFragment"
        android:text="切換"/>
    <FrameLayout
        android:id="@+id/id_content"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_below="@id/id_fragment_title" />
</RelativeLayout>

MainActivity.java

public class MainActivity extends AppCompatActivity {

    private Fragment contentFragment;
    private Fragment friendFragment;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        setContentView(R.layout.activity_main);

        FragmentManager fm = getFragmentManager();
        FragmentTransaction transaction = fm.beginTransaction();//開啓一個事務
        contentFragment = new ContentFragment();
        transaction.add(R.id.id_content, contentFragment);//往Activity中添加一個Fragment
        transaction.commit();
    }

    public void replaceFragment(View v){
        FragmentManager fm = getFragmentManager();
        FragmentTransaction transaction = fm.beginTransaction();
        friendFragment = (FriendFragment)fm.findFragmentById(R.id.id_content);
        if(null == friendFragment){
            friendFragment = new FriendFragment();
        }
        //使用另外一個Fragment替換當前的,實際上就是remove()而後add()的合體
        transaction.replace(R.id.id_content, friendFragment);
        transaction.commit();
    }
}
一、爲何須要判null呢?
主要是由於,當Activity由於配置發生改變(屏幕旋轉)或者內存不足被系統殺死,形成從新建立時,咱們的fragment會被保存下來,可是會建立新的FragmentManager,新的FragmentManager會首先會去獲取保存下來的fragment隊列,重建fragment隊列,從而恢復以前的狀態。
二、add(R.id.id_fragment_container,mContentFragment)中的佈局的id有何做用?
一方面呢,是告知FragmentManager,此fragment的位置;另外一方面是此fragment的惟一標識;就像咱們上面經過fm.findFragmentById(R.id.id_fragment_container)查找

transaction.add()

往Activity中添加一個Fragment

transaction.remove()

從Activity中移除一個Fragment,若是被移除的Fragment沒有添加到回退棧,這個Fragment實例將會被銷燬。

transaction.replace()

使用另外一個Fragment替換當前的,實際上就是remove()而後add()的合體

transaction.hide()

隱藏當前的Fragment,僅僅是設爲不可見,並不會銷燬

transaction.show()

顯示以前隱藏的Fragment

detach()

會將view從UI中移除,和remove()不一樣,此時fragment的狀態依然由FragmentManager維護。

attach()

重建view視圖,附加到UI上並顯示。

注意:經常使用Fragment可能會常常遇到這樣Activity狀態不一致:State loss這樣的錯誤。主要是由於:commit方法必定要在Activity.onSaveInstance()以前調用。

上述,基本是操做Fragment的全部的方式了,在一個事務開啓到提交能夠進行多個的添加、移除、替換等操做。

值得注意的是:若是你喜歡使用Fragment,必定要清楚這些方法,哪一個會銷燬視圖,哪一個會銷燬實例,哪一個僅僅只是隱藏,這樣才能更好的使用它們。

a、好比:我在FragmentA中的EditText填了一些數據,當切換到FragmentB時,若是但願會到A還能看到數據,則適合你的就是hide和show;也就是說,但願保留用戶操做的面板,你可使用hide和show,固然了不要使勁在那new實例,進行下非null判斷。

b、再好比:我不但願保留用戶操做,你可使用remove(),而後add();或者使用replace()這個和remove,add是相同的效果。

c、remove和detach有一點細微的區別,在不考慮回退棧的狀況下,remove會銷燬整個Fragment實例,而detach則只是銷燬其視圖結構,實例並不會被銷燬。那麼兩者怎麼取捨使用呢?若是你的當前Activity一直存在,那麼在不但願保留用戶操做的時候,你能夠優先使用detach。

2.3添加一個沒有UI的Fragment

上面的例子顯示了怎樣把Fragment做爲UI的一部分添加到Activity上,可是,你也可以使用Fragment只提供一個後臺行爲,而沒有額外的UI展示。

要添加一個沒有UI的Fragment,須要在Activity中使用add(Fragment,String)(給Fragment提供一個惟一的字符串「tag」,而不是視圖ID)方法來添加Fragment。可是,由於這樣添加的Fragment沒有跟Activity佈局中的視圖關聯,它不接受對onCreateView()方法的調用,所以你不須要實現這個方法。

不能說提供了字符串「tag」的Fragment就是非UIFragment,由於你也能夠給有UI的Fragment提供字符串「tag」,可是若是Fragment沒有UI,那麼只能使用字符串的方法來標識它。若是你想要從Activity獲取這個Fragment,須要使用findFragmentByTag()方法。

FragmentRetainInstance.java演示了一個沒有UI的使用Fragment做爲後臺工做器的Activity。

public class FragmentRetainInstance extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // First time init, create the UI.
        if (savedInstanceState == null) {
           getFragmentManager().beginTransaction().add(android.R.id.content,
                    newUiFragment()).commit();
        }
    }

    /**
     * This is a fragment showing UI that will be updated fromwork done
     * in the retained fragment.
     */
    public static class UiFragment extends Fragment {
        RetainedFragment mWorkFragment;

        @Override
        public View onCreateView(LayoutInflater inflater,ViewGroup container,
                BundlesavedInstanceState) {
            View v =inflater.inflate(R.layout.fragment_retain_instance, container, false);

            // Watch for button clicks.
            Button button =(Button)v.findViewById(R.id.restart);
            button.setOnClickListener(newOnClickListener() {
                public voidonClick(View v) {
                   mWorkFragment.restart();
                }
            });

            return v;
        }

        @Override
        public void onActivityCreated(BundlesavedInstanceState) {
           super.onActivityCreated(savedInstanceState);

            FragmentManager fm =getFragmentManager();

            // Check to see if we have retainedthe worker fragment.
            mWorkFragment =(RetainedFragment)fm.findFragmentByTag("work");

            // If not retained (or first timerunning), we need to create it.
            if (mWorkFragment == null) {
                mWorkFragment = newRetainedFragment();
                // Tell it who it isworking with.
               mWorkFragment.setTargetFragment(this, 0);
               fm.beginTransaction().add(mWorkFragment, "work").commit();
            }
        }

    }

    /**
     * This is the Fragment implementation that will be retainedacross
     * activity instances.  It represents some ongoingwork, here a thread
     * we have that sits around incrementing a progressindicator.
     */
    public static class RetainedFragment extends Fragment {
        ProgressBar mProgressBar;
        int mPosition;
        boolean mReady = false;
        boolean mQuiting = false;

        /**
         * This is the thread that will do our work. It sits in a loop running
         * the progress up until it has reached thetop, then stops and waits.
         */
        final Thread mThread = new Thread() {
            @Override
            public void run() {
                // We‘ll figure thereal value out later.
                int max = 10000;

                // This thread runsalmost forever.
                while (true) {

                    // Updateour shared state with the UI.
                   synchronized (this) {
                       // Our thread is stopped if the UI is not ready
                       // or it has completed its work.
                       while (!mReady || mPosition >= max) {
                           if (mQuiting) {
                               return;
                           }
                           try {
                               wait();
                           } catch (InterruptedException e) {
                           }
                       }

                       // Now update the progress.  Note it is important that
                       // we touch the progress bar with the lock held, so it
                       // doesn‘t disappear on us.
                       mPosition++;
                       max = mProgressBar.getMax();
                       mProgressBar.setProgress(mPosition);
                    }

                    //Normally we would be doing some work, but put a kludge
                    // hereto pretend like we are.
                   synchronized (this) {
                       try {
                           wait(50);
                       } catch (InterruptedException e) {
                       }
                    }
                }
            }
        };

        /**
         * Fragment initialization.  We way wewant to be retained and
         * start our thread.
         */
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);

            // Tell the framework to try to keepthis fragment around
            // during a configuration change.
            setRetainInstance(true);

            // Start up the worker thread.
            mThread.start();
        }

        /**
         * This is called when the Fragment‘s Activityis ready to go, after
         * its content view has been installed; it iscalled both after
         * the initial fragment creation and after thefragment is re-attached
         * to a new activity.
         */
        @Override
        public void onActivityCreated(Bundle savedInstanceState){
           super.onActivityCreated(savedInstanceState);

            // Retrieve the progress bar from thetarget‘s view hierarchy.
            mProgressBar =(ProgressBar)getTargetFragment().getView().findViewById(
                    R.id.progress_horizontal);

            // We are ready for our thread to go.
            synchronized (mThread) {
                mReady = true;
                mThread.notify();
            }
        }

        /**
         * This is called when the fragment is goingaway.  It is NOT called
         * when the fragment is being propagatedbetween activity instances.
         */
        @Override
        public void onDestroy() {
            // Make the thread go away.
            synchronized (mThread) {
                mReady = false;
                mQuiting = true;
                mThread.notify();
            }

            super.onDestroy();
        }

        /**
         * This is called right before the fragment isdetached from its
         * current activity instance.
         */
        @Override
        public void onDetach() {
            // This fragment is being detachedfrom its activity.  We need
            // to make sure its thread is notgoing to touch any activity
            // state after returning from thisfunction.
            synchronized (mThread) {
                mProgressBar = null;
                mReady = false;
                mThread.notify();
            }

            super.onDetach();
        }

        /**
         * API for our UI to restart the progressthread.
         */
        public void restart() {
            synchronized (mThread) {
                mPosition = 0;
                mThread.notify();
            }
        }
    }
}

3.管理Fragment

要管理Activity中Fragment,須要使用FragmentManager對象,在Activity中調用getFragmentManager()方法可以得到這個對象。

FragmentManager對象可以作如下事情:

1.得到Activity中既存的Fragment,用findFragmentById()得到Activity佈局中提供UI的Fragment,或用findFragmentByTag()方法得到沒有提供UI的Fragment;

2.使用popBackStack()方法從回退堆棧中彈出Fragment,相似用戶的回退命令;

3.用addOnBackStackChangedListener()給回退堆棧的改變註冊一個監聽器。

你也能使用FragmentManager來打開一個FragmentTransaction對象,以便執行諸如添加和刪除Fragment等事務。

4.執行Fragment事務

在Activity中使用有關Fragment的添加、刪除、替換以及用它們執行其餘響應用戶交互行爲的能力是一項偉大的功能。你提交給Activity的每組改變集合被叫作一個事務,而且你能使用FragmentTransaction中APIs來執行它。也可以把每一個事務保存到被Activity管理的回退堆棧中,並容許用戶經過Fragment改變來向後導航(相似同Activity的向後導航)。

你可以從FragmentManager對象中獲取一個FragmentTransaction對象的實例,例如

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

每一個事務是一組想要同時執行的改變,你可以使用諸如add()、remove()和replace()方法把想要在一個事務中執行的全部改變組合到一塊兒,而後,調用commit()方法,把事務的執行結果反映到Activity中。

可是,在調用commit()方法以前,爲了把事務添加到Fragment事務的回退堆棧,你可能要調用addToBackStack()方法。這個回退堆棧被Activity管理,而且容許用戶經過按返回按鈕返回到先前的Fragment狀態。

下例說明了怎樣用另外一個Fragment替換當前的Fragment,而且在回退堆棧中保留這個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替換了在佈局容器中被R.id.fragment_container ID標識的任何當前Fragment。經過調用addToBackStack()方法,替換事務被保存到回退堆棧中,以便用戶可以反轉事務,而且可以經過按回退按鈕返回到前一個Fragment。

若是給事務添加多個改變(如用add()或remove()方法),而且調用了addToBackStack()方法,那麼全部這些改變在調用commit()方法以前,都會做爲一個單一的事務被添加到回退堆棧中,而且案返回按鈕時,全部這些改變將會被恢復。

除了如下兩種狀況以外,添加到FragmentTransaction的改變順序可有可無:

1.  最後必須調用commit()方法;

2.  若是給同一個容器添加了多個Fragment,那麼添加的順序決定了它們在View層次樹中顯示順序。

在你執行刪除Fragment的事務時,若是沒有調用addToBackStack()方法,那麼Fragment將會在事務被提交時銷燬,而且用戶不能再向後導航。所以,在刪除Fragment時,若是調用了addToBackStack()方法,那麼這個Fragment就會被終止,而且用戶向後導航時將會被恢復。

提示:對於每一個Fragment事務,你可以在提交以前經過調用setTransition()方法,申請一個過渡動畫。

調用commit()方法並不當即執行這個事務,而是在Activity的UI線程之上(」main」線程)調度運行,以便這個線程可以儘快執行這個事務。可是,若是須要,能夠調用來自UI線程的executePendingTransactions()方法,直接執行被commit()方法提交的事務。一般直到事務依賴其餘線程的工做時才須要這樣作。

注意:經常使用Fragment的哥們,可能會常常遇到這樣Activity狀態不一致:State loss這樣的錯誤。主要是由於:commit方法必定要在Activity.onSaveInstance()以前調用。

警告:你可以使用commit()方法提交一個只保存以前Activity狀態的事務(在用戶離開Activity時)。若是試圖在用戶離開Activity以後提交,將會發生一個異常。這是由於若是Activity須要被恢復,而提交以後的狀態卻丟失了。這種狀況下,使用commitAllowingStateLoss()方法,你丟失的提交就沒問題了。

值得注意的是:若是你喜歡使用Fragment,必定要清楚這些方法,哪一個會銷燬視圖,哪一個會銷燬實例,哪一個僅僅只是隱藏,這樣才能更好的使用它們。

a、 好比:我在FragmentA中的EditText填了一些數據,當切換到FragmentB時,若是但願會到A還能看到數據,則適合你的就是hide和 show;也就是說,但願保留用戶操做的面板,你可使用hide和show,固然了不要使勁在那new實例,進行下非null判斷。

b、再好比:我不但願保留用戶操做,你可使用remove(),而後add();或者使用replace()這個和remove,add是相同的效果。

c、 remove和detach有一點細微的區別,在不考慮回退棧的狀況下,remove會銷燬整個Fragment實例,而detach則只是銷燬其視圖結 構,實例並不會被銷燬。那麼兩者怎麼取捨使用呢?若是你的當前Activity一直存在,那麼在不但願保留用戶操做的時候,你能夠優先使用detach
4.1案例

public class MainActivity extends Activity
{


	@Override
	protected void onCreate(Bundle savedInstanceState)
	{
		super.onCreate(savedInstanceState);
		requestWindowFeature(Window.FEATURE_NO_TITLE);
		setContentView(R.layout.activity_main);

		FragmentManager fm = getFragmentManager();
public class FragmentOne extends Fragment implements OnClickListener
{

	private Button mBtn;

	@Override
	public View onCreateView(LayoutInflater inflater, ViewGroup container,
			Bundle savedInstanceState)
	{
		View view = inflater.inflate(R.layout.fragment_one, container, false);
		mBtn = (Button) view.findViewById(R.id.id_fragment_one_btn);
		mBtn.setOnClickListener(this)
	return view;
	}

	@Override
	public void onClick(View v)
	{
		FragmentTwo fTwo = new FragmentTwo();
		FragmentManager fm = getFragmentManager();
		FragmentTransaction tx = fm.beginTransaction();
		tx.replace(R.id.id_content, fTwo, "TWO");
		tx.addToBackStack(null);
		tx.commit();

	}

}

5.跟Activity通

儘管Fragment是做爲一個獨立於Activity來實現的一個對象,而且可以在多個Activity內部使用,可是一個給定的Fragment實例直接被捆綁包含它的Activity中。

特別是Fragment可以使用getActivity()方法訪問Activity的實例,而且很容易執行如在Activity佈局中查找視圖的任務:

View listView = getActivity().findViewById(R.id.list);

一樣Activity經過從FragmentManager中得到的Fragment引用也可以調用Fragment中的方法,使用findFragmentById()或findFragmentByTag()方法獲取Fragment引用,例如:

ExampleFragmen fragment = (ExampleFragment) getFragmentManager().findFragmentById(R.id.example_fragment);

1)給Activity建立事件回調

在某些案例中,可能須要Fragment與Activity共享事件。在Fragment內部定義一個回調接口是一個好方法,而且規定由持有它的Activity實現這個回調方法。當Activity經過接口接受回調時,它能在必要時與佈局中的其餘Fragment共享信息。

例如,若是一個新聞類的應用程序在一個Activity中有兩個Fragment---一個用來顯示文章列表(Fragment A),另外一個用來顯示文章內容(Fragment B)---而後再列表項目被選中時Fragment A必須告訴Activity,以便它能告訴Fragment B顯示對應的文章。在下面的例子中在Fragment A的內部聲明瞭OnArticleSelectedListener接口。

public static class FragmentA extends ListFragment {
    ...
    // Container Activity must implement this interface
    public interface OnArticleSelectedListener {
        public void onArticleSelected(Uri articleUri);
    }
    ...
}

而後,持有這個Fragment的Activity要實現OnArticleSelectedListener接口,而且要重寫onArticleSelected()方法把來自Fragment A的事件通知給Fragment B。要確保持有Fragment的Activity實現這個接口, Fragment A 的onAttach()回調方法(當Fragment被添加到Activity時系統調用這個方法)經過類型轉換onAttach()傳入的Activity來實例化一個OnArticleSelectedListener的實例。

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沒有實現這個接口,那麼Fragment會拋出ClassCastException異常。若是成功,那麼mListener成員就會擁有Activity實現的OnArticleSelectedListener對象的引用,以便Fragment A可以經過OnArticleSelectedListener接口定義的回調方法和Activity共享事件。例如,若是ListFragment繼承了Fragment A,那麼用戶每次點擊列表項時,系統都會調用Fragment中的onListItemClick()方法,而後調用onArticleSelected()方法和Activity共享事件:

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參數是被點擊項目的行ID,Activity(或其餘的Fragment)使用這個ID從應用程序的ContentProvider對象中獲取對應的文章。

 

2)給動做欄添加項目

Fragment經過實現onCreateOptionsMenu()方法給Activity的可選菜單(包括動做欄)提供菜單項,可是爲了這個方法可以接受調用,必須在onCreate()方法中調用setHasOptionsMenu()方法來指示這個Fragment應該做爲可選菜單的添加項(不然,這個Fragment不接受對onCreateOptionsMenu()方法的調用)。

而後,你把來自Fragment的要添加到可選菜單中項目追加到既存的菜單中。當菜單項被選擇時,這個Fragment也接受onOptionsItemSelected()的回調。

你也可以經過調用registerForContextMenu()方法在Fragment佈局中註冊一個視圖來提供一個上下文菜單。當用戶打開上下文菜單時,Fragment會接受對onCreateContextMenu()方法的調用。當用戶選擇一個菜單項時,Fragment會接受對onContextItemSelected()方法的調用。

注意:儘管Fragment添加的每一個菜單項都接受一個on-item-selected回調,可是當用戶選擇一個菜單項時,對應的Activity會首先受到相應的回調。若是Activity的on-item-selected回調的實現不處理被選擇的項目,那麼事件會被傳遞給Fragment的回調。這是真正的可選菜單和上下文菜單。

6.處理Fragment生命週期

管理Fragment的生命週期有點像管理Activity的生命週期,跟Activity同樣,Fragment也存在三種狀態:

Resumed

這種狀態下,Fragment顯示在正在運行的Activity中。

Paused

這種狀態下,另外一個Activity在前臺,而且有焦點,但這個Fragment所在的Activity依然是可見的(它前面的Activity是部分透明或沒有徹底覆蓋它)。

Stopped

這種狀態下,Fragment是不可見的,既能夠是持有它的Activity已經被終止,也能夠是Fragment從Activity中被刪除,但被添加到了回退堆棧中。被終止的Fragment依然存活着(全部的狀態和成員信息被系統保留着)。可是,對用戶它再也不可見,而且若是Activity被殺死,它也會被殺死。

跟Activity同樣,你也能使用Bundle對象保留Fragment的狀態,這樣,在Activity的進程被殺死時,而且在Activity被重建時,你須要使用這個對象來恢復Fragment的狀態。你可以在Fragment的onSaveInstanceState()回調執行期間保存狀態,而且在onCreate(),onCreateView()回調或onActivityCreated()回調期間恢復狀態。關於保存狀態的更多信息,請看Activity文檔。

Activity和Fragment之間在生命週期中最顯著的不一樣是在各自的回退堆棧中它們是如何存儲的。在Activity被終止時,默認狀況下,Activity被放到了經過系統來管理的Activity的回退堆棧(所以用戶可以使用回退按鈕向後導航)。可是,在刪除Fragment的事務期間,只有經過調用addToBackStack()方法明確的請求要保存Fragment實例時,它才被放到由持有Fragment的Activity管理的回退堆棧中。

http://developer.android.com/images/activity_fragment_lifecycle.png不然,管理Fragment的生命週期與管理Activity的生命週期很是相似。所以,儘管你也須要理解Activity的生命是如何影響Fragment的生命的,可是在管理Activity生命週期(managing the activity lifecycle)文章中介紹的內容一樣適用Fragment。

圖3. 在Activity生命週期影響之下的Fragment生命週期

     destroy              onDestroy()

                              onDetach

與Activity生命週期的協調

擁有Fragment的Activity的生命週期直接影響了其中的Fragment的生命週期,這樣,針對Activity的每個生命週期的回調都會有一個相似的針對Fragment的回調。例如,當Activity收到onPause()回調時,在Activity中每一個Fragment都會收到onPause()回調。

可是,Fragment有幾個額外的生命週期回調方法,用來處理跟Activity的交互,以便執行諸如建立和銷燬Fragment的UI的動做。這些額外的回調方法以下:

onAttach()

當Fragment已經跟Activity關聯上的時候,這個回調被調用。Activity會做爲onAttach()回調方法的參數來傳遞。

onCreateView()

建立跟Fragment關聯的視圖層時,調用這個回調方法。

onActivityCreated()

當Activity的onCreate()方法執行完以後,調用這個回調方法。

onDestroyView()

當跟Fragment關聯的視圖層正在被刪除時,調用這個回調方法。

onDetach()

當從Activity中解除Fragment的關聯時,調用這個回調方法。

像圖3中說明的那樣,Fragment的生命週期流收到持有這些Fragment的Activity的影響,在這個圖中,你能看到每一個連續的Activity狀態決定了Fragment的那個回調方法能夠被調用。例如,當Activity已經收到了onCreate()的回調以後,在Activity中的Fragment就不會再接收onActivityCreated()以上的回調了。

一旦Activity到達了被恢復的狀態,你就能夠自由的給這個Activity添加和刪除Fragment了,只有Activity在恢復態時,Fragment的生命週期才能獨立的改變。

可是,當Activity離開恢復態時,Fragment會再次被推動Activity的生命週期中。

注意:除了onCreateView,其餘的全部方法若是你重寫了,必須調用父類對於該方法的實現

7.範例

下例中實驗了上面所講的全部內容。此例有一個activity,其含有兩個fragment。一個顯示莎士比亞劇的播放曲目,另外一個顯示選中曲目的摘要。此例還演示瞭如何跟據屏幕大小配置fragment

主activity建立layout。

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

    setContentView(R.layout.fragment_layout);
}

主activity的fragment_layout.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 class="com.example.android.apis.app.FragmentLayout$TitlesFragment"
            android:id="@+id/titles" android:layout_weight="1"
            android:layout_width="0px" android:layout_height="match_parent" />

    <FrameLayout android:id="@+id/details" android:layout_weight="1"
            android:layout_width="0px" android:layout_height="match_parent"
            android:background="?android:attr/detailsElementBackground" />

</LinearLayout>

系統在activity加載此layout時初始化TitlesFragment(用於顯示標題列表),TitlesFragment的右邊是一個FrameLayout,用於存放顯示摘要的fragment,可是如今它仍是空的,fragment只有當用戶選擇了一項標題後,摘要fragment纔會被放到FrameLayout中。

然而,並非全部的屏幕都有足夠的寬度來容納標題列表和摘要。因此,上述layout只用於橫屏,現把它存放於ret/layout-land/fragment_layout.xml

以外,當用於豎屏時,系統使用下面的layout,它存放於ret/layout/fragment_layout.xml:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent" android:layout_height="match_parent">
    <fragment class="com.example.android.apis.app.FragmentLayout$TitlesFragment"
            android:id="@+id/titles"
            android:layout_width="match_parent" android:layout_height="match_parent" />
</FrameLayout>

這個layout只包含TitlesFragment。這表示當使用豎屏時,只顯示標題列表。當用戶選中一項時,程序會啓動一個新的activity去顯示摘要,而不是加載第二個fragment。

下一步,你會看到Fragment類的實現。第一個是TitlesFragment,它從ListFragment派生,大部分列表的功能由ListFragment提供。

當用戶選擇一個Title時,代碼須要作出兩種行爲,一種是在同一個activity中顯示建立並顯示摘要fragment,另外一種是啓動一個新的activity。

public static class TitlesFragment extends ListFragment {
    boolean mDualPane;
    int mCurCheckPosition = 0;

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

        // Populate list with our static array of titles.
        setListAdapter(new ArrayAdapter<String>(getActivity(),
                android.R.layout.simple_list_item_activated_1, Shakespeare.TITLES));

        // Check to see if we have a frame in which to embed the details
        // fragment directly in the containing UI.
        View detailsFrame = getActivity().findViewById(R.id.details);
        mDualPane = detailsFrame != null && detailsFrame.getVisibility() == View.VISIBLE;

        if (savedInstanceState != null) {
            // Restore last state for checked position.
            mCurCheckPosition = savedInstanceState.getInt("curChoice", 0);
        }

        if (mDualPane) {
            // In dual-pane mode, the list view highlights the selected item.
            getListView().setChoiceMode(ListView.CHOICE_MODE_SINGLE);
            // Make sure our UI is in the correct state.
            showDetails(mCurCheckPosition);
        }
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putInt("curChoice", mCurCheckPosition);
    }

    @Override
    public void onListItemClick(ListView l, View v, int position, long id) {
        showDetails(position);
    }

    /**
     * Helper function to show the details of a selected item, either by
     * displaying a fragment in-place in the current UI, or starting a
     * whole new activity in which it is displayed.
     */
    void showDetails(int index) {
        mCurCheckPosition = index;

        if (mDualPane) {
            // We can display everything in-place with fragments, so update
            // the list to highlight the selected item and show the data.
            getListView().setItemChecked(index, true);

            // Check what fragment is currently shown, replace if needed.
            DetailsFragment details = (DetailsFragment)
                    getFragmentManager().findFragmentById(R.id.details);
            if (details == null || details.getShownIndex() != index) {
                // Make new fragment to show this selection.
                details = DetailsFragment.newInstance(index);

                // Execute a transaction, replacing any existing fragment
                // with this one inside the frame.
                FragmentTransaction ft = getFragmentManager().beginTransaction();
                if (index == 0) {
                    ft.replace(R.id.details, details);
                } else {
                    ft.replace(R.id.a_item, details);
                }
                ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
                ft.commit();
            }

        } else {
            // Otherwise we need to launch a new activity to display
            // the dialog fragment with selected text.
            Intent intent = new Intent();
            intent.setClass(getActivity(), DetailsActivity.class);
            intent.putExtra("index", index);
            startActivity(intent);
        }
    }
}

第二個fragment,DetailsFragment顯示被選擇的Title的摘要:

public static class DetailsFragment extends Fragment {
    /**
     * Create a new instance of DetailsFragment, initialized to
     * show the text at 'index'.
     */
    public static DetailsFragment newInstance(int index) {
        DetailsFragment f = new DetailsFragment();

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

        return f;
    }

    public int getShownIndex() {
        return getArguments().getInt("index", 0);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        if (container == null) {
            // We have different layouts, and in one of them this
            // fragment's containing frame doesn't exist.  The fragment
            // may still be created from its saved state, but there is
            // no reason to try to create its view hierarchy because it
            // won't be displayed.  Note this is not needed -- we could
            // just run the code below, where we would create and return
            // the view hierarchy; it would just never be used.
            return null;
        }

        ScrollView scroller = new ScrollView(getActivity());
        TextView text = new TextView(getActivity());
        int padding = (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
                4, getActivity().getResources().getDisplayMetrics());
        text.setPadding(padding, padding, padding, padding);
        scroller.addView(text);
        text.setText(Shakespeare.DIALOGUE[getShownIndex()]);
        return scroller;
    }
}

若是當前的layout沒有R.id.detailsView(它被用於DetailsFragment的容器),那麼程序就啓動DetailsActivity來顯示摘要。

下面是DetailsActivity,它只是簡單地嵌入DetailsFragment來顯示摘要。

public static class DetailsActivity extends Activity {

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

        if (getResources().getConfiguration().orientation
                == Configuration.ORIENTATION_LANDSCAPE) {
            // If the screen is now in landscape mode, we can show the
            // dialog in-line with the list so we don't need this activity.
            finish();
            return;
        }

        if (savedInstanceState == null) {
            // During initial setup, plug in the details fragment.
            DetailsFragment details = new DetailsFragment();
            details.setArguments(getIntent().getExtras());
            getFragmentManager().beginTransaction().add(android.R.id.content, details).commit();
        }
    }
}

注意這個activity在檢測到是豎屏時會結束本身,因而主activity會接管它並顯示出TitlesFragment和DetailsFragment。這能夠在用戶在豎屏時顯示在TitleFragment,但用戶旋轉了屏幕,使顯示變成了橫屏

 

 

 

 

 

 

 

 

 

 

相關文章
相關標籤/搜索