reorder pages in FragmentStatePagerAdapter using getItemPosition(Object object)

前端 未结 4 441
梦谈多话
梦谈多话 2020-12-04 20:08

I believe that FragmentStatePagerAdapter does not behave correctly when overriding getItemPosition(Object object) with the purpose of reordering the pages.

相关标签:
4条回答
  • 2020-12-04 20:22

    Okay, I have found a solution. This fixes the reordering issue of viewpager fragments, in case you are creating/modifying new tabs dynamically.

    Use this class in place of FragmentStatePagerAdapter.java

    package android.support.v4.app;
    import android.os.Bundle;
    import android.os.Parcelable;
    import android.support.v4.view.PagerAdapter;
    import android.util.Log;
    import android.view.View;
    import android.view.ViewGroup;
    
    import java.util.ArrayList;
    
    public abstract class NewFragmentStatePagerAdapter extends PagerAdapter {
        private static final String TAG = "FragmentStatePagerAdapt";
        private static final boolean DEBUG = false;
    
        private final FragmentManager mFragmentManager;
        private FragmentTransaction mCurTransaction = null;
    
        private ArrayList<Fragment.SavedState> mSavedState = new ArrayList<Fragment.SavedState>();
        private ArrayList<Fragment> mFragments = new ArrayList<Fragment>();
        private Fragment mCurrentPrimaryItem = null;
    
        public NewFragmentStatePagerAdapter(FragmentManager fm) {
            mFragmentManager = fm;
        }
    
        /**
         * Return the Fragment associated with a specified position.
         */
        public abstract Fragment getItem(int position);
    
        @Override
        public void startUpdate(ViewGroup container) {
            if (container.getId() == View.NO_ID) {
                throw new IllegalStateException("ViewPager with adapter " + this
                        + " requires a view id");
            }
        }
        public void destroyItemState(int position) {
            mFragments.remove(position);
            mSavedState.remove(position);
        }
    
        @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);
            fragment.setUserVisibleHint(false);
            mFragments.set(position, fragment);
            mCurTransaction.add(container.getId(), fragment);
    
            return fragment;
        }
    
        @Override
        public void destroyItem(ViewGroup container, int position, Object object) {
            Fragment fragment = (Fragment) object;
    
            if (mCurTransaction == null) {
                mCurTransaction = mFragmentManager.beginTransaction();
            }
            if (DEBUG) Log.v(TAG, "Removing item #" + position + ": f=" + object
                    + " v=" + ((Fragment)object).getView());
            while (mSavedState.size() <= position) {
                mSavedState.add(null);
            }
            mSavedState.set(position, fragment.isAdded()
                    ? mFragmentManager.saveFragmentInstanceState(fragment) : null);
            mFragments.set(position, null);
    
            mCurTransaction.remove(fragment);
        }
    
        @Override
        @SuppressWarnings("ReferenceEquality")
        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.commitNowAllowingStateLoss();
                mCurTransaction = null;
            }
        }
    
        @Override
        public boolean isViewFromObject(View view, Object object) {
            return ((Fragment)object).getView() == view;
        }
    
        @Override
        public Parcelable saveState() {
            Bundle state = null;
            if (mSavedState.size() > 0) {
                state = new Bundle();
                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 && f.isAdded()) {
                    if (state == null) {
                        state = new Bundle();
                    }
                    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);
                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);
                        }
                    }
                }
            }
        }
    }

    and use this to overide the method

     @Override
            public void destroyItem(ViewGroup container, int position, Object object) {
                super.destroyItem(container, position, object);
                if (getItemPosition(object) == POSITION_NONE) {
                    destroyItemState(position);
                }
            }

    Source : https://issuetracker.google.com/issues/36956111

    0 讨论(0)
  • 2020-12-04 20:26

    I've reimplemented the existing solution in Kotlin such that it allows you to return a String instead of a long for the item id. You can find it here or below:

    import android.annotation.SuppressLint
    import android.os.Bundle
    import android.os.Parcelable
    import android.support.v4.app.Fragment
    import android.support.v4.app.FragmentManager
    import android.support.v4.app.FragmentTransaction
    import android.view.View
    import android.view.ViewGroup
    import java.util.HashSet
    import java.util.LinkedHashMap
    
    /**
     * A PagerAdapter that can withstand item reordering. See
     * https://issuetracker.google.com/issues/36956111.
     *
     * @see android.support.v4.app.FragmentStatePagerAdapter
     */
    abstract class MovableFragmentStatePagerAdapter(
            private val manager: FragmentManager
    ) : NullablePagerAdapter() {
        private var currentTransaction: FragmentTransaction? = null
        private var currentPrimaryItem: Fragment? = null
    
        private val savedStates = LinkedHashMap<String, Fragment.SavedState>()
        private val fragmentsToItemIds = LinkedHashMap<Fragment, String>()
        private val itemIdsToFragments = LinkedHashMap<String, Fragment>()
        private val unusedRestoredFragments = HashSet<Fragment>()
    
        /** @see android.support.v4.app.FragmentStatePagerAdapter.getItem */
        abstract fun getItem(position: Int): Fragment
    
        /**
         * @return a unique identifier for the item at the given position.
         */
        abstract fun getItemId(position: Int): String
    
        /** @see android.support.v4.app.FragmentStatePagerAdapter.startUpdate */
        override fun startUpdate(container: ViewGroup) {
            check(container.id != View.NO_ID) {
                "ViewPager with adapter $this requires a view id."
            }
        }
    
        /** @see android.support.v4.app.FragmentStatePagerAdapter.instantiateItem */
        override fun instantiateItem(container: ViewGroup, position: Int): Any {
            val itemId = getItemId(position)
    
            val f = itemIdsToFragments[itemId]
            if (f != null) {
                unusedRestoredFragments.remove(f)
                return f
            }
    
            if (currentTransaction == null) {
                // We commit the transaction later
                @SuppressLint("CommitTransaction")
                currentTransaction = manager.beginTransaction()
            }
    
            val fragment = getItem(position)
            fragmentsToItemIds.put(fragment, itemId)
            itemIdsToFragments.put(itemId, fragment)
    
            val fss = savedStates[itemId]
            if (fss != null) {
                fragment.setInitialSavedState(fss)
            }
            fragment.setMenuVisibility(false)
            fragment.userVisibleHint = false
    
            currentTransaction!!.add(container.id, fragment)
    
            return fragment
        }
    
        /** @see android.support.v4.app.FragmentStatePagerAdapter.destroyItem */
        override fun destroyItem(container: ViewGroup, position: Int, fragment: Any) {
            (fragment as Fragment).destroy()
        }
    
        /** @see android.support.v4.app.FragmentStatePagerAdapter.setPrimaryItem */
        override fun setPrimaryItem(container: ViewGroup, position: Int, fragment: Any?) {
            fragment as Fragment?
            if (fragment !== currentPrimaryItem) {
                currentPrimaryItem?.let {
                    it.setMenuVisibility(false)
                    it.userVisibleHint = false
                }
    
                fragment?.setMenuVisibility(true)
                fragment?.userVisibleHint = true
                currentPrimaryItem = fragment
            }
        }
    
        /** @see android.support.v4.app.FragmentStatePagerAdapter.finishUpdate */
        override fun finishUpdate(container: ViewGroup) {
            if (!unusedRestoredFragments.isEmpty()) {
                for (fragment in unusedRestoredFragments) fragment.destroy()
                unusedRestoredFragments.clear()
            }
    
            currentTransaction?.let {
                it.commitAllowingStateLoss()
                currentTransaction = null
                manager.executePendingTransactions()
            }
        }
    
        /** @see android.support.v4.app.FragmentStatePagerAdapter.isViewFromObject */
        override fun isViewFromObject(view: View, fragment: Any): Boolean =
                (fragment as Fragment).view === view
    
        /** @see android.support.v4.app.FragmentStatePagerAdapter.saveState */
        override fun saveState(): Parcelable? = Bundle().apply {
            putStringArrayList(KEY_FRAGMENT_IDS, ArrayList<String>(savedStates.keys))
            putParcelableArrayList(
                    KEY_FRAGMENT_STATES,
                    ArrayList<Fragment.SavedState>(savedStates.values)
            )
    
            for ((f, id) in fragmentsToItemIds.entries) {
                if (f.isAdded) {
                    manager.putFragment(this, "$KEY_FRAGMENT_STATE$id", f)
                }
            }
        }
    
        /** @see android.support.v4.app.FragmentStatePagerAdapter.restoreState */
        override fun restoreState(state: Parcelable?, loader: ClassLoader?) {
            if ((state as Bundle?)?.apply { classLoader = loader }?.isEmpty == false) {
                state!!
    
                fragmentsToItemIds.clear()
                itemIdsToFragments.clear()
                unusedRestoredFragments.clear()
                savedStates.clear()
    
                val fragmentIds: List<String> = state.getStringArrayList(KEY_FRAGMENT_IDS)
                val fragmentStates: List<Fragment.SavedState> =
                        state.getParcelableArrayList(KEY_FRAGMENT_STATES)
    
                for ((index, id) in fragmentIds.withIndex()) {
                    savedStates.put(id, fragmentStates[index])
                }
    
                for (key: String in state.keySet()) {
                    if (key.startsWith(KEY_FRAGMENT_STATE)) {
                        val itemId = key.substring(KEY_FRAGMENT_STATE.length)
    
                        manager.getFragment(state, key)?.let {
                            it.setMenuVisibility(false)
                            fragmentsToItemIds.put(it, itemId)
                            itemIdsToFragments.put(itemId, it)
                        }
                    }
                }
    
                unusedRestoredFragments.addAll(fragmentsToItemIds.keys)
            }
        }
    
        private fun Fragment.destroy() {
            if (currentTransaction == null) {
                // We commit the transaction later
                @SuppressLint("CommitTransaction")
                currentTransaction = manager.beginTransaction()
            }
    
            val itemId = fragmentsToItemIds.remove(this)
            itemIdsToFragments.remove(itemId)
            if (itemId != null) {
                savedStates.put(itemId, manager.saveFragmentInstanceState(this))
            }
    
            currentTransaction!!.remove(this)
        }
    
        private companion object {
            const val KEY_FRAGMENT_IDS = "fragment_keys_"
            const val KEY_FRAGMENT_STATES = "fragment_states_"
            const val KEY_FRAGMENT_STATE = "fragment_state_"
        }
    }
    

    And the Java piece:

    import android.support.annotation.NonNull;
    import android.support.annotation.Nullable;
    import android.support.v4.view.PagerAdapter;
    import android.view.ViewGroup;
    
    /**
     * A PagerAdapter whose {@link #setPrimaryItem} is overridden with proper nullability annotations.
     */
    public abstract class NullablePagerAdapter extends PagerAdapter {
        @Override
        public void setPrimaryItem(@NonNull ViewGroup container,
                                   int position,
                                   @Nullable Object object) {
            // `object` is actually nullable. It's even in the dang source code which is hilariously
            // ridiculous:
            // `mAdapter.setPrimaryItem(this, mCurItem, curItem != null ? curItem.object : null);`
        }
    }
    
    0 讨论(0)
  • 2020-12-04 20:28

    Looking at the source of FragmentStatePagerAdapter, I figured out exactly what is going wrong. The FragmentStatePagerAdapter caches the fragments and saved states in ArrayLists: mFragments and mSavedState. But when the fragments are reordered, there's no mechanism for reordering the elements of mFragments and mSavedState. Therefore, the adapter will provide the wrong fragments to the pager.

    I've filed an issue for this, and attached a fixed implementation (NewFragmentStatePagerAdapter.java) to the issue. In the fix, I've added a getItemId() function to FragmentStatePagerAdapter. (This mirrors the reordering implementation in FragmentPagerAdapter.) An array of the itemIds by adapter position is stored at all times. Then, in notifyDataSetChanged(), the adapter checks if the itemIds array has changed. If it has, then mFragments and mSavedState are reordered accordingly. Further modifications can be found in destroyItem(), saveState() and restoreState().

    To use this class, getItemPosition() and getItemId() must be implemented consistently with getItem().

    0 讨论(0)
  • 2020-12-04 20:46

    For me worked one of answers of an issue. Answers #20 #21. Link to solution https://gist.github.com/ypresto/8c13cb88a0973d071a64. Best solution, works for updating pages and also reordering. Only in this solution Adapter didn't throw IndexOutOfBoundsExeption when destroying item (in method destroyItem), which is known bug for other solutions.

    0 讨论(0)
提交回复
热议问题