android可以無限迴圈滑動的ViewPager

MrChen的成長之路發表於2018-09-10

前言:最近有需求需要某個頁面是可以無限滑動的,這個頁面是用ViewPager實現的,但是ViewPager本身並不能無限滑動,所以想在android現有ViewPager的基礎之上,實現這個功能,本文提供基於PagerAdapter和FragmentPagerAdapter的可以複用view和fragment的一種實現,github地址:https://github.com/ChenSWD/InfiniteViewPager

分析:
ViewPager滑動邊界是怎麼判斷的?
ViewPager能不能滑動依賴於: mAdapter.getCount()和mCurItem變數,即:當前位置和item總個數。那我們直接讓mAdapter.getCount()返回一個很大的值(Integer.MAX_VALUE)就可以了,即有無限個item。
基於以上,這樣操作對效能上有多大的影響?其實ViewPager是有快取機制的,預設快取當前位置和其兩邊的各一個item,所以在記憶體上的問題是不存在的,但是我還是想自己快取一些可複用的view,向ListView那樣,因為一般在新建view時會執行LayoutInflater.inflate(),會解析xml格式的佈局檔案,會重複建立銷燬view,把view快取起來可以解決是個不錯的選擇。

基於PagerAdapter普通自定義佈局的分析:

因為要修改adapter和ViewPager的原始碼,所以把相關檔案拷貝一份:ViewPager(ViewPagerCopy),PagerAdapter(PagerAdapterCopy),FragmentPagerAdapter(InfiniteFragmentPagerAdapter)。
我們假設ViewPager需要迴圈的有3個item,position分別是0 1 2,把這個叫做item實際位置,在無限迴圈的ViewPager裡面,0的位置實際上對應的是Integer.MAX_VALUE/21-Integer.MAX_VALUE/2+12-Integer.MAX_VALUE/2+2,把對映後的位置稱為item的擴充套件位置。

1、新建InfinitePagerAdapter繼承PagerAdapterCopy。在PagerAdapterCopy裡面新增方法 public abstract int getRealCount();,該方法返回實際的item個數;新增方法:public int getRealPosition(int position){},該方法是給ViewPagerCopy裡OnPageChangeListener使用的,根據擴充套件的位置返回實際的位置;新增方法:public abstract void onPageSelected(int extendPosition, int realPosition),該方法在每一次切換頁面時呼叫。
2、所有需要迴圈的adapter繼承自InfinitePagerAdapter,需要說明的是getExtendItem()方法,該方法在設定ViewPager初始的時候顯示第幾頁時使用,根據實際的位置,返回擴充套件後的位置,所有destroyItem()instantiateItem()方法的position引數全部是擴充套件的位置:

public abstract class InfinitePagerAdapter extends PagerAdapterCopy {
    public static final int INFINITE_COUNT = Integer.MAX_VALUE;

    @Override
    public int getCount() {
        //返回無限個
        return INFINITE_COUNT;
    }

    /**
     * 在呼叫setCurrentItem()時使用該方法返回擴充套件後的位置
     * 該方法需要在getRealCount()有返回值的時候使用,
     * ViewPager的setCurrentItem()方法要在設定資料之後呼叫,因為不知道 RealCount,就不能算出擴充套件後的位置
     */
    public int getExtendItem(int item) {
        int real = getRealCount();
        int total = getCount();
        if (real <= 0) {
            return item;
        }
        //找到大概中間位置的組號
        int index = total / real / 2;
        //返回中間組的第item個位置
        return index * real + item;
    }
}

3、下面給出具體使用的PagerActivity和PagerAdapter的程式碼,相關注釋在程式碼中都有,為什麼快取view的個數是limit*2+2,在程式碼中特別做了解釋:

public class PagerActivity extends AppCompatActivity {
    private ViewPagerCopy mViewPager;
    private PagerAdapter mAdapter;
    List<Integer> text = new ArrayList<>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_pager);
        for (int i = 0; i < 3; i++) {
            text.add(0);
        }
        //最多快取兩個(當前view一邊兩個,總共5個)
        int limit = 2;
        mViewPager = findViewById(R.id.view_pager);

        //快取view的個數為limit*2 + 2
        //為什麼是limit*2 + 2:正常情況下應該快取 limit*2 + 1 個view,因為左右兩邊各快取limit個,
        //在從左向右滑動換頁時是沒有問題的,因為會先執行destroyItem()方法再執行instantiateItem()方法,
        //這樣instantiateItem時就可以複用destroyItem時remove的view
        //但是ViewPager在從右向左滑動的時候,會先執行instantiateItem()方法再執行destroyItem()方法,
        // 這樣就必須多快取一個,即:limit*2 + 2個
        mAdapter = new PagerAdapter(limit * 2 + 2);
        mAdapter.setTextList(text);
        mViewPager.setAdapter(mAdapter);
        //設定快取的限制,預設會快取1個
        mViewPager.setOffscreenPageLimit(limit);
        //資料的設定(setTextList())要在setCurrentItem()之前,
        mViewPager.setCurrentItem(mAdapter.getExtendItem(1));
        mAdapter.notifyDataSetChanged();
    }
}

