前言
Fragment想必大家不陌生吧,在日常開發中,對於Fragment的使用也很頻繁,現在主流的APP中,基本的架構也都是一個主頁,然后每個Tab項用Fragment做布局,不同選項做切換,使用起來也方便。但是否對它有足夠的認識嗎,谷歌推薦用Fragment來代替Activity,但又沒有明確說為什么要用Fragment來代替Activity,這里就引發爭議了,那到底是要不要用,是否使用Fragment完全替換Activity真的比常規開發模式更好嗎?如果要用的話,那需要了解為何要使用Fragment,Fragment是什么,它的生命周期如何,如何使用,通信又是怎樣,有什么缺點嗎?帶着這些問題,我們一一去解讀。
目錄
- Fragment為何要用
- Fragment是什么
- Fragment生命周期
- Fragment怎么用
- Fragment通信
- Fragment是否很完美
- 小結
- 參考地址
Fragment為何要用
Fragment是Android 3.0 (Honeycomb)被引入的。主要目的是為了給大屏幕(如平板電腦)上更加動態和靈活的UI設計提供支持。由於平板電腦的屏幕比手機的屏幕大很多,因此可用於組合和交換的UI組件的空間更大,利用Fragment實現此類設計的時,就無需管理對視圖層次結構的復雜更改。
通過將 Activity 布局分成片段,您可以在運行時修改 Activity 的外觀,並在由 Activity 管理的返回棧中保留這些更改。如果僅僅只有Activity布局,那是不夠的,不僅在手機上有一套布局,同時在平板上還需要設計一套布局,那樣維護起來也麻煩,代碼上也有一定的冗余,對於APP包的大小也有一定的壓力。Fragment的優勢是布局在不同設備上的適配。
比如:
從圖中我們可以看到,在平板中,一個Activity A包含了兩個Fragment,分別是Fragment A和Fragment B,但在手機中呢,就需要兩個Activity,分別是Activity A包含Fragment A和Activity B包含Fragment B。同時每個Fragment都具有自己的一套生命周期回調方法,並各自處理自己的用戶輸入事件。 因此,在平板中使用一個Activity 就可以了,左側是列表,右邊是內容詳情。
除此之外,使用Fragment還有這么幾個方面優勢:
- 代碼復用。特別適用於模塊化的開發,因為一個Fragment可以被多個Activity嵌套,有個共同的業務模塊就可以復用了,是模塊化UI的良好組件。
- Activity用來管理Fragment。Fragment的生命周期是寄托到Activity中,Fragment可以被Attach添加和Detach釋放。
- 可控性。Fragment可以像普通對象那樣自由的創建和控制,傳遞參數更加容易和方便,也不用處理系統相關的事情,顯示方式、替換、不管是整體還是部分,都可以做到相應的更改。
- Fragments是view controllers,它們包含可測試的,解耦的業務邏輯塊,由於Fragments是構建在views之上的,而views很容易實現動畫效果,因此Fragments在屏幕切換時具有更好的控制。
Fragment是什么
說了半天的Fragment,也看到這么多次Fragment這個名詞出現,那么Fragment到底是什么東東呢?定義又是如何?
Fragment也可以叫為“片段”,但我覺得“片段”中文叫法有點生硬,還是保持叫Fragment比較好,它可以表示Activity中的行為或用戶界面部分。我們可以在一個Activity中用多個Fragment組合來構建多窗格的UI,以及在多個Activity中重復使用某個Fragment。它有自己的生命周期,能接受自己的輸入,並且可以在 Activity 運行時添加或刪除Fragment(有點像在不同 Activity 中重復使用的“子 Activity”)。
簡單來說,Fragment其實可以理解為一個具有自己生命周期的控件,只不過這個控件又有點特殊,它有自己的處理輸入事件的能力,有自己的生命周期,又必須依賴於Activity,能互相通信和托管。
Fragment生命周期
如圖:
這張圖是Fragment生命周期和Activity生命周期對比圖,可以看到兩者還是有很多相似的地方,比如都有onCreate(),onStart(),onPause(),onDestroy()等等,因為Fragment是被托管到Activity中的,所以多了兩個onAttach()和onDetach()。這里講講與Activity生命周期不一樣的方法。
onAttach()
Fragment和Activity建立關聯的時候調用,被附加到Activity中去。
onCreate()
系統會在創建Fragment時調用此方法。可以初始化一段資源文件等等。
onCreateView()
系統會在Fragment首次繪制其用戶界面時調用此方法。 要想為Fragment繪制 UI,從該方法中返回的 View 必須是Fragment布局的根視圖。如果Fragment未提供 UI,您可以返回 null。
onViewCreated()
在Fragment被繪制后,調用此方法,可以初始化控件資源。
onActivityCreated()
當onCreate(),onCreateView(),onViewCreated()方法執行完后調用,也就是Activity被渲染繪制出來后。
onPause()
系統將此方法作為用戶離開Fragment的第一個信號(但並不總是意味着此Fragment會被銷毀)進行調用。 通常可以在此方法內確認在當前用戶會話結束后仍然有效的任何更改(因為用戶可能不會返回)。
onDestroyView()
Fragment中的布局被移除時調用。
onDetach()
Fragment和Activity解除關聯的時候調用。
但需要注一點是:除了onCreateView,其他的所有方法如果你重寫了,必須調用父類對於該方法的實現。
還有一般在啟動Fragment的時候,它的生命周期就會執行這幾個方法。
Fragment怎么用
前面介紹了半天,不耐煩的人會說,這么多廢話,也不見的到底是如何使用,畢竟我們是開發者,需要的使用方式,那么現在就來說說用法如何吧。兩種方式:靜態用法和動態用法。
靜態用法
1、繼承Fragment,重寫onCreateView決定Fragemnt的布局
2、在Activity中聲明此Fragment,就當和普通的View一樣
首先是布局文件:fragment1.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#00ff00" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="This is fragment 1"
android:textColor="#000000"
android:textSize="25sp" />
</LinearLayout>
可以看到,這個布局文件非常簡單,只有一個LinearLayout,里面加入了一個TextView。我們再新建一個fragment2.xml :
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ffff00" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="This is fragment 2"
android:textColor="#000000"
android:textSize="25sp" />
</LinearLayout>
然后新建一個類Fragment1,這個類是繼承自Fragment的:
public class Fragment1 extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment1, container, false);
}
}
可以看到,在onCreateView()方法中加載了fragment1.xml的布局。同樣fragment2.xml也是一樣的做法,新建一個Fragment2類:
public class Fragment2 extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment2, container, false);
}
}
然后打開或新建activity_main.xml作為主Activity的布局文件,在里面加入兩個Fragment的引用,使用android:name前綴來引用具體的Fragment:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:baselineAligned="false" >
<fragment
android:id="@+id/fragment1"
android:name="com.example.fragmentdemo.Fragment1"
android:layout_width="0dip"
android:layout_height="match_parent"
android:layout_weight="1" />
<fragment
android:id="@+id/fragment2"
android:name="com.example.fragmentdemo.Fragment2"
android:layout_width="0dip"
android:layout_height="match_parent"
android:layout_weight="1" />
</LinearLayout>
最后新建MainActivity作為程序的主Activity,里面的代碼非常簡單,都是自動生成的:
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
現在我們來運行一次程序,就會看到,一個Activity很融洽地包含了兩個Fragment,這兩個Fragment平分了整個屏幕,效果圖如下:
動態用法
上面僅僅是Fragment簡單用法,它真正強大部分是在動態地添加到Activity中,那么動態用法又是如何呢?
還是在靜態用法代碼的基礎上修改,打開activity_main.xml,將其中對Fragment的引用都刪除,只保留最外層的LinearLayout,並給它添加一個id,因為我們要動態添加Fragment,不用在XML里添加了,刪除后代碼如下:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/main_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:baselineAligned="false" >
</LinearLayout>
然后打開MainActivity,修改其中的代碼如下所示:
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Display display = getWindowManager().getDefaultDisplay();
if (display.getWidth() > display.getHeight()) {
Fragment1 fragment1 = new Fragment1();
getFragmentManager().beginTransaction().replace(R.id.main_layout, fragment1).commit();
} else {
Fragment2 fragment2 = new Fragment2();
getFragmentManager().beginTransaction().replace(R.id.main_layout, fragment2).commit();
}
}
}
看到了沒,首先,我們要獲取屏幕的寬度和高度,然后進行判斷,如果屏幕寬度大於高度就添加fragment1,如果高度大於寬度就添加fragment2。動態添加Fragment主要分為4步:
1.獲取到FragmentManager,在Activity中可以直接通過getFragmentManager得到。
2.開啟一個事務,通過調用beginTransaction方法開啟。
3.向容器內加入Fragment,一般使用replace方法實現,需要傳入容器的id和Fragment的實例。
4.提交事務,調用commit方法提交。
現在運行一下程序,效果如下圖所示:
要想管理 Activity 中的片段,需要使用 FragmentManager。要想獲取它,需要 Activity 調用 getFragmentManager()。
使用 FragmentManager 執行的操作包括:
- 通過 findFragmentById()(對於在 Activity 布局中提供 UI 的片段)或 findFragmentByTag()(對於提供或不提供 UI 的片段)獲取 Activity 中存在的片段
- 通過 popBackStack()將片段從返回棧中彈出
- 通過 addOnBackStackChangedListener() 注冊一個偵聽返回棧變化的偵聽器
也可以使用 FragmentManager 打開一個 FragmentTransaction,通過它來執行某些事務,如添加和刪除片段。
Fragment通信
盡管 Fragment 是作為獨立於 Activity的對象實現,並且可在多個 Activity 內使用,但Fragment 的給定實例會直接綁定到包含它的 Activity。具體地說,Fragment 可以通過 getActivity() 訪問 Activity實例,並輕松地執行在 Activity 布局中查找視圖等任務。如:
View listView = getActivity().findViewById(R.id.list);
同樣地,Activity 也可以使用 findFragmentById() 或 findFragmentByTag(),通過從 FragmentManager 獲取對 Fragment 的引用來調用Fragment中的方法。例如:
ExampleFragment fragment = (ExampleFragment) getFragmentManager().findFragmentById(R.id.example_fragment);
創建對 Activity 的事件回調
在某些情況下,可能需要通過與 Activity 共享事件。執行此操作的一個好方法是,在Fragment 內定義一個回調接口,並要求宿主 Activity 實現它。 當 Activity 通過該接口收到回調時,可以根據需要與布局中的其他Fragment共享這些信息。
例如,如果一個新聞應用的 Activity 有兩個Fragment ,一個用於顯示文章列表(Fragment A),另一個用於顯示文章(Fragment B)—,那么Fragment A必須在列表項被選定后告知 Activity,以便它告知Fragment B 顯示該文章。 在本例中,OnArticleSelectedListener 接口在片段 A 內聲明:
public static class FragmentA extends ListFragment {
public interface OnArticleSelectedListener {
public void onArticleSelected(Uri articleUri);
}
}
然后,該Fragment的宿主 Activity 會實現 OnArticleSelectedListener 接口並替代 onArticleSelected(),將來自Fragment A 的事件通知Fragment B。為確保宿主 Activity 實現此界面,Fragment A 的 onAttach() 回調方法(系統在向 Activity 添加Fragment時調用的方法)會通過轉換傳遞到 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 未實現界面,則片段會引發 ClassCastException。實現時,mListener 成員會保留對 Activity 的 OnArticleSelectedListener 實現的引用,以便Fragment A 可以通過調用 OnArticleSelectedListener 界面定義的方法與 Activity 共享事件。例如,如果Fragment A 是 ListFragment 的一個擴展,則用戶每次點擊列表項時,系統都會調用Fragment中的 onListItemClick(),然后該方法會調用 onArticleSelected() 以與 Activity 共享事件:
public static class FragmentA extends ListFragment {
OnArticleSelectedListener mListener;
@Override
public void onListItemClick(ListView l, View v, int position, long id) {
Uri noteUri = ContentUris.withAppendedId(ArticleColumns.CONTENT_URI, id);
mListener.onArticleSelected(noteUri);
}
}
Fragment是否很完美
因為Fragment是由FragmentManager來管理,每一個Activity有一個FragmentManager,管理着一個Fragment的棧,Activity是系統級別的,由系統來管理ActivityManager,棧也是系統范圍的。而Fragment則是每個Activity范圍內的,所以在使用Fragment的時候也有幾點要注意。
- 同一個Activity中,只能有一個ID或TAG標識的Fragment實例。
這很容易理解,同一個范圍內,有標識的實例肯定是要唯一才行(否則還要標識干嘛)這個在布局中經常犯錯,在布局中寫Fragment最好不要加ID或者TAG,否則很容易出現不允許創建的錯誤。我的原則是如果放在布局中,就不要加ID和TAG,如果需要ID和TAG就全用代碼控制。創建新實例前先到FragmentManager中查找一番,這也正是有標識的意義所在。 - 一個Activity中有一個Fragment池,實例不一定會被銷毀,可能會保存在池中。
這個跟第一點差不多。就好比系統會緩存Activity的實例一樣,FragmentManager也會緩存Fragment實例,以方便和加速再次顯示。 - FragmentManager的作用范圍是整個Activity,所以,某一個布局ID,不能重復被Fragment替換。
通常顯示Fragment有二種方式,一種是層疊到某個布局上,或者把某個布局上面的Fragment替換掉,但是這個布局不能出現二次,比如布局A中有ID為id的區域,要顯示為Fragment,此布局A,只能在一個Activity中顯示一個,否則第二個id區域不能被Fragment成功替換。因為雖有二個ID布局的實例,但ID是相同的,對FragmentManager來說是一樣的,它會認為只有一個,因為它看的是布局的ID,而不是布局的實例。 - Fragment的生命周期反應Activity的生命周期。
Fragment在顯示和退出時會走一遍完整的生命周期。此外,正在顯示時,就跟Activity的一樣,Activity被onPause,里面的Fragment就onPause,以此類推,由此帶來的問題就是,比如你在onStart()里面做了一些事情,那么,當宿主Activity被擋住,又出現時(比如接了個電話),Fragment的onStart也會被高到,所以你要想到,這些生命周期不單單在顯示和退出時會走到。 - Fragment的可見性。
這個問題出現在有Fragment棧的時候,也就是說每個Fragment不知道自己是否真的對用戶可見。比如現在是Fragment A,又在其上面顯示了Fragment B,當B顯示后,A並不知道自己上面還有一個,也不知道自己對用戶不可見了,同樣再有一個C,B也不知。C退出后,B依然不知自己已在棧頂,對用戶可見,B退后,A也不知。也就是說Fragment顯示或者退出,棧里的其他Fragment無法感知。這點就不如Activity,a被b蓋住后,a會走到onStop(),同樣c顯示后,b也能通過onStop()感知。Fragment可以從FragmentManager監聽BackStackState的變化,但它只告訴你Stack變了,不告訴你是多了,還是少,還有你處的位置。有一個解決方案就是,記錄頁面的Path深度,再跟Fragment所在的Stack深度來比較,如果一致,那么這個Fragment就在棧頂。因為每個頁面的Path深度是固定的,而Stack深度是不變化的,所以這個能准確的判斷Fragment是否對用戶可見,當然,這個僅針對整個頁面有效,對於布局中的一個區域是無效的。 - Fragment的事件傳遞。
對於層疊的Fragment,其實就相當於在一個FrameLayout里面加上一堆的View,所以,如果處於頂層的Fragment沒處理點擊事件,那么事件就會向下層傳遞,直到事件被處理。比如有二個Fragment A和B,B在A上面,B只有TextView且沒處理事件,那么點擊B時,會發現A里的View處理了事件。這個對於Activity也不會發生,因為事件不能跨窗體傳播,上面的Activity沒處理事件,也不會傳給下面的Activity,即使它可見。解決之法,就是讓上面的Fragment的根布局吃掉事件,為每個根ViewGroup添加onClick=“true”。 - 與第三方Activity交互。與第三方交互,仍要采用Android的標准startActivityForResult()和onActivityResult()這二個方法來進行。但對於Fragment有些事情需要注意,Fragment也有這二個方法,但是為了能正確的讓Fragment收到onActivityResult(),需要:
- 宿主Activity要實現一個空的onActivityResult(),里面調用super.onActivityResult()
- 調用Fragment#startActivityForResult()而不是用Activity的 當然,也可以直接使用Activity的startActivityForResult(),那樣的話,就只能在宿主Activity里處理返回的結果了。
小結
在用法的代碼部分參考郭神的博客,感覺郭神在代碼講解部分通俗易懂,看起來也方便。總之,在使用Fragment也有一些注意事項,不是那么完美的,雖然谷歌推薦我們用Fragment來代替Activity來使用,我們也確實這做了,現在基本主流的APP也都是少量Activity+很多Fragment,但也需要避免有些坑慎入。
參考地址
1,https://developer.android.com/guide/components/fragments.html
2,http://blog.csdn.net/guolin_blog/article/details/8881711
3,http://toughcoder.net/blog/2014/10/22/effective-android-ui-architecture
閱讀擴展
源於對掌握的Android開發基礎點進行整理,羅列下已經總結的文章,從中可以看到技術積累的過程。
1,Android系統簡介
2,ProGuard代碼混淆
3,講講Handler+Looper+MessageQueue關系
4,Android圖片加載庫理解
5,談談Android運行時權限理解
6,EventBus初理解
7,Android 常見工具類
8,對於Fragment的一些理解
9,Android 四大組件之 " Activity "
10,Android 四大組件之" Service "
11,Android 四大組件之“ BroadcastReceiver "
12,Android 四大組件之" ContentProvider "
13,講講 Android 事件攔截機制
14,Android 動畫的理解
15,Android 生命周期和啟動模式
16,Android IPC 機制
17,View 的事件體系
18,View 的工作原理
19,理解 Window 和 WindowManager
20,Activity 啟動過程分析
21,Service 啟動過程分析
22,Android 性能優化
23,Android 消息機制
24,Android Bitmap相關
25,Android 線程和線程池
26,Android 中的 Drawable 和動畫
27,RecylerView 中的裝飾者模式
28,Android 觸摸事件機制
29,Android 事件機制應用
30,Cordova 框架的一些理解
31,有關 Android 插件化思考
32,開發人員必備技能——單元測試