简体   繁体   English

ViewPager,FragmentStatePagerAdapter和无限滚动

[英]ViewPager, FragmentStatePagerAdapter and Infinite Scrolling

What I'm trying to achieve: 我想要实现的目标:

An Activity with a ViewPager that displays Fragments for a list of Objects in the Adapter ( FragmentStatePagerAdapter ). 具有ViewPager的Activity,它显示适配器中对象列表的FragmentStatePagerAdapterFragmentStatePagerAdapter )。

Initially the Activity loads N (lets say 5) objects from the SQLite DB into the Adapter. 最初,Activity将SQL(从5个)对象从SQLite DB加载到适配器中。 These objects are chosen with some randomness. 选择这些对象具有一些随机性。

When the user is reaching the end of the list, the activity shall load M (let M be 3) more objects from the DB, add them to the adapter and call notifyDataSetChanged() . 当用户到达列表的末尾时,活动应从DB中加载M(令M为3)更多对象,将它们添加到适配器并调用notifyDataSetChanged() When adding them, I check if the new Objects already exist in the list and if they do, the pre-existing one gets removed and the loaded one gets added to the list's tail. 添加它们时,我会检查列表中是否已存在新对象,如果已存在,则会删除预先存在的对象,并将已加载的对象添加到列表的尾部。

Thus, I'm trying to achieve something like an infinite scrolling ViewPager (NOT a "circular" ViewPager as I want new Objects to be fetched constantly, instead of going back to the begging of the list). 因此,我试图实现像无限滚动ViewPager (不是“循环”ViewPager,因为我希望不断获取新对象,而不是回到列表的乞讨)。

I have some working code and included a sample for the pattern I'm following down bellow. 我有一些工作代码,并包含了我正在跟踪的模式的样本。 However, I keep getting this exception and have no idea why: 但是,我一直得到这个例外,并且不知道为什么:

IllegalStateException: Fragment MyObjectFragment{id...} is not currently in the FragmentManager
at android.app.FragmentManagerImpl.saveFragmentInstanceState(FragmentManager.java:553)
at android.support.v13.app.FragmentStatePagerAdapter.destroyItem(FragmentStatePagerAdapter.java:140)
at android.support.v4.ViewPager.populate(ViewPager.java:1002)
...

Code Sample: 代码示例:

The Activity: 活动:

public class MyActivitty extends FragmentActivity {

    public MyPagerAdapter adapter;
    public ViewPager pager;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.my_acyivity_layout);
        pager = (ViewPager) findViewById(R.id.viewPager);

        ArrayList<MyObject> myObjects = new ArrayList<MyObject>();

        // loadInitialObjectsFromDB(int N) goes to SQLite DB and loads the N first objects to show on the ViewPager...
        myObjects = loadInitialObjectsFromDB(5); 

        // Adapter will use the previously fetched objects
        adapter = new MyPagerAdapter(this, getFragmentManager(), myObjects); 

        pager.setAdapter(adapter);
        pager.setOffscreenPageLimit(2);

        // (...)
    }

    // (...)
}

The PagerAdapter: PagerAdapter:

public class MyPagerAdapter extends FragmentStatePagerAdapter implements
    ViewPager.OnPageChangeListener {

    private MyActivity context;
    private ArrayList<MyObject> objectList;
    private int currentPosition = 0;

    // (...)

    public MyPagerAdapter(MyActivity context, FragmentManager fragmentManager, ArrayList<MyObject> objects) 
    {
            super(fragmentManager);
            this.context = context;
            this.objectList = objects;
    }

    @Override
    public Fragment getItem(int position) 
    {
            MyObject object = objectList.get(position);
            return MyObjectFragment.newInstance(position, object);
    }

    @Override
    public int getItemPosition(Object object){
            MyObjectFragment frag = (MyObjectFragment) object;
            MyObject object = frag.getMyObject();

            for(int i = 0; i < objectList.size(); i++)
            {
                    if(objectList.get(i).getId() == object.getId())
                    {
                            return i;
                    }
            }

            return PagerAdapter.POSITION_NONE;
    }

    @Override
    public int getCount()
    {       
            return objectList.size();
    }

    @Override
    public void onPageSelected(int position) 
    {
           currentPosition = position;
    }

    // (...)

}


