我所見的Fragment
是什么
Fragment (碎片)是 Android 中的行為或用戶界面部分。
為什么
在 Android 中引入 Fragment 主要是為了給大屏幕 (如平板電腦) 上更加動態和靈活的UI設計提供支持。使用Fragment可以在app適配平板時無需大范圍的更改布局。下圖是google提供的一個示例圖:
如上兩幅圖中可以看出, 通過使用兩個 Fragment 進行不同的排列組合可以很好的適配平板和手機兩種不同布局風格, 而減少大量的布局開發作業.
時間點
Android 在 Android 3.0(API 級別 11)中引入了片段, 如果使用v4包可以兼容到 Android 1.6;
如果使用 V4 包的話需要注意一些地方:
1. 果你使用了v4包下的 Fragment , 那么所在的那個Activity就要繼承 FragmentActivity .
2. 如果要使用FragmentManager必須使用 getSupportFragmentManager() ;
怎么用
1. 生命周期
管理 Fragment 的生命周期與管理 Activity 的生命周期非常相似,片段也以三種狀態存在:
1. 繼續
片段在運行中的 Activity 中可見。
暫停
另一個 Activity 位于前臺并具有焦點,但此片段所在的 Activity 仍然可見(前臺 Activity 部分透明,或未覆蓋整個屏幕)。
停止
片段不可見。宿主 Activity 已停止,或片段已從 Activity 中移除,但已添加到返回棧。 停止片段仍然處于活動狀態(系統會保留所有狀態和成員信息)。 不過,它對用戶不再可見,如果 Activity 被終止,它也會被終止。</code></pre>
Fragment 與 Activity 的生命周期最顯著的區別是它們各自返回棧中的存儲方式, 和 Activity 停止時自動放入由系統管理的 Activity 返回棧不同, Fragment 僅在被移除的事務執行期間調用 addToBackStack() 顯示請求保存實例時, 系統才會將 Fragment 放入由宿主 Activity 管理的返回棧.
Fragment 與 Activity 的生命周期具有協調一致性, 這體現在 Activity 的每次生命周期回調都會引發每個片段的類似回調. 例如, 當 Activity 收到 onPause() 時, Activity 中的每個片段也會收到 onPause(). Fragment中還有一些額外的方法:
onAttach()
在片段已與 Activity 關聯時調用(Activity 傳遞到此方法內)。
onCreateView()
調用它可創建與片段關聯的視圖層次結構。
onActivityCreated()
在 Activity 的 onCreate() 方法已返回時調用。
onDestroyView()
在移除與片段關聯的視圖層次結構時調用。
onDetach()
在取消片段與 Activity 的關聯時調用。</code></pre>
使用 <fragment> 標簽引入 Fragment 的 Activity 的創建到銷毀, 生命周期方法調用的順序:
-Fragment: onCreate
-Fragment: onCreateView
-Fragment: onViewCreated
-Activity: onCreate
-Fragment: onActivityCreated
-Activity: onStart
-Fragment: onStart
-Activity: onResume
-Fragment: onResume
-Fragment: onPause
-Activity: onPause
-Fragment: onStop
-Activity: onStop
-Fragment: onDestroyView
-Fragment: onDestroy
-Fragment: onDetach
-Activity: onDestroy
2. 管理 Fragment
要想管理 Activity 中的 Fragment, 需要使用 FragmentManager. 可以從 Activity 中調用 getFragmentManager() 獲取.
可以使用 FragmentManager 執行的操作包括:
- 通過 findFragmentById()(對于在 Activity 布局中提供 UI 的片段)或 findFragmentByTag()(對于提供或不提供 UI 的片段)獲取 Activity 中存在的片段。
- 通過 popBackStack()(模擬用戶發出的返回命令)將片段從返回棧中彈出。
- 通過 addOnBackStackChangedListener() 注冊一個偵聽返回棧變化的偵聽器。
注意: 在 Fragment 的嵌套情況下在父 Fragment 中使用 getFragmentManager() / getSupportFragmentManager() 獲取 FragmentManager; 在子 Fragment 中需要使用getChildFragmentManager 來獲取.
2. Fragment的簡單使用
- 使用< fragment >標簽在布局中添加碎片
- 通過 android:name 屬性來顯式指明要添加的碎片類名,注意一定要將類的包名也加上。
<fragment
android:id = "@+id/left_fragment"
android:name = "com.example.FragmentTest.fragment.HomeFragment"
android:layout_width = "1dp"
android:layout_height = "match_parent"
android:layout_weight = "1"
tools:layout = "@layout/left_fragment"/>
注:每個片段都需要一個唯一的標識符,重啟 Activity 時,系統可以使用該標識符來恢復片段(您也可以使用該標識符來捕獲片段以執行某些事務,如將其移除)。 可以通過三種方式為片段提供 ID:
- 為 android:id 屬性提供唯一 ID。
- 為 android:tag 屬性提供唯一字符串。
- 如果您未給以上兩個屬性提供值,系統會使用容器視圖的 ID。
3. 動態添加Fragment
碎片真正的強大之處在于,它可以在程序運行時動態地添加到活動當中
- 創建待添加的碎片實例。
- 獲取到 FragmentManager,在活動中可以直接調用 getFragmentManager()方法得到。
- 開啟一個事務,通過調用 beginTransaction()方法開啟。
- 向容器內加入碎片,可以使用 replace() 或 add() 方法實現,需要傳入容器的 id 和待添加的碎
片實例。
- 提交事務,調用 commit()方法來完成。
FragmentTransaction ft = getFragmentManager().beginTransaction();
ft.add(R.id.fg_main, new MainFragment());
ft.addToBackStack(null);
ft.commit();
注意:
1. 調用 commit() 不會立即執行事務,而是在 Activity 的 UI 線程(“主”線程)可以執行該操作時再安排其在線程上運行。不過,如有必要,您也可以從 UI 線程調用 executePendingTransactions() 以立即執行 commit() 提交的事務。通常不必這樣做,除非其他線程中的作業依賴該事務。
2. 您只能在 Activity 保存其狀態(用戶離開 Activity)之前使用 commit() 提交事務。如果您試圖在該時間點后提交,則會引發異常。 這是因為如需恢復 Activity,則提交后的狀態可能會丟失。 對于丟失提交無關緊要的情況,請使用 commitAllowingStateLoss()。
4. Fragment 和 Activity 之間進行通訊
- 在 Activity 中獲取相應 Fragment 的實例, 可以通過 FragmentManager 使用 findFragmentById() 或 findFragmentByTag().
RightFragment rightFragment = (RightFragment) getFragmentManager().findFragmentById(R.id.right_fragment);
- 得到當前碎片相關聯的活動實例
MainActivity activity = (MainActivity) getActivity();
-
同時出現在 Activity 中的兩個 Fragment 之間如何傳遞信息
執行此操作的一個好方法是,在片段內定義一個回調接口,并要求宿主 Activity 實現它。 當 Activity 通過該接口收到回調時,可以根據需要與布局中的其他片段共享這些信息。
public static class FragmentA extends ListFragment
{
...
// Container Activity must implement this interface
public interface OnArticleSelectedListener
{
public void onArticleSelected(Uri articleUri);
}
@Override
public void onAttach(Activity activity)
{
super.onAttach(activity);
try {
mListener = (OnArticleSelectedListener) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString() + " must implement OnArticleSelectedListener");
}
}
...
}</code></pre>
遇到的坑
對于Fragment中的各種坑推薦大神的 Fragment全解析系列 , 以及大神所寫的 Fragmentation庫 , 以下就只寫一些我自己遇到的問題了.
1. getActivity() return null
原因:
大部分原因是由于在 Fragment 與 Activity 斷開關系 (onDetach() 被調用) 后使用 getActivity() (e.g. 異步網絡訪問中onDetach() 被調用, 獲取訪問結果后調用 getActivity() 操作布局)
解決:
- 最好的辦法就是我們應該避免在已經onDetach這種情況之后再去調用宿主Activity對象,比如取消這些異步任務;
- 還可以在Fragment基類里設置一個Activity mActivity的全局變量,在onAttach(Activity activity)里賦值,使用mActivity代替getActivity(),保證Fragment即使在onDetach后,仍持有Activity的引用(有引起內存泄露的風險,但是異步任務沒停止的情況下,本身就可能已內存泄漏,相比Crash,這種做法“安全”些),即:
protected Activity mActivity;
@Override
public void onAttach(Activity activity)
{
super.onAttach(activity);
this.mActivity = activity;
}
/**
如果你用了support 23的庫,上面的方法會提示過時,有強迫癥的小伙伴,可以用下面的方法代替
*/
@Override
public void onAttach(Context context)
{
super.onAttach(context);
this.mActivity = (Activity)context;
}</code></pre>
2. 異常:Can not perform this action after onSaveInstanceState
原因:
在你離開當前Activity等情況下,系統會調用onSaveInstanceState()幫你保存當前Activity的狀態、數據等,直到再回到該Activity之前(onResume()之前),你使用commit()提交了Fragment事務,就會拋出該異常!
解決: 1. (不推薦)該事務使用`commitAllowingStateLoss()`方法提交, 但是有可能導致該次提交無效!(在此次離開時恰巧Activity被強殺時) 2. (推薦)在重新回到該Activity的時候(onResumeFragments()或onPostResume()), 再執行該事務!
注意事項
參數傳遞
對Fragment傳遞數據,建議使用`setArguments(Bundle args)`,而后在onCreate中使用`getArguments()`取出,在 “內存重啟”前,系統會幫你保存數據,不會造成數據的丟失。和Activity的Intent恢復機制類似。
創建 Fragment 對象
使用`newInstance(參數) `創建Fragment對象,優點是調用者只需要關系傳遞的哪些數據,而無需關心傳遞數據的Key是什么。
public static class TestFragment extends Fragment
{
private static final String ARG = "arg";
public static Fragment newInstance(String arg)
{
TestFragment fragment = new TestFragment();
Bundle bundle = new Bundle();
bundle.putString( ARG, arg);
fragment.setArguments(bundle);
return fragment;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
View rootView = inflater.inflate(R.layout. fragment_main, container, false);
TextView tv = (TextView) rootView.findViewById(R.id. tv);
tv.setText(getArguments().getString( ARG));
return rootView;
}
}</code></pre>
add() 和 replace()
-
add() 和 replace() 的區別
- show(),hide()最終是讓Fragment的View setVisibility(true還是false),不會調用生命周期;
- replace()的話會銷毀視圖,即調用onDestoryView、onCreateView等一系列生命周期;
-
使用建議
如果你有一個很高的概率會再次使用當前的Fragment,建議使用show(),hide(),可以提高性能。
在我使用Fragment過程中,大部分情況下都是用show(),hide(),而不是replace()。
-
onHiddenChanged()
當使用add() + show(),hide()跳轉新的Fragment時,舊的Fragment回調 onHiddenChanged() ,不會回調onStop()等生命周期方法,而新的Fragment在創建時是不會回調onHiddenChanged(),這點要切記。
注意:如果你的app有大量圖片,這時更好的方式可能是replace,配合你的圖片框架在Fragment視圖銷毀時,回收其圖片所占的內存。
使用 ViewPager + Fragment 時使用懶加載
我們在做應用開發的時候,一個Activity里面可能會以viewpager(或其他容器)與多個Fragment來組合使用,而如果每個fragment都需要去加載數據,或從本地加載,或從網絡加載,那么在這個activity剛創建的時候就變成需要初始化大量資源。這樣的結果,我們當然不會滿意。那么,能不能做到當切換到這個fragment的時候,它才去初始化(不同于View的初始化,這里指的是數據的加載)呢?
-
答案就在Fragment里的setUserVisibleHint這個方法里具體可查看 android API
該方法用于告訴系統,這個Fragment的UI是否是可見的。所以我們只需要繼承Fragment并重寫該方法,即可實現在fragment可見時才進行數據加載操作,即Fragment的懶加載。
-
為什么不使用ViewPager的預加載功能呢
現在大體上放置ViewPager預加載的方法有兩種:
-
在使用ViewPager嵌套Fragment的時候,由于VIewPager的幾個Adapter的設置來說,都會有一定的預加載(默認是左右各一個Frament)。通過設置 setOffscreenPageLimit (int number) 來設置預加載的熟練,在V4包中,默認的預加載是1,即使你設置為0,也是不起作用的,設置的只能是大于1才會有效果的。我們需要通過更改V4包中的默認屬性才可以。
-
限制預加載,會出現滑動過程中卡頓現象。其實Fragment中防止預加載主要是防止數據的預加載,Fragment中的VIew預加載是有好處的,我們可以通過Fragment中的一個方法來達到預加載View 但是不加載數據,在Fragment顯示的時候才去加載數據。
public abstract class LazyFragment extends Fragment
{
protected boolean isVisible;
/**
* 在這里實現Fragment數據的緩加載.
* @param isVisibleToUser
*/
@Override
public void setUserVisibleHint(boolean isVisibleToUser)
{
super.setUserVisibleHint(isVisibleToUser);
if(getUserVisibleHint())
{
isVisible = true;
onVisible();
} else {
isVisible = false;
onInvisible();
}
}
protected void onVisible()
{
lazyLoad();
}
protected abstract void lazyLoad();
protected void onInvisible(){}
}</code></pre>
在LazyFragment,增加了三個方法,一個是onVisiable,即fragment被設置為可見時調用,一個是onInvisible,即fragment被設置為不可見時調用。另外再寫了一個lazyLoad的抽象方法,該方法在onVisible里面調用。你可能會想,為什么不在getUserVisibleHint里面就直接調用呢?
我這么寫是為了代碼的復用。因為在fragment中,我們還需要創建視圖(onCreateView()方法),可能還需要在它不可見時就進行其他小量的初始化操作(比如初始化需要通過AIDL調用的遠程服務)等。而setUserVisibleHint是在onCreateView之前調用的,那么在視圖未初始化的時候,在lazyLoad當中就使用的話,就會有空指針的異常。而把lazyLoad抽離成一個方法,那么它的子類就可以這樣做:
public class OpenResultFragment extends LazyFragment
{
// 標志位,標志已經初始化完成。
private boolean isPrepared;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
Log.d(LOG_TAG, "onCreateView");
View view = inflater.inflate(R.layout.fragment_open_result, container, false);
//XXX初始化view的各控件
isPrepared = true;
//這里再一次調用lazyLoad()方法是因為setUserVisibleHint()是在onCreateView()方法之后調用的
lazyLoad();
return view;
}
@Override
protected void lazyLoad()
{
if(!isPrepared || !isVisible)
{
return;
}
//填充各控件的數據
}
}
來自:http://blog.csdn.net/ice_coffee_mzp/article/details/53583641