web-dev-qa-db-fra.com

réorganiser les pages dans FragmentStatePagerAdapter à l'aide de getItemPosition (Object Object)

Je pense que FragmentStatePagerAdapter ne se comporte pas correctement lors de la substitution de getItemPosition(Object object) dans le but de réorganiser les pages.

Voici un exemple simple. Dans l'état initial, l'ordre des pages est {A, B, C}. Lors de l'appel de toggleState(), l'ordre des pages est modifié en {A, C, B}. En remplaçant getItemPosition(Object object), nous nous assurons que la page en cours d'affichage (A, B ou C) ne change pas.

public static class TestPagerAdapter extends FragmentStatePagerAdapter {
    private boolean mState = true;

    public TestPagerAdapter(FragmentManager fragmentManager) {
        super(fragmentManager);
    }

    @Override
    public int getCount() {
        return 3;
    }

    private void toggleState() {
        mState = !mState;
        notifyDataSetChanged();
    }

    private String getLabel(int position) {
        switch (position) {
            case 0:
                return "A";
            case 1:
                return mState ? "B" : "C";
            default:
                return mState ? "C" : "B";
        }
    }

    @Override
    public int getItemPosition(Object object) {
        String label = ((TestFragment) object).getLabel();
        if (label.equals("A")) {
            return 0;
        } else if (label.equals("B")) {
            return mState ? 1 : 2;
        } else {
            return mState ? 2 : 1;
        }
    }

    @Override
    public CharSequence getPageTitle(int position) {
        return getLabel(position);
    }

    @Override
    public Fragment getItem(int position) {
        return TestFragment.newInstance(getLabel(position));
    }
}

J'ai rencontré deux comportements distincts qui semblent incorrects.

  1. Si j'appelle immédiatement toggleState() (lors de l'affichage de la page A, avant de passer à une autre page), l'application se bloque.

    Java.lang.IndexOutOfBoundsException: Invalid index 2, size is 2
      at Java.util.ArrayList.throwIndexOutOfBoundsException(ArrayList.Java:251)
      at Java.util.ArrayList.set(ArrayList.Java:477)
      at Android.support.v4.app.FragmentStatePagerAdapter.destroyItem(FragmentStatePagerAdapter.Java:136)
      at Android.support.v4.view.ViewPager.populate(ViewPager.Java:867)
      at Android.support.v4.view.ViewPager.setCurrentItemInternal(ViewPager.Java:469)
      at Android.support.v4.view.ViewPager.setCurrentItemInternal(ViewPager.Java:441)
      at Android.support.v4.view.ViewPager.dataSetChanged(ViewPager.Java:766)
      at Android.support.v4.view.ViewPager$PagerObserver.onChanged(ViewPager.Java:2519)
      at Android.database.DataSetObservable.notifyChanged(DataSetObservable.Java:37)
      at Android.support.v4.view.PagerAdapter.notifyDataSetChanged(PagerAdapter.Java:276)
      at com.ugglynoodle.test.testfragmentstatepageradapter.MainActivity$TestPagerAdapter.toggleState(MainActivity.Java:55)
      ...
    

    En regardant la source de FragmentStatePagerAdapter, cela serait corrigé en vérifiant d’abord la taille de mFragments (comme aux lignes 113 à 115) avant d’appeler set() à la ligne 136.

  2. Si je glisse d'abord vers la page B, alors getItem(2) est appelé, la page C est créée et mFragments a maintenant une taille de 3 (cela évitera que le crash ci-dessus ne se produise dans un instant). Ensuite, je glisse de nouveau à la page A, et la page C est détruite, comme il se doit (étant donné qu’elle se trouve à deux pages et que j’utilise la limite de page hors écran par défaut de 1). Maintenant, j'appelle toggleState(). La page B est maintenant détruite. Cependant, la page C n'est PAS recréée! Cela signifie que, lorsque je glisse maintenant vers la droite, la page est vide.

Tout d’abord, il serait bon de savoir si j’ai raison et qu’il s’agit en fait de bugs ou si je fais quelque chose de mal. S'il s'agit de bogues, quelqu'un peut-il suggérer une solution de contournement (autre que le débogage et la reconstruction de la bibliothèque de support moi-même)? Sûrement quelqu'un doit avoir écrasé getItemPosition(Object object) avec succès (mis à part que tout a été réglé sur POSITION_NONE)?

J'utilise la révision actuelle (10) de la bibliothèque de support.

23
UgglyNoodle

En regardant la source de FragmentStatePagerAdapter, j'ai compris exactement ce qui ne va pas. FragmentStatePagerAdapter met en cache les fragments et les états enregistrés dans ArrayLists: mFragments et mSavedState. Mais lorsque les fragments sont réordonnés, il n’existe aucun mécanisme permettant de réordonner les éléments de mFragments et mSavedState. Par conséquent, l'adaptateur fournira les fragments erronés au pageur.

J'ai classé un problème pour cela et associé une implémentation fixe (NewFragmentStatePagerAdapter.Java) au problème. Dans le correctif, j'ai ajouté une fonction getItemId() à FragmentStatePagerAdapter. (Cela reflète l'implémentation de réorganisation dans FragmentPagerAdapter.) Un tableau des éléments itemIds par position de l'adaptateur est stocké à tout moment. Ensuite, dans notifyDataSetChanged(), l'adaptateur vérifie si le tableau itemIds a été modifié. Si c'est le cas, alors mFragments et mSavedState sont réorganisés en conséquence. Vous trouverez d'autres modifications dans destroyItem(), saveState() et restoreState().

Pour utiliser cette classe, getItemPosition() et getItemId() doivent être implémentés de manière cohérente avec getItem().

35
UgglyNoodle

Pour moi travaillé une des réponses de un problème . Réponses # 20 # 21. Lien vers la solution https://Gist.github.com/ypresto/8c13cb88a0973d071a64 . La meilleure solution, fonctionne pour la mise à jour des pages et la réorganisation. Seulement dans cette solution, l’adaptateur n’a pas lancé IndexOutOfBoundsExeption lors de la destruction d’un élément (dans la méthode destroyItem), qui est un bogue connu pour d’autres solutions.

1
hovo888s

J'ai réimplémenté la solution existante dans Kotlin, de sorte qu'elle vous permette de renvoyer une String au lieu d'une long pour l'id de l'article. Vous pouvez le trouver ici ou ci-dessous:

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_"
    }
}

Et la pièce Java:

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
SUPERCILEX