public class PagerAdapter extends InfinitePagerAdapter {
    View[] viewList;
    List<Integer> textList = new ArrayList<>();

    public PagerAdapter(int size) {
        viewList = new View[size];
    }

    public void setTextList(List<Integer> textList) {
        this.textList = textList;
    }

    @Override
    public Object instantiateItem(ViewGroup container, final int position) {
        final ViewHolder holder;
        //這裡position是擴充套件後的位置,所以要根據位置,計算出當前要複用的item的下標位置
        //迴圈複用viewList裡的view
        int viewPos = position % viewList.length;
        if (viewList[viewPos] == null) {
            View view = LayoutInflater.from(container.getContext()).inflate(R.layout.pager_adapter_item, container, false);
            holder = new ViewHolder(view);
            view.setTag(holder);
            viewList[viewPos] = view;
        } else {
            holder = (ViewHolder) viewList[viewPos].getTag();
        }
        final int realPos = position % textList.size();
        holder.textView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                textList.set(realPos, textList.get(realPos) + 1);
                holder.textView.setText("實際位置 " + realPos
                        + "\n是第" + position + "個擴充套件的位置"
                        + "\ncount = " + textList.get(realPos));
            }
        });
        bindViewHolder(position);
        container.addView(viewList[viewPos]);
        return viewList[viewPos];
    }

    //每一次切換頁面的時候重新整理一下狀態
    public void bindViewHolder(int extendPos) {
        int viewPos = extendPos % viewList.length;
        if (viewList[viewPos] != null) {
            final ViewHolder holder = (ViewHolder) viewList[viewPos].getTag();
            final int realPos = extendPos % textList.size();
            holder.textView.setText("實際位置 " + realPos
                    + "\n是第" + extendPos + "個擴充套件的位置"
                    + "\ncount = " + textList.get(realPos));
        }
    }

    @Override
    public int getRealCount() {
        return textList.size();
    }

    @Override
    public void onPageSelected(int extendPosition, int realPosition) {
        bindViewHolder(extendPosition);
    }

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

    @Override
    public void destroyItem(ViewGroup container, int position, Object object) {
        container.removeView((View) object);
    }

    private static class ViewHolder {
        private TextView textView;

        public ViewHolder(View view) {
            textView = view.findViewById(R.id.text);
        }
    }
}

基於FragmentPagerAdapter的Fragment無限迴圈的分析:

分析:因為ViewPager配合Fragment使用,很多時候Fragment都不相同,不能簡單的複用,一般來說實際Fragment是比較多的,直接在instantiateItem()方法中,把擴充套件position轉換成實際position就行(position%getRealCount)。
但是當實際fragment個數小於limit*2+2的時候,會出現fragment快取不夠用的情況,例如真實fragment:0,1,2,需要快取的個數limit=2,那麼就需要6個真實的fragment才能完成迴圈複用,比如當前位置為2,快取的fragment需要是0,1,2,3,4。3的位置是不能複用0位置的,因為同一個fragment不能被attach兩次,這時候fragment是不夠用的,怎麼辦呢?我是在3的位置new了一個新的和0位置一樣的fragment,4的位置new了一個和1一樣的fragment,然後新增到fragment棧中的。
我是這樣做的(感覺還可以完善很多東西),定義一個Map:Map<Integer, List<AbstractFragment>> fragmentMap;,key是fragment的實際位置,value是一個list,用於裝入在key這個位置重複的fragment,比如上面0和3都會被裝入到key為0的list中。
以下都是fragment很少,需要重複新增的情況下做的分析:
1、在AbstractFragment 中增加一個欄位表示當前fragment是不是能被複用(沒有被attach),因為用isAdded()和isDetached()方法似乎都不能標識:

public abstract class AbstractFragment<T> extends Fragment {
    //是否可被新增,初始的true
    private boolean isUsable = true;

    public boolean isUsable() {
        return isUsable;
    }

    public void setUsable(boolean isUsable) {
        this.isUsable = isUsable;
    }

    //用來重新整理fragment的狀態
    public abstract void refreshData(T data);
}