@Override
public void onPageScrollStateChanged(int state) 
{   
    switch(state)
    {
    case ViewPager.SCROLL_STATE_DRAGGING:
            // (...)
        break;

    case ViewPager.SCROLL_STATE_IDLE:
         // if we are reaching the "end" of the list (while scrolling to the right), load more objects
            if(currentPosition <= position && position >= answerList.size()-1)
            {
                    // Function in MyActivity that fetches N more objects.
                    // and adds them to this adapter's ArrayList<MyObject> objectList
                    // it checks for duplicates so that if the fetched object was already in the back of the list, it is shown again
                    context.getMoreObjects(3); 
                    notifyDataSetChanged();
            }
        getMoreQuestions(currentPosition);



    case ViewPager.SCROLL_STATE_SETTLING:
        break;
    }

}

My Fragment: 我的碎片:

public class MyObjectFragment extends Fragment {

    // Object to represent
    private MyObject object;

    public static Fragment newInstance(MyActivity context, 
         int position, MyObject object) {

            MyObjectFragment frag = new MyObjectFragment();

            Bundle args = new Bundle();
            args.putParcelable("Object", object);
            frag.setArguments(args);

            return frag;
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
        Bundle savedInstanceState) {

            this.currentInflater = inflater;

            final View rootView = inflater.inflate(
                    R.layout.fragment_my_object, container, false);

            // get object from Bundle, set UI, events, etc...

    }

    // (...)

}

Any idea on why am I getting this Exception ? 我有什么想法得到这个Exception吗? It seems like the FragmentStatePagerAdapter is trying to destroy an item that no longer exists, but I don't understand why. 似乎FragmentStatePagerAdapter试图销毁不再存在的项目,但我不明白为什么。

EDIT 1: 编辑1:

If I comment my @Override getItemPosition(Object object) , I don't get the exception anymore. 如果我评论我的@Override getItemPosition(Object object) ,我不再得到异常了。 However, I need to override getItemPosition because there is a use case in which the user deletes the currently shown Object causing it to disappear from the adapter's array and forcing the getItemPosition to return POSITION_NONE if the item doesn't exist anymore. 但是,我需要覆盖getItemPosition因为有一个用例,用户在其中删除当前显示的Object,导致它从适配器的数组中消失,并强制getItemPosition返回POSITION_NONE如果该项目不再存在。

EDIT 2: 编辑2:

Now I do know that this exception only happens when I remove items from my adapter's objectList . 现在我知道只有当我从适配器的objectList删除项目时才会发生此异常。 I have two situations where MyObject instances are deleted from the objectList : 我有两种情况,从objectList中删除MyObject实例:

  • When the getMoreObjects() adds fetches an object from the DB that was already in the objectList , I delete it and re-add it to the head of the list. getMoreObjects()添加从已经在objectList的数据库中提取对象时,我将其删除并重新添加到列表的头部。 I do this to avoid having objects with the same Id in the objectList, as their Id is used by the getItemPosition() to know if they exist and their position. 我这样做是为了避免在objectList中具有相同Id的对象,因为getItemPosition()使用它们的Id来知道它们是否存在以及它们的位置。

  • Before returning, getMoreObjects() , removes the N first objects from the list. 在返回之前, getMoreObjects()从列表中删除N个第一个对象。 I do know that the FragmentStatePagerAdapter already saves memory by only keeping in memory fragments for some of the objects, but I still would like to avoid growing my objectList too much. 我知道FragmentStatePagerAdapter已经通过仅保留一些对象的内存片段来节省内存,但我仍然希望避免过多地增加我的objectList For now, I have this line commented, as it's not that important. 现在,我对这一行进行了评论,因为它并不重要。

Solved with the help of this question which itself points at this issue . 这个问题的帮助下解决了这个问题

FragmentStatePagerAdapter caches the fragments and their saved states in two ArrayLists : mFragments and mSavedState . FragmentStatePagerAdapter将片段及其保存的状态缓存在两个ArrayListsmFragmentsmSavedState But when the fragments' order changes (as could happen in my case), there's no mechanism for reordering the elements of mFragments and mSavedState . 但是当片段的顺序发生变化时(在我的情况下会发生),没有重新排序mFragmentsmSavedState元素的机制。 Therefore, the adapter will provide the wrong fragments to the pager. 因此,适配器将向寻呼机提供错误的片段。

I've adapted the code provided in that and changed the import from app.support.v4.Fragment to android.app.Fragment . 我已经调整了提供的代码,并将app.support.v4.Fragment的导入app.support.v4.Fragmentandroid.app.Fragment

public abstract class MyFragmentStatePagerAdapter extends PagerAdapter {
    private static final String TAG = "FragmentStatePagerAdapter";
    private static final boolean DEBUG = true;

    private final FragmentManager mFragmentManager;
    private FragmentTransaction mCurTransaction = null;

    private long[] mItemIds = new long[] {};
    private ArrayList<Fragment.SavedState> mSavedState = new ArrayList<Fragment.SavedState>();
    private ArrayList<Fragment> mFragments = new ArrayList<Fragment>();
    private Fragment mCurrentPrimaryItem = null;

    public MyFragmentStatePagerAdapter(FragmentManager fm) {
        mFragmentManager = fm;
        mItemIds = new long[getCount()];
        for (int i = 0; i < mItemIds.length; i++) {
            mItemIds[i] = getItemId(i);
        }
    }

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

    /**
     * Return a unique identifier for the item at the given position.
     */
    public int getItemId(int position) {
        return position;
    }

    @Override
    public void notifyDataSetChanged() {
        long[] newItemIds = new long[getCount()];
        for (int i = 0; i < newItemIds.length; i++) {
            newItemIds[i] = getItemId(i);
        }

        if (!Arrays.equals(mItemIds, newItemIds)) {
            ArrayList<Fragment.SavedState> newSavedState = new ArrayList<Fragment.SavedState>();
            ArrayList<Fragment> newFragments = new ArrayList<Fragment>();

            for (int oldPosition = 0; oldPosition < mItemIds.length; oldPosition++) {
                int newPosition = POSITION_NONE;
                for (int i = 0; i < newItemIds.length; i++) {
                    if (mItemIds[oldPosition] == newItemIds[i]) {
                        newPosition = i;
                        break;
                    }
                }
                if (newPosition >= 0) {
                    if (oldPosition < mSavedState.size()) {
                        Fragment.SavedState savedState = mSavedState.get(oldPosition);
                        if (savedState != null) {
                            while (newSavedState.size() <= newPosition) {
                                newSavedState.add(null);
                            }
                            newSavedState.set(newPosition, savedState);
                        }
                    }
                    if (oldPosition < mFragments.size()) {
                        Fragment fragment = mFragments.get(oldPosition);
                        if (fragment != null) {
                            while (newFragments.size() <= newPosition) {
                                newFragments.add(null);
                            }
                            newFragments.set(newPosition, fragment);
                        }
                    }
                }
            }

            mItemIds = newItemIds;
            mSavedState = newSavedState;
            mFragments = newFragments;
        }

        super.notifyDataSetChanged();
    }

    @Override
    public void startUpdate(ViewGroup container) {
    }

    @Override
    public Object instantiateItem(ViewGroup container, int position) {
        // 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 (mFragments.size() > position) {
            Fragment f = mFragments.get(position);
            if (f != null) {
                return f;
            }
        }

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

        Fragment fragment = getItem(position);
        if (DEBUG) Log.v(TAG, "Adding item #" + position + ": f=" + fragment);
        if (mSavedState.size() > position) {
            Fragment.SavedState fss = mSavedState.get(position);
            if (fss != null) {
                fragment.setInitialSavedState(fss);
            }
        }
        while (mFragments.size() <= position) {
            mFragments.add(null);
        }
        fragment.setMenuVisibility(false);
        mFragments.set(position, fragment);
        mCurTransaction.add(container.getId(), fragment);

        return fragment;
    }

    public void destroyItemState(int position) {
        mFragments.remove(position);
        mSavedState.remove(position);
    }

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

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

        //position = getItemPosition(object);
        if (DEBUG) Log.v(TAG, "Removing item #" + position + ": f=" + object
                + " v=" + ((Fragment)object).getView());
        if (position >= 0) {
            while (mSavedState.size() <= position) {
                mSavedState.add(null);
            }
            mSavedState.set(position, mFragmentManager.saveFragmentInstanceState(fragment));
            if(position < mFragments.size()){
                mFragments.set(position, null);
            }
        }

        mCurTransaction.remove(fragment);
    }

    @Override
    public void setPrimaryItem(ViewGroup container, int position, Object object) {
        Fragment fragment = (Fragment)object;
        if (fragment != mCurrentPrimaryItem) {
            if (mCurrentPrimaryItem != null) {
                mCurrentPrimaryItem.setMenuVisibility(false);
            }
            if (fragment != null) {
                fragment.setMenuVisibility(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() {
        Bundle state = new Bundle();
        if (mItemIds.length > 0) {
            state.putLongArray("itemids", mItemIds);
        }
        if (mSavedState.size() > 0) {
            Fragment.SavedState[] fss = new Fragment.SavedState[mSavedState.size()];
            mSavedState.toArray(fss);
            state.putParcelableArray("states", fss);
        }
        for (int i=0; i<mFragments.size(); i++) {
            Fragment f = mFragments.get(i);
            if (f != null) {
                String key = "f" + i;
                mFragmentManager.putFragment(state, key, f);
            }
        }
        return state;
    }

    @Override
    public void restoreState(Parcelable state, ClassLoader loader) {
        if (state != null) {
            Bundle bundle = (Bundle)state;
            bundle.setClassLoader(loader);
            mItemIds = bundle.getLongArray("itemids");
            if (mItemIds == null) {
                mItemIds = new long[] {};
            }
            Parcelable[] fss = bundle.getParcelableArray("states");
            mSavedState.clear();
            mFragments.clear();
            if (fss != null) {
                for (int i=0; i<fss.length; i++) {
                    mSavedState.add((Fragment.SavedState)fss[i]);
                }
            }
            Iterable<String> keys = bundle.keySet();
            for (String key: keys) {
                if (key.startsWith("f")) {
                    int index = Integer.parseInt(key.substring(1));
                    Fragment f = mFragmentManager.getFragment(bundle, key);
                    if (f != null) {
                        while (mFragments.size() <= index) {
                            mFragments.add(null);
                        }
                        f.setMenuVisibility(false);
                        mFragments.set(index, f);
                    } else {
                        Log.w(TAG, "Bad fragment at key " + key);
                    }
                }
            }
        }
    }
}

Credit for the original code goes to user @UgglyNoodle . 原始代码的信用额转到用户@UgglyNoodle

Then, instead of using FragmentStatePagerAdapter I use the MyFragmentStatePagerAdapter from above and override getItemPosition() and getItemId() consistently with getItem() . 然后,我使用上面的MyFragmentStatePagerAdapter而不是使用FragmentStatePagerAdapter ,并使用getItem()一致地覆盖getItemPosition()getItemId() getItem()

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM