Android Fragment(生命週期+懶載入)
以前面試的時候吧,能把Fragment的生命週期從頭到尾背一遍,然後懶載入也是知道怎麼實現,但是呢,沒有寫過demo具體研究過,於是就準備寫篇部落格就當筆記了。
先附上一張面試時候常考的一張Fragment生命週期圖:
測試的佈局activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height ="match_parent"
android:orientation="vertical">
<android.support.design.widget.TabLayout
android:id="@+id/id_tab"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<android.support.v4.view.ViewPager
android:id="@+id/id_viewpager"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:background="@android:color/white" />
</LinearLayout>
測試的Fragment:
/**
* @author EX_YINQINGYANG
* @version [Android PABank C01, @2016-09-21]
* @date 2016-09-21
* @description
*/
public class MyFragment extends Fragment {
private static final String PAGE_NO = "PAGE_NO";
private static final String TAG = "MyFragment";
private int pageNo;
public static Fragment newInstance(int pageNo){
MyFragment fragment=new MyFragment();
Bundle bundle=new Bundle();
bundle.putInt(PAGE_NO,pageNo);
fragment.setArguments(bundle);
return fragment;
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
pageNo=getArguments().getInt(PAGE_NO,0);
Log.e(TAG+" "+pageNo,"---->onAttach");
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.e(TAG+" "+pageNo,"---->onCreate");
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
Log.e(TAG+" "+pageNo,"---->onCreateView");
TextView view=new TextView(getActivity());
view.setGravity(Gravity.CENTER);
view.setTextColor(Color.BLACK);
view.setTextSize(TypedValue.COMPLEX_UNIT_DIP,18f);
view.setText("Fragment"+pageNo);
return view;
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
Log.e(TAG+" "+pageNo,"---->onActivityCreated");
}
@Override
public void onResume() {
super.onResume();
Log.e(TAG+" "+pageNo,"---->onResume");
}
@Override
public void onPause() {
super.onPause();
Log.e(TAG+" "+pageNo,"---->onPause");
}
@Override
public void onStop() {
super.onStop();
Log.e(TAG+" "+pageNo,"---->onStop");
}
@Override
public void onDestroyView() {
super.onDestroyView();
Log.e(TAG+" "+pageNo,"---->onDestroyView");
}
@Override
public void onDestroy() {
super.onDestroy();
Log.e(TAG+" "+pageNo,"---->onDestroy");
}
@Override
public void onDetach() {
super.onDetach();
Log.e(TAG+" "+pageNo,"---->onDetach");
}
}
測試的MainActivity:
public class MainActivity extends AppCompatActivity {
private TabLayout mTab;
private ViewPager mPager;
private List<Fragment>fragments;
private String[]titles={"page1","page2","page3"};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTab= (TabLayout) findViewById(R.id.id_tab);
mPager= (ViewPager) findViewById(R.id.id_viewpager);
mPager.setOffscreenPageLimit(0);
initDatas();
}
private void initDatas() {
fragments=new ArrayList<>(3);
fragments.add(MyFragment.newInstance(1));
fragments.add(MyFragment.newInstance(2));
fragments.add(MyFragment.newInstance(3));
mPager.setAdapter(new FragmentPagerAdapter(getSupportFragmentManager()) {
@Override
public Fragment getItem(int position) {
return fragments.get(position);
}
@Override
public int getCount() {
return fragments.size();
}
@Override
public CharSequence getPageTitle(int position) {
return titles[position];
}
});
mTab.setTabMode(TabLayout.MODE_FIXED);
mTab.setupWithViewPager(mPager);
mTab.setTabTextColors(Color.GRAY,Color.RED);
}
}
執行一下程式碼如圖:
相信大家走到這一步都是so easy的啦,我就不具體解釋程式碼了,
首先我們看一下列印的log:
是不是跟我們一開始貼的那張圖期待的是一樣的?是的! 然後我們注意這麼一段程式碼
mPager.setOffscreenPageLimit(0);
我們改為:
mPager.setOffscreenPageLimit(1);
執行看到log:
結果跟我們上面設定為0是一樣的效果,也就是說即使我設定了0,ViewPager任然快取了1個Fragment,我們看看ViewPager原始碼。
public void setOffscreenPageLimit(int limit) {
if (limit < DEFAULT_OFFSCREEN_PAGES) {
Log.w(TAG, "Requested offscreen page limit " + limit + " too small; defaulting to " +
DEFAULT_OFFSCREEN_PAGES);
limit = DEFAULT_OFFSCREEN_PAGES;
}
if (limit != mOffscreenPageLimit) {
mOffscreenPageLimit = limit;
populate();
}
}
我們看看DEFAULT_OFFSCREEN_PAGES這個屬性預設是多少?我們可以看到DEFAULT_OFFSCREEN_PAGES 預設是1,跟我們猜測是一樣的,也就是說ViewPager預設快取了一個item。
private static final int DEFAULT_OFFSCREEN_PAGES = 1;
那麼如果不想讓ViewPager做快取怎麼辦呢? 只需要自己創一個ViewPager叫MyViewPager,然後把ViewPager的所有原始碼匯入進去,修改下
private static final int DEFAULT_OFFSCREEN_PAGES = 0;
即可!!,是不是soeasy呢? 這也算是一小小技巧吧,我們接著往下走……
下面我們來說說Fragment的懶載入,使用場景ViewPager+Fragment的時候,Fragment對使用者可見的時候去載入資料,以時間換空間,載入到哪個Fragment的時候才去載入資料,而不是一出來就載入所有資料,上網一搜一大堆實現方式,但是主要的還是利用重寫Fragment的setUserVisibleHint方法實現。
下面我們來看看為什麼通過重寫這個方法就可以實現了,我們看看我們的FragmentPagerAdapter的原始碼。
public abstract class FragmentPagerAdapter extends PagerAdapter {
private static final String TAG = "FragmentPagerAdapter";
private static final boolean DEBUG = false;
private final FragmentManager mFragmentManager;
private FragmentTransaction mCurTransaction = null;
private Fragment mCurrentPrimaryItem = null;
public FragmentPagerAdapter(FragmentManager fm) {
mFragmentManager = fm;
}
/**
* Return the Fragment associated with a specified position.
*/
public abstract Fragment getItem(int position);
@Override
public void startUpdate(ViewGroup container) {
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
final long itemId = getItemId(position);
// Do we already have this fragment?
String name = makeFragmentName(container.getId(), itemId);
Fragment fragment = mFragmentManager.findFragmentByTag(name);
if (fragment != null) {
if (DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment);
mCurTransaction.attach(fragment);
} else {
fragment = getItem(position);
if (DEBUG) Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment);
mCurTransaction.add(container.getId(), fragment,
makeFragmentName(container.getId(), itemId));
}
if (fragment != mCurrentPrimaryItem) {
fragment.setMenuVisibility(false);
fragment.setUserVisibleHint(false);
}
return fragment;
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
if (DEBUG) Log.v(TAG, "Detaching item #" + getItemId(position) + ": f=" + object
+ " v=" + ((Fragment)object).getView());
mCurTransaction.detach((Fragment)object);
}
@Override
public void setPrimaryItem(ViewGroup container, int position, Object object) {
Fragment fragment = (Fragment)object;
if (fragment != mCurrentPrimaryItem) {
if (mCurrentPrimaryItem != null) {
mCurrentPrimaryItem.setMenuVisibility(false);
mCurrentPrimaryItem.setUserVisibleHint(false);
}
if (fragment != null) {
fragment.setMenuVisibility(true);
fragment.setUserVisibleHint(true);
}
mCurrentPrimaryItem = fragment;
}
}
@Override
public void finishUpdate(ViewGroup container) {
if (mCurTransaction != null) {
mCurTransaction.commitAllowingStateLoss();
mCurTransaction = null;
mFragmentManager.executePendingTransactions();
}
}
@Override
public boolean isViewFromObject(View view, Object object) {
return ((Fragment)object).getView() == view;
}
@Override
public Parcelable saveState() {
return null;
}
@Override
public void restoreState(Parcelable state, ClassLoader loader) {
}
/**
* Return a unique identifier for the item at the given position.
*
* <p>The default implementation returns the given position.
* Subclasses should override this method if the positions of items can change.</p>
*
* @param position Position within this adapter
* @return Unique identifier for the item at position
*/
public long getItemId(int position) {
return position;
}
private static String makeFragmentName(int viewId, long id) {
return "android:switcher:" + viewId + ":" + id;
}
其實呢也就是繼承了一個PagerAdapter,跟我們平時的用法一樣,我們看看程式碼會發現在這個類中setUserVisibleHint方法多次被呼叫,我們簡單擼一擼原始碼,
@Override
public Object instantiateItem(ViewGroup container, int position) {
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
final long itemId = getItemId(position);
// Do we already have this fragment?
String name = makeFragmentName(container.getId(), itemId);
Fragment fragment = mFragmentManager.findFragmentByTag(name);
if (fragment != null) {
if (DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment);
mCurTransaction.attach(fragment);
} else {
fragment = getItem(position);
if (DEBUG) Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment);
mCurTransaction.add(container.getId(), fragment,
makeFragmentName(container.getId(), itemId));
}
if (fragment != mCurrentPrimaryItem) {
fragment.setMenuVisibility(false);
fragment.setUserVisibleHint(false);
}
return fragment;
}
在Adapter的instantiateItem方法中,將即將展現的Fragment的fragment.setUserVisibleHint(false);isVisible賦值為了false,也就是此時Fragment還不可見。
我們緊接著看看FragmentPagerAdapter中有一個setPrimaryItem的方法,將setUserVisibleHint賦值為了true,也就是說此時的Fragment狀態變為了對使用者可見,是的!setPrimaryItem方法是在Fragment即將對使用者可見的時候呼叫。
@Override
public void setPrimaryItem(ViewGroup container, int position, Object object) {
Fragment fragment = (Fragment)object;
if (fragment != mCurrentPrimaryItem) {
if (mCurrentPrimaryItem != null) {
mCurrentPrimaryItem.setMenuVisibility(false);
mCurrentPrimaryItem.setUserVisibleHint(false);
}
if (fragment != null) {
fragment.setMenuVisibility(true);
fragment.setUserVisibleHint(true);
}
mCurrentPrimaryItem = fragment;
}
}
到了這裡,想必大家對Fragment懶載入的實現有了一點小小的思路了咯。
我們把我們程式碼改改(重寫下setUserVisibleHint方法):
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
Log.e(TAG+" "+pageNo,"---->setUserVisibleHint-->"+isVisibleToUser);
}
我們來執行下程式碼看看log:
我們看見當呼叫完第一個Fragment的onCreate方法的時候呼叫了setUserVisibleHint–>true此時第一個Fragment對使用者可見。同時也發現,setUserVisibleHint先於onCreate方法執行,並且執行了兩次,但是此時的Fragment對使用者不可見。
我們從1—–>2,然後看看log:
第一個Fragment的setUserVisibleHint設為了false,即將出現的第二個Fragment的setUserVisibleHint設定為了true。
我們從2—–>1,然後看看log:
第一個Fragment的setUserVisibleHint設為了true,即將消失的第二個Fragment的setUserVisibleHint設定為了false。
好了,到了這裡相信你一定很有思路了。。。。。。
那我們就一起來實現一下Fragment的懶載入。
首先建立一個BaseFragment:
**
* Author:Yqy
* Date:2016-09-21
* Desc:
* Company:cisetech
*/
public abstract class BaseFragment extends Fragment {
/**
* 是否對使用者可見標記
*/
protected boolean isVisible;
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if(isVisibleToUser){
isVisible = true;
onVisible();
}else{
isVisible = false;
onInvisible();
}
}
/**
* 不可見的時候操作
*/
protected void onVisible(){
lazyLoad();
}
/**
* 抽象lazyLoad方法,讓子類實現。
*/
protected abstract void lazyLoad();
protected void onInvisible(){}
}
然後讓MyFragment繼承BaseFragment:
**
* @author EX_YINQINGYANG
* @version [Android PABank C01, @2016-09-21]
* @date 2016-09-21
* @description
*/
public class MyFragment extends BaseFragment {
/**
* 初始化View完畢的標記
*/
private boolean isPrepared;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
TextView view=new TextView(getActivity());
view.setGravity(Gravity.CENTER);
view.setTextColor(Color.BLACK);
view.setTextSize(TypedValue.COMPLEX_UNIT_DIP,18f);
isPrepared=true;
lazyLoad();
return view;
}
private ProgressDialog dialog;
@Override
protected void lazyLoad() {
if(!isPrepared || !isVisible) {
return;
}
dialog=ProgressDialog.show(getActivity(),"","載入中,請稍後...");
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
Toast.makeText(getActivity(),"載入完畢!",Toast.LENGTH_SHORT).show();
((TextView)getView()).setText(""+getArguments().getInt("PAGE_NO"));
dialog.dismiss();
}
},3000);
//填充各控制元件的資料
}
public static Fragment newInstance(int i) {
MyFragment fragment=new MyFragment();
Bundle bundle=new Bundle();
bundle.putInt("PAGE_NO",i);
fragment.setArguments(bundle);
return fragment;
}
}
因為setUserVisibleHint會先於onCreateView執行,如果這個時候載入資料的話,然後賦值會報各種空指標錯誤,所以當執行完onCreateView方法後,此時的isVisible也為true,所以再去執行一次lazyLoad();方法。
Fragment就暫時先研究到這裡了,3Q!!!
最後給出demo的github連結: