Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android中Fragment的解析和使用詳解

Android中Fragment的解析和使用詳解

編輯:關於Android編程

前言

Android Fragment的生命周期和Activity類似,實際可能會涉及到數據傳遞,onSaveInstanceState的狀態保存,FragmentManager的管理和Transaction,切換的Animation。

我們首先簡單的介紹一下Fragment的生命周期。

大致上,從名字就可以判斷出每個生命周期是干嘛的。

AppCompatActivity就是FragmentActivity的子類,如果想使用Fragment,是要繼承FragmentActivity,因為考慮到兼容的問題,我們要使用getSupportFragmentManager,而這個方法是FragmentActivity中聲明的。

Activity中同樣也有個類似的方法,getFragmentManager,兩個方法返回的都是FragmentManager,不過一個是v4包。

至於Android到底是如何為低版本兼容Fragment這個問題,這裡就不研究了,因為涉及到的源碼估計應該很多,而且可能會很深。

Fragment到底是如何將自己的生命周期和Activity綁定在一起呢?

這裡有一個很關鍵的類:FragmentController。

在FragmentActivity的生命周期中,會調用FragmentController對應的方法,而這些方法會調用到FragmentManager對應的方法。

我們來看看FragmentActivity的onCreate方法。

mFragments.attachHost(null /*parent*/);
super.onCreate(savedInstanceState);

這裡調用了attachHost方法,而attachHost方法又調用了FragmentManager的attachController方法。

attachController這個方法實際上,是將需要的FragmentHostCallback,FragmentContainer和Fragment傳進來。

FragmentHostCallback是FragmentContainer的子類,實際上,它就是Fragment所要附加的Activity,它持有這個Activity的實例,Context和Handler。

FragmentContainer和FragmentHostCallback是同一個實例,就是要附加的Activity。

而Fragment傳入的是null,參數名是parent,這裡附加的是Activity,因此沒有Parent Fragment是很正常的。

當我們使用FragmentManager的時候,如果要添加Fragment,是需要這樣寫:

FragmentManager manager = ((FragmentActivity) context).getSupportFragmentManager();
FragmentTransaction transaction = manager.beginTransaction();
transaction.add(fragment, context.getClass().getSimpleName());
transaction.commit();

這裡出現了新的類:FragmentTransaction。

FragmentTransaction是用於處理Fragment的棧操作,具體的子類是BackStackRecord,它同時也是一個Runnable。

當我們調用FragmentTransaction的add時候,實際上是調用BackStackRecord的addOp方法,Op是自定義的數據結構:

static final class Op {
  Op next;
  Op prev;
  int cmd;
  Fragment fragment;
  int enterAnim;
  int exitAnim;
  int popEnterAnim;
  int popExitAnim;
  ArrayList<Fragment> removed;
 }

也就是Fragment棧裡面的節點的數據結構。

當我們commit的時候,就會調用FragmentManager的allocBackStackIndex,方法內部使用了對象這是為了保證Fragment的正常寫入順序,實際上,內部是用一個BackStackRecord的ArrayList來保存傳入的BackStackRecord。

執行Fragment的寫入後,關鍵一步就是調用FragmentManager的enqueueAction,將我們的操作添加到操作隊列中。

執行這個方法的時候,會先檢查是否已經保存了狀態,也就是是否處於onStop的生命周期,如果是的話,就會報異常信息。所以我們不能在Activity的onStop裡面進行任何有關Fragment的操作。

為了保證操作是串行的,同樣也使用了對象鎖。

最關鍵的是運行了FragmentManager的mExecCommit這個Runnable,這裡主要是把每一個Active的Fragment作為參數傳給moveToState這個方法,判斷Fragment的狀態。

這裡的邏輯比較復雜,會將Fragment的State和mCurState進行比較。一開始commit的每個Fagment的狀態都是INITIALIZING。

分為2種情況:

1.mCurState > State

說明Fragment開始創建。

onCreate最後會調用FragmentController和FragmentManager的dispatchCreate,將mCurState的狀態改為CREATED,這時同樣是調用moveToState方法,每個Fragment的狀態都是INITIALIZING,就會開始讀取保存的狀態,並且分別調用Fragment的onAttach,onCreate,onCreateView和onViewCreate。

如果沒有在commit之前就setArguments來傳遞數據,調用commit後是無法讀取到的,因為setArguments傳遞過來的Bundle是在Fragment初始化的時候才會賦值給Fragment的mArguments,而Fragment的初始化動作是在FragmentManager的onCreateView中進行。我們使用Fragment的時候,都是在FragmentActivity的onCreate中commit,所以這時候Fragment實際上在commit的時候就會開始初始化了,如果放在commit後面setArguments,就根本沒機會傳遞給Fragment。

這裡我們要注意,上面都是在FragmentActivity的onCreate中進行,也就是說,這時候Activity根本還沒創建好,所以關於Activity的資源在這裡是無法獲取到的。

2.mCurState < State

說明Fragment已經創建完畢。

所以,Fragment真正和Activity綁定是在commit調用的時候。

官方推薦我們通過setArguments來傳遞構造Fragment需要的參數,不推薦通過構造方法直接來傳遞參數,因為橫豎屏切換的時候,是重新創建新的Activity,也就是重新創建新的Fragment,原先的數據就會全部丟失,但是setArguments傳遞的Bundle會保留下來。

我們只要看FragmentActivity的onCreate方法就知道,它會判斷之前的配置和savedInstanceState是否不為null,而savedInstanceState會保存Fragment的數據,這些數據是以Parcelable的形式保存下來,這些數據就是FragmentManagerState,如果不為null,就會重新加載這些數據。

實際上,上面的生命周期的圖是有問題的,onActivityCreated真正被調用是在FragmentActivity的onStart裡面,這時mCurState就變成ACTIVITY_CREATED,而Fragment的狀態變成CREATED,這時如果Fragment並不是布局文件中聲明 ,采用的是動態添加的方式,那麼Fragment就是在這裡調用onCreateView和onViewCreated,並且將Fragment添加到FragmentActivity的布局上。

首先我們必須明確的是,onStart的時候,Activity雖然可見,但是還沒有顯示到前台,所以這時候才處理動態添加Fragment的情況是合理的,如果我們把動態添加Fragment的邏輯放在onCreate的時候,那時候Activity自身的布局都還沒創建,怎麼可能找到Container加載Fragment呢?

這同時也是提醒我們,不要在Fragment的onCreateView和onViewCreated處理耗時的邏輯,否則就會影響到FragmentActivity顯示到前台的時間。

當FragmentActivity進入onResume的時候,已經顯示到前台了,這時候發送一個消息給Handler,通知FragmentManager,mCurState變為RESUMED,這時Fragment就會開始進行監聽事件等的設置。

當FragmentActivity進入onPause的時候,會先檢查Fragment是否還沒有設置監聽事件,如果沒有,就讓它進行設置,然後修改mCurState為STARTED,這時就屬於前面的第二種情況,Fragment進入onPause。

當FragmentActivity進入onStop的時候,首先通知FragmentManager修改mCurState為STOPPED,這時就會通知Fragment進入onStop,然後就是Handler接收到消息,通知FragmentManager將mCurState改為ACTIVITY_CREATED,通知Fragment調用performReallyStop,也就是真正的結束。

當FragmentActivity進入onDestroy的時候,會確認是否真的reallyStop,然後通知FragmentManager修改mCurState為CREATED,這時Fragment的狀態為ACTIVITY_CREATED,開始保存視圖數據,調用onDestroyView,父布局開始移除Fragment。

仔細看這段邏輯,就會發現,不管有沒有設置Fragment是需要保留的,都會進入onDetach,表示該Fragment和FragmentActivity已經不再關聯了。

我們再來看一下onRetainNonConfigurationInstance這個方法,它會設置Fragment的mRetaining為true,這樣就會使Fragment不會進入onDestroy,就算是重新創建新的FragmentActivity,也只是清除Fragment的mHost,mParentFragment,mFragmentManager和mChildFragmentManager,之前的數據都會保存下來,並且這個Fragment並沒有被銷毀,這就會導致一個問題:重新創建的FragmentActivity本身也會創建新的Fragment,因此會出現Fragment的重疊,因為這時Fragment的狀態為STOPPED,會分別進入onStart和onResume,也就是重新顯示到前台的過程。

我們在實際的測試中就會發現,在沒做任何處理的情況下,FragmentManager中的Fragment是越來越多,所以實際上,考慮到這種情況:應用在後台如果被殺掉的話,重新啟動應用,之前的Fragment就可能會重疊在界面上。

這種情況在處理Tab的時候是比較麻煩的,因為Tab是好幾個Fragment同時顯示在前台,如果Activity被干掉,重新創建的時候,進入的是第一個Fragment,但如果這時候是在另一個Fragment下被干掉的,就可能導致這兩個Fragment重疊。

所以可以在onCreate中判斷是否重新創建Activity,只要判斷savedInstanceState是否為null,如果為null,說明該Activity沒有被重建過,可以添加Fragment,就算是上面的Tab的情況也可以處理,只要不添加第一個Fragment就可以。

如果是基於這樣的判斷來解決這個問題,我們還可以在添加Fragment的時候,指定一個Id或者Tag,判斷FragmentManager中對應的Id或者Tag的Fragment是否存在來決定是否要添加。

當然,如果項目實在沒有需要,我們是可以強制豎屏的。

如果只是針對橫豎屏切換,也有另一種解決方案,在AndroidManifest中對應的activity標簽中設置android:configChanges="orientation|keyboardHidden" ,但是這個屬性在Android 4.0以上就失效了,必須這樣寫才行:android:configChanges="orientation|keyboardHidden|screenSize" 。這樣在橫豎屏切換的時候,不會走onRetainNonConfigurationInstance,走的是onConfigurationChanged,切換時不會銷毀當前的FragmentActivity,自然Fragment也同樣能夠保持下來。

如果我們想要為Fragment增加過場動畫,針對v4和非v4,有兩種做法。

 1.針對v4,使用的是View Animation,動畫資源放在res\anim\目錄下。

 2.針對非v4,使用的是屬性動畫,動畫資源放在res\animator\目錄下。

   一般我們使用的都是v4的Fragment,並且針對的轉場動畫,View Animation已經足夠滿足我們的要求。

我們再來看一下FragmentTransaction的addToBackStack這個方法。

如果我們想要實現這樣的效果:點擊返回鍵,返回的是上一個Fragment。那就得調用addToBackStack這個方法。這個方法要求傳入一個String的參數,實際上我們只要傳入null就行,如果我們不想指定棧(雖說是棧,實際上只是個ArrayList,並沒有實現棧的結構)的名字。

仔細看源碼,我們就會發現,如果不調用這個方法,在按返回鍵的時候,就直接finish當前的FragmentActivity。

Fragment的回退和Activity的回退是有很大的區別的,我們知道,Fragment的操作是FragmentTransaction,而BackStackRecord真是這些操作的具體子類實現。

這時問題就來了:如果我們是兩次FragmentTransactiont添加Fragment,第一次添加A,第二次添加B和C,我們回退並不是Fragment,是BackStackRecord的Op,而Op中記錄的是每次操作的Fragment,當我們回退第二次操作的時候,是把第二次添加的B和C都退出來。

如果我們只有一個Fragment,並且也不想實現Fragment的回退棧,就千萬不要調用addToBackState,不然在Activity按返回鍵的時候,並不會馬上退出Activity,而是返回一個空白,因為就算是null,也會添加到BackStackRecord的ArrayList中,因為這個參數是作為mName來標記BackStackRecord, 在實際的處理中,它是否為null根本不重要。

當然,我們也可以自己調用FragmentManager的popBackStack方法進行回退棧的操作,如果我們想要馬上執行的話,就要調用popBackStackImmediate方法,實際上,默認調用的就是這個方法。

如果我們在添加Fragment的時候,並沒有設置任何Tag,但是在彈出棧的時候,要求彈出最新的Fragment,增加新的Fragment。

Fragment的棧並不像是Activity的棧那麼復雜,提供多種啟動模式,如果看源碼的話,就會發現,實際上它就只有一種:彈出最近的BackStackRecord中的所有Fragment。

如果我們調用popBackStack的時候,沒有指定flag為POP_BACK_STACK_INCLUSIVE,源碼中的實現雖然是用if-else分成兩種判斷情況,但實際的處理是差不多的,不過沒有指定的話,它會處理比較麻煩,如果可能的話,我們還是指定一下。

回到我們上面的問題,我們該如何做呢?

replace並不會影響到回退棧,如果我們真的要使用replace來替代某個Fragment,並且想要實現回退棧,就要addToBackStack,但如果這時我們想要替換某個Fragment,回退棧中的記錄並不會跟著被替換,也就是說,這時我們選擇回退,會退回到我們被替換的Fragment,所以我們必須在替換前就彈出這個Fragment。

FragmentManager提供了getBackStackEntryCount方法告訴我們回退棧的數量,還有getBackStackEntryAt方法來獲取到對應的BackStackRecord,這時我們就能以下的處理來實現彈出:

if(manager.getBackStackEntryCount()>0){ 
 int n = manager.getBackStackEntryCount();
 manager.popBackStack(manager.getBackStackEntryAt(n-1).getName(), FragmentManager.POP_BACK_STACK_INCLUSIVE);
}

然後我們就能使用replace了。

我們必須注意,add,remove和replace影響到的是Fragment在界面上的顯示,它們跟回退棧一點關系都沒有,實際上,如果我們沒有調用addToBackStack,甚至根本就不會有回退棧,而且回退棧是在該方法每次調用後,就會添加一個,不論是否重復,它都不會進行任何判斷,所以如果一次FragmentTransaction提交多個Fragment,但是只是調用一次addToBackStack,雖然界面上有多個Fragment,但是回退棧中只有一個記錄。

Fragment說歸到底,在源碼上來看,就只是和Activity生命周期同步的View,它不可能做到和Activity一樣復雜的功能,它的任何邏輯業務代碼,實際上也屬於Activity,只不過移動到另一個類中而已,當然,如果願意的話,就算把它當做一個輕量級的ViewController也是可以的,畢竟它只是負責自己負責的View的一切業務功能。

FragmentTransaction為Fragment提供了add,remove,hide,show和replace幾種操作,我們要注意的是,add和replace的區別。

replace實際上就是remove + add的結合,並且使用replace的話,每次切換的話,會導致Fragment重新創建,因為它會把被替換的Fragment從視圖中移除,這樣當替換回來的時候,就要重新創建了。

這樣頻繁切換,就會嚴重影響到性能和流量。

所以,官方的說法是:replace()這個方法只是在上一個Fragment不再需要時采用的簡便方法。

正確的切換方式是add() ,切換時hide() add()另一個Fragment;再次切換時,只需hide()當前,show()另一個。

當然,在hide之前,我們還需通過isAdd來判斷是否添加過。

如果通過hide和show來實現切換,我們就不需要保存數據,因為Fragment並沒有被銷毀,如果是replace這種方式,我們就要保存數據,舉個例子,如果界面中有EditText,我們如果想要保存之前在EditText的輸入,就要保存這個值,不然使用replace的話,是會移除整個View的。

Fragment還涉及到和Activity以及其他Fragment的通信。

最好的方式就是只讓Activity和Fragment進行通信,如果Fragment想要和其他Fragment進行通信,也得通過Activity。

我們可以利用回調Fragment的方法進行通信,當然,也可以在Fragment中聲明接口,只要Activity實現這些接口,就能實現Activity和Fragment的通信。

想到setArguments是通過Bundle的形式來保存數據,那麼我們是否可以利用這點,在傳參上做一點文章呢?

在軟件設計上,為了減少依賴,提議利用一個高層抽象來負責組件之間的通信,這樣各個組件之間就不需要互相依賴了,也就是所謂的依賴倒置原則。

那麼,我們這裡是否也可以利用這個原則來做點事情呢?

依賴倒置在很多框架中的表現是采取注解的形式,我們可以考慮一下注解的方式來解決這個問題。

如果僅僅是為了構建Fragment而傳輸的參數,問題倒是比較簡單,只要合理的利用反射,我們就可以獲取到Fragment的字段,然後賦值。

類似的表現形式如下:

class FragmentA extends Fragment{
  @Arg
  private int age;
  public void onCreate(){
   FragmentInject.inject(this);
  }
}
class ActivityA extends Activity{
  
  public voi onCreate(){
   FragmentA a = new FragmentA();
   Bundle bundle = new Bundle();
   bundle.putString("text", "你好");
   a.setArguments(bundle);
   FragmentManager manager = getSupportFragmentManager();
   FragmentTransaction transaction = manager.beginTransaction();
   transaction.add(R.id.container, a);
   transaction.commit();
  }
}

實際上,這種方式無非就是代碼組織方式上的改變,因為我們完全可以在Fragment的onCreate中獲取到Bundle,同樣也可以進行相同的操作,並且總的代碼量會更少,但如果單純只是從Fragment來看,我們只需要調用FragmentInject.inject方法和聲明Arg注解,其他的東西根本不用考慮,相關的解析Bundle和字段賦值都放在FragmentInject這個抽象中,我們就不用每個Fragment都要寫同樣的代碼,只要交給FragmentInject就行。

當然,上面只是簡單的實現,真的是要實現一個成熟的東西是要考慮很多方面的,我們這裡就把這個簡單的項目放在Github上:https://github.com/wenjiang/FragmentArgs.git,如果有新的想法,歡迎補充。

總結

以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作能帶來一定的幫助,如果有疑問大家可以留言交流。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved