正確實現ViewPager+多Fragment組合方式展示UI

1.簡介

在項目中有時會使用ViewPager展示頁面,在頁面較少時很簡單直接使用FragmentPagerAdapter即可滿足(非本文重點略過)。但是如果有很多幾十甚至幾百時,如果還像原來那么使用,就會出現界面卡,內存占用高,甚至會出現OOM問題(不信的話自己可以嘗試下)。那么要如何才能高效、低耗內存的實現呢?下面就重點來講講。

2.正確使用

2.1 準備

重寫系統FragmentStatePagerAdapter類,為什么要重新呢?因為系統源碼中緩存采用的是ArrayList進行數據緩存,當Frgment數量過大時會出現錯誤,所以這里進行了重寫,采用LongSparseArray對Fragment和SaveState進行緩存,代碼如下:

package com.zhh.commonview.adapter;

import android.os.Bundle;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.app.FragmentTransaction;
import android.support.v4.util.LongSparseArray;
import android.support.v4.view.PagerAdapter;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;

/**
 * Created by zhanghai on 2020/3/9.
 *
 * function:
 * ......
 * 由于篇幅注釋忽略,參考系統的FragmentStatePagerAdapter注釋
 */
public abstract class BaseFragmentStatePagerAdapter extends PagerAdapter {
    private static final String TAG = "FragmentPagerAdapter";

    @NonNull
    private final FragmentManager mFragmentManager;
    @Nullable
    private FragmentTransaction mCurTransaction = null;
    @Nullable
    private Fragment mCurrentPrimaryItem = null;

    @NonNull
    private final LongSparseArray<Fragment> mFragments = new LongSparseArray<>();
    @NonNull
    private final LongSparseArray<Fragment.SavedState> mSavedStates = new LongSparseArray<>();

    public BaseFragmentStatePagerAdapter(@NonNull FragmentManager fm) {
        mFragmentManager = fm;
    }

    /**
     * Clear cache data.
     */
    public void clear(){
        if(mFragments != null && mFragments.size() > 0){
            mFragments.clear();
        }
        if(mSavedStates != null && mSavedStates.size() > 0){
            mSavedStates.clear();
        }
    }

    /**
     * Return the Fragment associated with a specified position.
     */
    public abstract Fragment getItem(int position);

    /**
     * 獲取指定position的fragment
     * @return
     */
    public Fragment getCurrentPrimaryItem(int position){
        long tag = getItemId(position);
        return mFragments.get(tag);
    }

    @Override
    public void startUpdate(@NonNull ViewGroup container) {
        if (container.getId() == View.NO_ID) {
            throw new IllegalStateException("ViewPager with adapter " + this
                    + " requires a view id");
        }
    }

    @Override
    @NonNull
    public Object instantiateItem(ViewGroup container, int position) {
        long tag = getItemId(position);
        Fragment fragment = mFragments.get(tag);
        // If we already have this item instantiated, there is nothing
        // to do.  This can happen when we are restoring the entire pager
        // from its saved state, where the fragment manager has already
        // taken care of restoring the fragments we previously had instantiated.
        if (fragment != null) {
            return fragment;
        }

        if (mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
        }

        fragment = getItem(position);
        // restore state
        final Fragment.SavedState savedState = mSavedStates.get(tag);
        if (savedState != null) {
            fragment.setInitialSavedState(savedState);
        }
        fragment.setMenuVisibility(false);
        fragment.setUserVisibleHint(false);
        mFragments.put(tag, fragment);
        mCurTransaction.add(container.getId(), fragment, "f" + tag);

        return fragment;
    }

    @Override
    public void destroyItem(ViewGroup container, int position, @NonNull Object object) {
        Fragment fragment = (Fragment) object;
        int currentPosition = getItemPosition(fragment);

        int index = mFragments.indexOfValue(fragment);
        long fragmentKey = -1;
        if (index != -1) {
            fragmentKey = mFragments.keyAt(index);
            mFragments.removeAt(index);
        }

        //item hasn't been removed
        if (fragment.isAdded() && currentPosition != POSITION_NONE) {
            mSavedStates.put(fragmentKey, mFragmentManager.saveFragmentInstanceState(fragment));
        } else {
            mSavedStates.remove(fragmentKey);
        }

        if (mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
        }

        mCurTransaction.remove(fragment);
    }

    @Override
    public void setPrimaryItem(ViewGroup container, int position, @Nullable 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.commitNowAllowingStateLoss();
            mCurTransaction = null;
        }
    }

    @Override
    public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
        return ((Fragment) object).getView() == view;
    }

    @Override
    public Parcelable saveState() {
        Bundle state = null;
        if (mSavedStates.size() > 0) {
            // save Fragment states
            state = new Bundle();
            long[] stateIds = new long[mSavedStates.size()];
            for (int i = 0; i < mSavedStates.size(); i++) {
                Fragment.SavedState entry = mSavedStates.valueAt(i);
                stateIds[i] = mSavedStates.keyAt(i);
                state.putParcelable(Long.toString(stateIds[i]), entry);
            }
            state.putLongArray("states", stateIds);
        }
        for (int i = 0; i < mFragments.size(); i++) {
            Fragment f = mFragments.valueAt(i);
            if (f != null && f.isAdded()) {
                if (state == null) {
                    state = new Bundle();
                }
                String key = "f" + mFragments.keyAt(i);
                mFragmentManager.putFragment(state, key, f);
            }
        }
        return state;
    }

    @Override
    public void restoreState(@Nullable Parcelable state, ClassLoader loader) {
        if (state != null) {
            Bundle bundle = (Bundle) state;
            bundle.setClassLoader(loader);
            long[] fss = bundle.getLongArray("states");
            mSavedStates.clear();
            mFragments.clear();
            if (fss != null) {
                for (long fs : fss) {
                    mSavedStates.put(fs, (Fragment.SavedState) bundle.getParcelable(Long.toString(fs)));
                }
            }
            Iterable<String> keys = bundle.keySet();
            for (String key : keys) {
                if (key.startsWith("f")) {
                    Fragment f = mFragmentManager.getFragment(bundle, key);
                    if (f != null) {
                        f.setMenuVisibility(false);
                        mFragments.put(Long.parseLong(key.substring(1)), f);
                    } else {
                        Log.w(TAG, "Bad fragment at key " + key);
                    }
                }
            }
        }
    }

    /**
     * Return a unique identifier for the item at the given position.
     * <p>
     * <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;
    }
}

邏輯基本跟系統的FragmentStatePagerAdapter邏輯一致。新建一個adapter類繼承該Adapter,并重寫下方法:

@Override
    public int getCount() {
        return mItems == null ? 0 : mItems.size();
    }

    @Override
    public long getItemId(int position) {
        return mItems == null || mItems.size() <=0 ? super.getItemId(position) : mItems.get(position).hashCode();
    }

    @Override
    public int getItemPosition(Object object) {
        if(object instanceof BaseMoreFragmentAdv){
            //TODO:這里是重點
            BaseMoreFragmentAdv item = (BaseMoreFragmentAdv) object;
            int itemValue = item.getFragmentIdentifier();
            for (int i = 0; i < mItems.size(); i++) {
                if (mItems.get(i).hashCode() == itemValue) {
                    return i;
                }
            }
        }
        return POSITION_NONE;
    } 

其中關鍵點是getItemPosition方法,這里通過判斷標記唯一性去檢查是否新new一個fragment。item.getFragmentIdentifier()是在Fragment中實現的一個方法,該方法是返回Fragment的唯一標識,主要可通過傳入的數據進行綁定,我這里是通過重寫類的hashCode方法與之綁定,確保唯一性。

2.2 Adapter使用

通過數據去創建Fragment,并且不自己進行維護一個List去存儲Fragment。

private void initPagers() {
        final List<ObjectBean> list = new ArrayList<>();
        for (int i = 0;i < 200;i++){
            list.add(new ObjectBean(String.format(Locale.CHINESE,"fragment_%d",i)));
        }
        CommonViewPagerFragmentStateAdapter adapter = new CommonViewPagerFragmentStateAdapter<ObjectBean>(getSupportFragmentManager(),list) {
            @Override
            public Fragment getItem(final int position) {
                ObjectBean bean = list.get(position);
                TestFragment f = new TestFragment();
                Bundle bundle = new Bundle();
                bundle.putInt("identifier",bean.hashCode());
                bundle.putString("index",bean.index);
                f.setArguments(bundle);
                return f;
            }
        };

        vp_pager.setAdapter(adapter);
    }

代碼很簡單,很容易理解。
針對Fragment多的情況下:
請勿先創建Fragment保存到List中,然后把該list傳給Adapter。
請勿先創建Fragment保存到List中,然后把該list傳給Adapter。
請勿先創建Fragment保存到List中,然后把該list傳給Adapter。
錯誤例子:

List<TestFragment> fragments = new ArrayList<>();
        for (int i = 0;i < 200;i++){
            TestFragment f = new TestFragment();
            String index = String.format(Locale.CHINESE,"fragment_%d",i);
            Bundle bundle = new Bundle();
            bundle.putInt("identifier",index.hashCode());
            bundle.putString("index",index);
            f.setArguments(bundle);
            fragments.add(f);
        }
CommonViewPagerFragmentStateAdapter adapter = new CommonViewPagerFragmentStateAdapter<ObjectBean>(getSupportFragmentManager(),fragments);

至于原因,我想源碼中注釋的一句話最能說明。如下:

* <p>This version of the pager is more useful when there are a large number
 * of pages, working more like a list view.  When pages are not visible to
 * the user, their entire fragment may be destroyed, only keeping the saved
 * state of that fragment.  This allows the pager to hold on to much less
 * memory associated with each visited page as compared to
 * {@link FragmentPagerAdapter} at the cost of potentially more overhead when
 * switching between pages.

大致意思是說,當有大量頁數時它會跟有用,類似列表。當頁面對用戶不可見時,整個fragment會被銷毀,僅保留Fragment對應的SaveState數據,這樣就能保證占用內存低。

所以如果你自己存儲了一份Fragment list,那么就起不到低占內存的作用。

3 擴展

其實看到上面就已經能滿足ViewPager+Fragment使用了。
那么為什么還有個擴展呢?其實這里主要是想講講我在使用的過程碰到的問題。

1.TransactionTooLargeException異常

這個問題主要是由于使用Bundle傳遞到Fragment中造成的,Bundle數據累計超過Android設定的上限。具體可參考https://blog.csdn.net/self_study/article/details/60136277,這里對該異常進行了詳細描述。
下面我就說一說我的解決方案:
Bundle傳的數據大,想要不拋異常,只能降低Bundle傳的數據,因此我是通過修改傳索引的方式,而非直接穿序列化的實體,然后在Fragment中通過索引從列表中獲取數據。例子如下,一看就明白。
Activity中:

mPagerAdapter = new CommonViewPagerFragmentStateAdapter<MistakeBean>(getSupportFragmentManager(), mistakeBeanList){
                @Override
                public Fragment getItem(int position) {
                    Bundle bundle = new Bundle();
                    ...
                      //原來的使用方式
//                    bundle.putSerializable("bean", mistakeBeanList.get(position));
                    //修改后的使用方式
                    bundle.putInt("position",position);
                    ...
                    RedoMistakeFragment_ fragment_ = new RedoMistakeFragment_();
                    fragment_.setArguments(bundle);
                    return fragment_;
                }
            };

在Fragment中通過索引獲取對應的數據:

......
private int mFragmentPosition;//當前fragment對應的索引
private void init() {
      Bundle bundle = getArguments();
      if (bundle != null) {
          mFragmentPosition = bundle.getInt("position");
          //通過索引獲取緩存列表中的實體數據
          mBean = (MistakeBean) TaskCacheManager.getInstance().getQuestion(mFragmentPosition);
      }
}
2.同界面UI刷新問題

在當前的界面中刷新UI,即重新執行ViewPager初始化時:
1.如果數據列表數量發生變化,需要清除adapter中的數據,可調用clear()方法,否則會出現越界問題;
2.如果數據數量未發生變化,可不清除原adapter中的緩存數據,當然這里建議要清除下,減少內存使用;

4.源碼例子

https://github.com/zhang-hai/zhhcommonview

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容