標籤:ima sid 兩種 als 郵件 程式 使用者 detail select
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上下文。
@Overridepublic 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即可。
@Overridepublic void onAttach(Activity myActivity) { Log.v(MainActivity.TAG, "in TitlesFragment onAttach; activity is: " + myActivity); super.onAttach(myActivity); this.myActivity = (MainActivity)myActivity;}
@Overridepublic 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執行個體中儲存狀態
@Overridepublic 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();
Android中Fragment的使用