reorder pages in FragmentStatePagerAdapter using getItemPosition(Object object)

穿精又带淫゛_ 提交于 2019-11-27 18:23:09

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().

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.

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);`
    }
}

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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!