Fragment可能是我心中一直以來的執念,由於Android開發並沒有像一般流程一樣系統的學習,而是直接在公司項目中改bug開始的。當時正是Fragment被提出來的時候,那時把全部精力放到了梳理代碼業務邏輯上,錯過了Fragment首班車,而這一等就到現在。
Android發布的前兩個版本只適配小尺寸的手機。開發適配小尺寸手機app只需要考慮怎么將控件布局到Activity中,怎樣打開一個新的Activity等就可以了。然而Android3.0開始支持平板,屏幕尺寸增大到10寸。這在很大程度上提升了Android開發的難度,因為支持的屏幕尺寸變大導致了更多不同尺寸手機的產生,一個簡單的Activity很難同時適配這么多不同的尺寸。以郵件應用為例,在小尺寸的手機上我們可以使用一個Activity來顯示郵件標題,另一個Activity顯示郵件詳情。但是在大屏幕的平板上有更合理的方式:同一個Activity的左側顯示標題,右側顯示詳情。
Android 3.0引入了一個核心的類Fragment,這個類能夠優雅的實現上述郵件例子中的屏幕適配問題。同時Android也發布了一個官方的支持庫 support-v4,使用該庫能夠使用Fragment的接口適配之前的Android版本。有了這個庫,我們能夠容易的為手機,平板甚至電視來開發應用程序。
1、Fragment是什么?
以上面提到的郵件app為例,我們希望郵件App在小屏幕的手機上一個Activity顯示標題,一個Activity顯示詳情。而在大屏幕平板上左邊顯示標題右邊顯示詳情。
假如我們僅使用Activity來實現這個需求,我們需要根據設備類型創建兩個不同的Activity顯示流程。針對手機,需要兩個Activity來協作,一個包含ListView的Activity來顯示標題,另一個包含其他控件組合來顯示詳情;而針對平板,需要重新創建一個包含ListView和其他控件的Activity。在使用如上的方案時,我們可以通過標簽重用layout布局文件。但是編碼部分呢?沒有一個很好的方式來重用代碼。Fragment就是為了解決這個重用的問題。
Fragment的主要功能是將布局和其對應的代碼組合到一起統一管理和重用。針對郵件App,可以將顯示標題的ListView部分組合為一個Fragment,顯示詳情的部分組合為一個Fragment,這樣在針對手機和平板適配時,Activity只需要根據不同情況顯示不同的Fragment即可,優雅的解決了代碼和布局重用的問題。如下所示:

2、Fragment的構成
Fragment用於管理UI,因此其內部肯定有視圖層級,為了在Fragment銷毀后重建一致,需要傳入一個bundle來重新配置視圖。
當Fragment被銷毀后重建時,Android會調用Fragment的空參構造方法來生成一個新的對象,並通過一個傳入bundle參數的方法設置其狀態。因此我們在繼承以Fragment時必須保留其空構造方法。
因為每個Fragment都有自己的視圖,很有可能的一種設計是:在某個操作后,將Activity中原來的Fragment替換為一個新的Fragment,而同時又想要在按返回鍵時返回到原來的Fragment,因此Fragment又有一個返回棧的設計。
3、Fragment的生命周期
Fragment的生命周期與Activity有很多相同,但更復雜,具體流程如下圖:
Fragment是一個繼承至Object的類,與Activity不同,Android並不為我們事先創建好該對象,因此在將Fragment附加給一個Activity時必須自己創建一個Fragment對象。
在之前也提到過,Android雖然不創建Fragment,但是當Fragment附加到Activity時,Android會管理其銷毀和重建,重建過程類似於如下代碼:
public static MyFragment newInstance(int index) { MyFragment f = new MyFragment(); Bundle args = new Bundle(); args.putInt("index", index); f.setArguments(args); return f; }
因此我們在創建一個Fragment時有必要按照如上代碼的方式來創建Fragment實例。
當我們將創建的Fragment實例附加給Activity時,其生命周期的回調方法即開始起作用了。
onInflate( ) 回調
通過在layout中添加標簽的方式使用Fragment時,onInflate()會執行。其主要目的是為了提供標簽中的屬性,可以從該回調中讀取屬性並保留以后使用。
onAttach( )回調
當Fragment附加到Activity后立即進行onAttach(),回調會傳入所附加的Activity作為Context上下文。
@Override public void onAttach(Context context) { super.onAttach(context); if (context instanceof OnFragmentInteractionListener) { mListener = (OnFragmentInteractionListener) context; } else { throw new RuntimeException(context.toString() + " must implement OnFragmentInteractionListener"); } }
上述代碼使用onAttach()回調優雅的實現了listener的賦值。
注意:
1、你可以保存Context對象作為Activity的引用也可以不這么做,因為Fragment有一個getActivity()會返回你所需要的Activity。
2、在onAttach()之后就不能再進行setArgument()調用了,因為onAttach()時已經附加到Activity,應該在之前確定Fragment的各個參數。因此setArgument()應該盡早調用。
onCreate()回調
onCreate是下一個要執行的方法,回調方法執行時,整個Fragment的參數設置已經齊全了,包括Bundle傳入的參數和所屬Activity對象,但並不意味着視圖層級已經構造完成了。同時回調方法不一定在Activity實例的onCreate之后。該回調的存在目的:
- 獲取傳入的bundle;
- 為Fragment提供一個盡早執行的入口,用於獲取所需數據;
注: 回調方法都在主線程,因此是不能執行耗時較長的方法例如網絡請求或者讀取本地較大文件等。可以在onCreate中創建線程來獲取數據,再通過handle 或者Loader的方式返回結果。
onCreateView( )回調
onCreateView()試下一個要執行的回調方法,該方法中創建了一個視圖層級(view 對象)並返回。參數包括一個LayoutInflater,一個ViewGroup和一個Bundle。需要注意的是盡管有parent(ViewGroup),我們並不能將創建的view 附加給parent。此處的parent僅僅在創建view時提供一些參考,之后會自動附加。
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { if(container == null) return null; View v = inflater.inflate(R.layout.details, container, false); TextView text1 = (TextView) v.findViewById(R.id.text1); text1.setText(myDataSet[ getPosition() ] ); return v; }
注:container 為null,說明沒有Fragment沒有視圖層級上。
onViewCreated( ) 回調
onCreateView之后並且在UI布局之前,其參數是一個view,即剛剛在onCreateView中返回的view。
onActivityCreated( ) 回調
在onActivityCreated()回調方法之后,Fragment就可以與用戶進行交互了。onActivityCreated()在Activity的onCreate()之后,並且Activity所有用到的Fragment都已准備完成。
onViewStateRestored( ) 回調
該回調在Android 4.2之后引入,在Fragment重建時調用,之前重建時必須將重建邏輯放在在onActivityCreated(),現在可以放到這里。
onStart( ) 回調
此時,Fragment已經可見,該回調與Activity的onStart()一致,之前在Activity中onStart回調的代碼可以直接放到這里。
onResume( ) 回調
與Activity的onResume()回調一致。
onPause( ) 回調
與Activity的onPause()一致。
onSaveInstanceState( )回調
與Activity相同,Fragment也提供一個能夠保存狀態的回調。通過該回調方法,可以將Fragment中的狀態值以bundle的形式保存起來,在onViewStateRestored()的時候重建。需要注意的是,Fragment之所以被回收就是因為內存問題,因此應該只保留需要保留的數據。
如果該Fragment依賴於另一個Fragment,不要試圖保存其直接的引用,而應該使用id或者tag。
注:盡管該回調通常發生在onPause()之后,但這並不意味着就在onPause之后立即執行。
onStop( ) 回調
與Activity的onStop()一致。
onDestroyView( ) 回調
在創建的view視圖從Activity脫離(detach)之前的回調。
onDestroy( ) 回調
在View銷毀之后,Fragment真正開始銷毀了,此時已然能夠找到該Fragment但是該Fragment已經不能進行任何操作。
onDetach( ) 回調
從Activity脫離,Fragment不在擁有view視圖層級。
使用 setRetainInstance( )
Fragment與Activity是分開存在的兩個對象,因此在Activity銷毀並重建時有兩種選擇:1、完全重建Fragment;2、在銷毀時保留Fragment對象並在Activity重建時使用,正如上圖8-2中虛線路徑。
Fragment將這種選擇交給了開發者,通過提供的 setRetainInstance()方法來決定使用哪種辦法。如果方法傳入false則使用第一種,否則使用第二種方式。
該方法設置的時機可以在onCreate()、onCreateView()以及onActivityCreate(),越早越好。
Fragment 簡單案例
案例代碼
案例是一個類似於郵件的布局的小說展示應用,分為橫屏和豎屏不同布局,橫屏時顯示左右結構,豎屏時先后顯示。為了簡化實現過程,所有數據為內存中的數據。
首先是main.xml的實現,對於橫屏和豎屏分別實現兩個不同的main.xml布局(分別對應res/layout 文件目錄和res/layout-land目錄)
<?xml version="1.0" encoding="utf-8"?> <!-- This file is res/layout/main.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 class="com.androidbook.fragments.bard.TitlesFragment" android:id="@+id/titles" android:layout_width="match_parent" android:layout_height="match_parent" /> </LinearLayout>
<?xml version="1.0" encoding="utf-8"?> <!-- This file is res/layout-land/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" android:background="#fff"> <fragment class="com.androidbook.fragments.bard.TitlesFragment" android:id="@+id/titles" android:layout_weight="1" android:layout_width="0px" android:layout_height="match_parent" android:background="#00550033" /> <FrameLayout android:id="@+id/details" android:layout_weight="2" android:layout_width="0px" android:layout_height="match_parent" /> </LinearLayout>
當手機豎屏是,創建的MainActivity中只包含一個TitleFragment,當為橫屏時包含兩部分,因此我們實現一個方法來確定是否為多面板應用。
public boolean isMultiPane() { return getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE; }
我們在加載TitlesFragment完成之后做這么一件事:加載一篇文章。對於橫屏的顯示到右邊對於豎屏顯示到新的Activity。因此該實現邏輯需要放到MainActivity,TitlesFragment在適合的事件調用MainActivity即可。
@Override public void onAttach(Activity myActivity) { Log.v(MainActivity.TAG, "in TitlesFragment onAttach; activity is: " + myActivity); super.onAttach(myActivity); this.myActivity = (MainActivity)myActivity; }
@Override public void onActivityCreated(Bundle icicle) { super.onActivityCreated(icicle); ...... myActivity.showDetails(mCurCheckPosition); }
showDetails的實現
public void showDetails(int index) { Log.v(TAG, "in MainActivity showDetails(" + index + ")"); if (isMultiPane()) { // Check what fragment is 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 inside the frame with the new one. Log.v(TAG, "about to run FragmentTransaction..."); FragmentTransaction ft = getFragmentManager().beginTransaction(); //ft.setCustomAnimations(R.animator.fragment_open_enter, // R.animator.fragment_open_exit); ft.setCustomAnimations(R.animator.bounce_in_down, R.animator.slide_out_right); //ft.setCustomAnimations(R.animator.fade_in, // R.animator.fade_out); //ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE); ft.replace(R.id.details, details); ft.addToBackStack(TAG); 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(this, DetailsActivity.class); intent.putExtra("index", index); startActivity(intent); } }
根據橫豎屏的不同,分別顯示到右邊或者新的Activity。
整體實現完畢,詳見代碼 https://github.com/votzone/DroidCode/tree/master/Fragments
注意:
1、在案例中Fragment的添加和替換有兩種方式
1) 通過xml直接添加fragmet標簽,指定其實現類即可。
2) 通過FragmentManager來動態添加,就像DetailsFragment中一樣,或者拿到父view添加:
DetailsFragment details = DetailsFragment.newInstance(getIntent().getExtras());
getFragmentManager().beginTransaction()
.add(android.R.id.content, details)
.commit();
2、使用Fragment的引用時,可以通過FragmentManager的findFragmentById 或findFragmentByTag 的方式獲取。
3、在onSaveInstanceState的參數bundle實例中保存狀態
@Override public void onSaveInstanceState(Bundle icicle) { Log.v(MainActivity.TAG, "in TitlesFragment onSaveInstanceState"); super.onSaveInstanceState(icicle); icicle.putInt("curChoice", mCurCheckPosition); }
4、與Fragment之間的交互(獲取引用)的方法
1)通過FragmentManager的findFragmentByTag或者findFragmentById來找到該Fragment,然后調用方法
FragmentOther fragOther = (FragmentOther)getFragmentManager().findFragmentByTag("other");
fragOther.callCustomMethod( arg1, arg2 );
2)通過getTargetFragment()找到當前Fragment的TargetFragment來獲取引用;
TextView tv = (TextView)getTargetFragment().getView().findViewById(R.id.text1);
tv.setText("Set from the called fragment");
對一個Fragment設置TargetFragment需要使用FragmentManager,如下:
mCalledFragment = new CalledFragment(); mCalledFragment.setTargetFragment(this, 0); fm.beginTransaction().add(mCalledFragment, "work").commit();
