存在一種需求,當用戶系統中,屬於某一組織的用戶登陸以後(或者帳戶切換),要求主頁面顯示不一樣的ViewPager + Fragment組合,而且要求app無需退出就能刷新組合以及組合中的頁面。android
此外,爲了保證Fragment和Fragment中View沒必要要的inflate和渲染,要求儘量重用已存在的Fragment和View。顯然FragmentPagerAdapter是首選。可是存在三個問題:緩存
一、FragmentPagerAdapter默認沒法更新,須要重寫getItemPosition,返回值爲PagerAdapter.POSITION_NONE才能夠更新app
二、重用的Fragment設置參數沒法從新初始化ide
三、重用的Fragment類型和新的Fragment類型存在不匹配問題,如舊的UserFragment頁面,可是新的要求是ListFragment,因此類型存在問題。oop
咱們須要重寫FragmentPagerAdapter,但問題是存在各類不方便的因素,所以,咱們須要自定義FragmentPagerAdapter。post
public abstract class CustomFragmentPagerAdapter 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; private final LongSparseArray<String> fragmentViewTypeManager = new LongSparseArray<String>(); public CustomFragmentPagerAdapter(FragmentManager fm) { mFragmentManager = fm; } @Override public void startUpdate(ViewGroup container) { if (container.getId() == View.NO_ID) { //viewPager必須賦值ID,不然沒法添加fragment throw new IllegalStateException("ViewPager with adapter " + this + " requires a view id"); } } @SuppressWarnings("ReferenceEquality") @Override public Object instantiateItem(ViewGroup container, int position) { mCurTransaction = beginTransaction(); final long itemId = getItemId(position); final int count = this.getFragmentTypeCount(); int fragmentType = 0; if(count>0){ fragmentType = getItemFragmentType(position); } if(fragmentType>0 && fragmentType>=count){ throw new IllegalArgumentException("{fragmentType's number >= fragmentTypeCount's number} is illegal"); } // 生成tag,用於保存和標記每一個位置的fragment final String name = makeFragmentName(container.getId(), itemId); //生成tag final String oldFragmentName = fragmentViewTypeManager.get(fragmentType); Fragment fragment = mFragmentManager.findFragmentByTag(name); if (fragment != null) { final String fragmentClassName = fragment.getClass().getName(); if(!fragmentClassName.equals(oldFragmentName)) { //若是發現新舊類型不一致,移除舊類型 if (DEBUG) Log.v(TAG, "Removeing item #" + itemId + ": f=" + fragment); mCurTransaction.remove(fragment); //獲取新類型 fragment = getItem(null,position); if (DEBUG) Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment); mCurTransaction.add(container.getId(), fragment,name); }else { Fragment newFragment = getItem(fragment,position); //獲取newFragment ,若是2次fragment不一致,移除舊的fragment if(newFragment!=fragment){ if (DEBUG) Log.v(TAG, "Removeing item #" + itemId + ": f=" + fragment); mCurTransaction.remove(fragment); fragment = newFragment; if (DEBUG) Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment); mCurTransaction.add(container.getId(), fragment,name); }else { //若是獲取到fragment與原來的是同一個,attach便可 if (DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment); mCurTransaction.attach(fragment); } } } else { fragment = getItem(fragment,position); if (DEBUG) Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment); mCurTransaction.add(container.getId(), fragment,name); } if(fragment!=null){ //保存類型,用來校驗緩存的正確性 fragmentViewTypeManager.put(fragmentType,fragment.getClass().getName()); } if (fragment != mCurrentPrimaryItem) { fragment.setMenuVisibility(false); fragment.setUserVisibleHint(false); } return fragment; } @Override public void destroyItem(ViewGroup container, int position, Object object) { mCurTransaction = beginTransaction(); if (DEBUG) Log.v(TAG, "Detaching item #" + getItemId(position) + ": f=" + object + " v=" + ((Fragment)object).getView()); mCurTransaction.detach((Fragment)object); //dattach fragment } @SuppressWarnings("ReferenceEquality") @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 } } @Override public void finishUpdate(ViewGroup container) { if (mCurTransaction != null) { mCurTransaction.commitNowAllowingStateLoss(); //提交,注意該方法將任務加入到mainLooper中,可能產生延遲 mCurTransaction = null; } } @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) { } /** * * 獲取每一個fragment的id,注意保證惟一性 */ public long getItemId(int position) { return position; } //生成tag public static String makeFragmentName(int viewId, long id) { return "android:switcher:" + viewId + ":" + id; } public FragmentTransaction beginTransaction(){ if (mCurTransaction == null) { mCurTransaction = mFragmentManager.beginTransaction(); } return mCurTransaction; } public FragmentManager getFragmentManager(){ return mFragmentManager; } /** * 獲取當前位置的fragment */ public abstract Fragment getItem(Fragment contentFragment,int position); /** * 獲取當前位置的type FragmentType */ public abstract int getItemFragmentType(int position); /** * 獲取當前類型的數量 FragmentCount */ public abstract int getFragmentTypeCount(); /** * 在ViewPager中調用,告訴ViewPager該位置的Fragment是能夠被替換和更新的 * 這裏人能夠繼續優化,因爲ViewPager.LayoutParams中的position是非public的,所以要優化能夠在該類的基類中完成 */ @Override public int getItemPosition(Object object) { if(!(object instanceOf Fragment)) { return PagerAdapter.POSITION_NONE; } return PagerAdapter.POSITION_UNCHANGED; } public boolean isEmpty(){ return getCount()==0; } }
到這裏,咱們即可以實現他的子類優化
static class MyPagerAdapter extends CustomFragmentPagerAdapter{ private ArrayList<FragmentTabEntity> dataEntities; private final String TAG_NAME = "MyPagerAdapter "; public MyPagerAdapter(FragmentManager fm,List<FragmentTabEntity> dataEntities) { super(fm); this.dataEntities = new ArrayList<>(); this.dataEntities.addAll(dataEntities); } @SuppressWarnings("unchecked") public void updateDataEntities(List<FragmentTabEntity> dataEntities) { this.dataEntities.clear(); if(dataEntities!=null && dataEntities.size()>0){ this.dataEntities.addAll(dataEntities); } this.notifyDataSetChanged(); } @Override public CharSequence getPageTitle(int position) { final FragmentTabEntity entity = dataEntities.get(position); return entity.getTitle(); } @Override public int getItemFragmentType(int position) { final FragmentTabEntity dataEntity = dataEntities.get(position); return dataEntity.getType(position); //獲取類型,注意,最大值不能大於getFragmentTypeCount() } @Override public int getFragmentTypeCount() { return FragmentTabEntity.getTotalTypeCount(); //獲取全部fragment的類型數量,通常是固定值,主要看程序實現方式了 } @Override public Fragment getItem(Fragment contentFragment,int position) { final int fragmentType = getItemFragmentType(position); final FragmentTabEntity dataEntity = dataEntities.get(position); BaseFragment fragment = null; if(contentFragment==null) { if (fragmentType == 0) { fragment = new IndexFragment(); } else if(fragmentType ==1){ fragment = new UserFragment(); } else if(fragmentType ==2){ fragment = new WebFragment(); }else if(fragmentType ==3){ fragment = new ListFragment(); } }else{ fragment = (BaseFragment) contentFragment; } fragment.setPosition(position); if(fragment!=null) { Bundle fb = new Bundle(); fb.putString(BaseFragment.KEY_TYPE, dataEntity.getType()); fb.putString(BaseFragment.KEY_TITLE, dataEntity.getTitle()); fragment.setNoneStateArguments(fb); //使用非狀態參數傳遞方法 } return fragment; } @Override public int getCount() { return dataEntities.size(); } }
使用方法ui
private MyPagerAdapter mTabPagerAdapter; public void updatePager(List<FragmentTabEntity> data) { if((pager.getAdapter() instanceof MyPagerAdapter)){ mTabPagerAdapter = (MyPagerAdapter) pager.getAdapter(); } if(mTabPagerAdapter==null){ mTabPagerAdapter = new MyPagerAdapter(getChildFragmentManager(),data); pager.setAdapter(mTabPagerAdapter); }else{ mTabPagerAdapter.updateDataEntities(data); } }
到這一步事實上咱們的自定義FragmentPagerAdapter已經完成了,可是這裏還存在不完美的問題,那就是Fragment中添加了View Cache的狀況,此外,對於生命週期的控制,可能或多或少出現舊頁面向新頁面過渡時閃爍問題。this
一、View Cache 問題spa
先來看看這種Fragment的定義方式
public class BaseFragment extends Fragment{ private SoftReference<View> mRootViewCache = null; //實現viewcache private boolean isFinishedInflated = false; private Bundle mNoneStateArguments; private int position = -1; public void setPosition(int position){ this.position = position; } public int getPosition(){ return this.position; } @Nullable @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){ View root = null; if(!cacheIsEmpty()){ root = mRootViewCache.get(); } if(root==null){ root = inflater.inflate(R.layout.base_view_layout, container, false); root.findViewById(R.id.toolbar).setVisibility(View.GONE); mRootViewCache = new SoftReference<View>(root); } return root; } @Override public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); try { isFinishedInflated = true; renderFragmentView(view); } catch (Exception e) { e.printStackTrace(); } } private boolean cacheIsEmpty(){ return mRootViewCache==null || mRootViewCache.get()==null; } @Override public void onResume() { super.onResume(); if(getUserVisibleHint() ){ onFragmetShow(); } } @Override public void setUserVisibleHint(boolean isVisibleToUser) { super.setUserVisibleHint(isVisibleToUser); if(!isFinishedInflated) return; if( getUserVisibleHint()){ onFragmetShow(); }else if(isResumed()){ onFragmetHide(); } } @Override public void onStop() { super.onStop(); if(getUserVisibleHint()){ onFragmetHide(); } } public void onFragmetShow(){ if(getView()==null) return ; getView().post(new Runnable(){ public void run(){ //這裏能夠用來獲取Fragment的參數,而後更新 } }); } public void onFragmetHide(){ } public void setNoneStateArguments(Bundle bundle){ //解決已初始化狀態的參數刷新問題 this.mNoneStateArguments = bundle; } public void getNoneStateArguments(){ return this.mNoneStateArguments!=null? this.mNoneStateArguments:new Bundle(); } }
對於原生頁面,新舊頁面閃爍並非很明顯,可是對於Webview頁面,這種閃爍很明顯,致使該問題的緣由是View Cache,所以,咱們須要在Fragment中添加clearView方法來清空一下Cache
public void clearView() { if(mRootViewCache!=null){ mRootViewCache.clear(); } }
在MyPagerAdapter的getItem方法中,咱們有必要植入一個flag
@Override public Fragment getItem(Fragment contentFragment,int position) { final int fragmentType = getItemFragmentType(position); final FragmentTabEntity dataEntity = dataEntities.get(position); BaseFragment fragment = null; if(contentFragment==null) { if (viewType == 0) { fragment = new IndexFragment(); } else if(fragmentType ==1){ fragment = new UserFragment(); } else if(fragmentType ==2){ fragment = new WebFragment(); }else if(fragmentType ==3){ fragment = new ListFragment(); } }else{ fragment = (BaseFragment) contentFragment; } final Bundle fa = fragment.getArguments(); if(fa!=null) { final String oldTag = fa.getString("md5", ""); if (!TextUtils.isEmpty(oldTag) && !oldUrl.oldTag(dataEntity.getMd5())) { fragment.clearView(); //若是tag不一致,清空一下view cache } } if(fragment!=null) { Bundle fb = new Bundle(); fb.putString("md5",dataEntity.getMd5()); //植入新的md5 tag fb.putString(BaseFragment.KEY_TYPE, dataEntity.getType()); fb.putString(BaseFragment.KEY_TITLE, dataEntity.getTitle()); fragment.setArguments(fb); } return fragment; }
二、生命週期問題
關於onFragmentShow與onFragmentHide的生命週期用法,請參考《Fragment頁面切換》,這裏咱們主要說一下mainLooper問題
public void onFragmetShow(){ if(getView()==null) return ; getView().post(new Runnable(){ public void run(){ //這裏能夠用來獲取Fragment的參數,而後更新 } }); }
若是要更新UI,咱們建議這裏使用post將消息發送到mainLooper,爲何要這樣呢?
主要緣由是FragmentPagerAdapter的finishUpdate中使用了commit方法,這個方法是將任務發送到mainLooper的隊列中,而不是當即執行。
@Override public void finishUpdate(ViewGroup container) { if (mCurTransaction != null) { mCurTransaction.commitNowAllowingStateLoss(); mCurTransaction = null; } }
基於隊列的先進先出,FragmentTransaction將更新消息加入到Fragment add/attach消息以後,咱們若是直接獲取argument可能出現數據不一致的問題,所以咱們須要將咱們的方法做爲任務一樣放入到mainLooper中。若是不這麼作,可能致使獲取到的argument是舊的,致使咱們更新時使用了舊的參數。固然,能夠參考《Android Fragment重複添加問題解決方法》,原理基本相同。
以上是通常常見的問題,至於其餘問題,能夠留言。