編輯: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,如果有新的想法,歡迎補充。
我們在平時做開發的時候,免不了會用到各種各樣的對話框,相信有過其他平台開發經驗的朋友都會知道,大部分的平台都只提供了幾個最簡單的實現,如果我們想實現自己特定需求的對話框,
activity_ui6.xml<?xml version=1.0 encoding=utf-8?><GridView xmlns:android=ht
在AndroidManifest.xml文件中有<application android:theme=@style/AppTheme>,其中的@style/A
Android - 內容提供者(Content Provider)內容提供者組件通過請求從一個應用程序向其他的應用程序提供數據。這些請求由類 Conten