2、修改InfinitePagerAdapter的instantiateItem()destroyItem(),這兩個方法主要給fragment設定可用的標識(呼叫setUsable()方法),新增方法 protected abstract String findUsableFragmentTag(int pos)是自定義FragmentAdapter需要實現的,這個方法是用來根據擴充套件的位置,返回可用的fragment的Tag,改變了原先Tag獲取的方式:

 @Override
    public Object instantiateItem(ViewGroup container, int position) {
        if (mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
        }

        final long itemId = getItemId(position);
        //先查詢有沒有可以複用的fragment,沒有的話要呼叫getItem()新建
        String name = findUsableFragmentTag(position);
        if (name == null) {
            // Do we already have this fragment?
            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);
        }
        if (fragment instanceof AbstractFragment) {
            ((AbstractFragment) fragment).setUsable(false);
        }
        return fragment;
    }

    protected abstract String findUsableFragmentTag(int pos);

    @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);
        if (object instanceof AbstractFragment) {
            ((AbstractFragment) object).setUsable(true);
        }
    }

3、自定義的FragmentPagerAdapter,主要是getItem()方法,該方法根據擴充套件位置,返回一個可用的實際位置的fragment,做法是根據實際位置去查詢Map,找到list裡面第一個可用的fragment返回,如果list中無當前位置可用的fragment,就去新建一個:

public class FragmentPagerAdapter extends InfiniteFragmentPagerAdapter {
    private int realCount;
    private Map<Integer, List<AbstractFragment>> fragmentMap = new HashMap<>();
    private FragmentAdapterCallBack callBack;
    private List<DataEntity> entityList;

    public FragmentPagerAdapter(FragmentManager fm, List<AbstractFragment> fragments, FragmentAdapterCallBack callBack, List<DataEntity> entityList) {
        super(fm);
        realCount = fragments.size();
        for (int i = 0; i < fragments.size(); i++) {
            List<AbstractFragment> list = new ArrayList<>();
            list.add(fragments.get(i));
            fragmentMap.put(i, list);
        }
        this.callBack = callBack;
        this.entityList = entityList;
    }

    @Override
    public Fragment getItem(int position) {
        int realPos = position % getRealCount();
        List<AbstractFragment> fragments = fragmentMap.get(realPos);
        //根據實際位置去查詢Map,找到list裡面第一個可用的fragment返回
        for (AbstractFragment fragment : fragments) {
            if (fragment.isUsable()) {
                return fragment;
            }
        }
        AbstractFragment fragment = null;
        //如果list中無當前位置可用的fragment,就去新建一個
        if (callBack != null) {
            fragment = callBack.generateFragmentByPosition(realPos);
            fragments.add(fragment);
        }
        if (fragment == null)
            throw new NullPointerException("新生成的fragment不能為空");
        return fragment;
    }

    @Override
    protected String findUsableFragmentTag(int position) {
        int realPos = position % getRealCount();
        //根據實際位置去查詢Map,找到可用的就返回它的Tag
        List<AbstractFragment> fragments = fragmentMap.get(realPos);
        for (AbstractFragment fragment : fragments) {
            if (fragment.isUsable()) {
                return fragment.getTag();
            }
        }
        return null;
    }

    @Override
    public int getRealCount() {
        return realCount;
    }

    @Override
    public void onPageSelected(int extendPosition, int realPosition) {
        //在換頁的時候,判斷是不是要重新整理擴充套件後的fragment的狀態
        refreshFragmentsIfNeed(realPosition, entityList.get(realPosition));
    }

    //當前fragment是不是擴充套件新增的,用來判斷是否重新整理Fragment的狀態
    public void refreshFragmentsIfNeed(int pos, DataEntity entity) {
        //在當前位置有擴充套件的fragment的情況下,且當前位置的資料被更改過,查詢當前已經attach的fragment,重新整理它們的狀態
        if (entity != null && entity.isRefresh()) {
            List<AbstractFragment> fragments = fragmentMap.get(pos);
            if (fragments != null && fragments.size() > 1) {
                for (AbstractFragment fragment : fragments) {
                    if (!fragment.isUsable()) {
                        fragment.refreshData(entity);
                    }
                }
            }
            entity.setRefresh(false);
        }
    }
}

總結:以上基本完成了自定義PagerAdapter和FragmentPagerAdapter實現無限滑動的功能,這裡只是提供一種解決方式,在具體使用中可能還要具體修改一些邏輯。在後續個人使用中,會完善一些程式碼,如果發現一些bug也會及時修復,如果有好的思路也請指教。說了這麼多可能都不如看一下程式碼:https://github.com/ChenSWD/InfiniteViewPager

相關文章