android - tablayout - viewpager con fragments
reordenar las páginas en FragmentStatePagerAdapter usando getItemPosition(objeto Object) (3)
En cuanto a la fuente de FragmentStatePagerAdapter, me di cuenta exactamente de lo que está mal. El FragmentStatePagerAdapter guarda en caché los fragmentos y estados guardados en ArrayLists: mFragments
y mSavedState
. Pero cuando los fragmentos se reordenan, no hay ningún mecanismo para reordenar los elementos de mFragments
y mSavedState
. Por lo tanto, el adaptador proporcionará los fragmentos incorrectos al buscapersonas.
He presentado un problema para esto y adjunto una implementación fija (NewFragmentStatePagerAdapter.java) al problema. En la corrección, he agregado una función getItemId()
a FragmentStatePagerAdapter. (Esto refleja la implementación de la reordenación en FragmentPagerAdapter.) Se almacena una matriz de itemIds por posición de adaptador en todo momento. Luego, en notifyDataSetChanged()
, el adaptador verifica si la matriz de itemIds ha cambiado. Si es así, entonces mFragments
y mSavedState
se reordenan en consecuencia. Otras modificaciones se pueden encontrar en destroyItem()
, saveState()
y restoreState()
.
Para usar esta clase, getItemPosition()
y getItemId()
deben implementarse de manera coherente con getItem()
.
Creo que FragmentStatePagerAdapter no se comporta correctamente cuando se reemplaza getItemPosition(Object object)
con el propósito de reordenar las páginas.
A continuación se muestra un ejemplo simple. En el estado inicial, el orden de las páginas es {A, B, C}. Al llamar a toggleState()
, el orden de las páginas cambia a {A, C, B}. Al invalidar getItemPosition(Object object)
, nos aseguramos de que la página que se está viendo (A, B o C) no cambie.
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));
}
}
Me he encontrado con dos comportamientos separados que parecen incorrectos.
Si llamo inmediatamente a
toggleState()
(mientras visualizo la página A, antes de pasar a otra página), la aplicación falla.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 cuanto a la fuente de
FragmentStatePagerAdapter
, esto se solucionaría al verificar primero el tamaño demFragments
(como en las líneas 113-115) antes de llamar aset()
en la línea 136.Si paso primero a la página B, se
getItem(2)
, se crea la página C ymFragments
ahora tiene un tamaño de 3 (esto evitará que el bloqueo anterior ocurra en un momento). Luego vuelvo a la página A, y la página C se destruye, como debería ser (ya que faltan 2 páginas, y estoy usando el límite de páginas predeterminado fuera de la pantalla de 1). Ahora, llamotoggleState()
. La página B ahora está destruida. Sin embargo, la página C NO es recreada! Esto significa que cuando paso a la derecha, obtengo una página vacía.
En primer lugar, sería bueno saber si estoy en lo cierto y estos son, de hecho, errores o si estoy haciendo algo mal. Si son errores, ¿alguien puede sugerir una solución (aparte de depurar y reconstruir la biblioteca de soporte por mi cuenta)? ¿Seguramente alguien debe haber anulado getItemPosition(Object object)
éxito (aparte de configurar todo en POSITION_NONE
)?
Estoy usando la revisión actual (10) de la biblioteca de soporte.
Para mi funcionó una de las respuestas de un problema . Respuestas # 20 # 21. Enlace a la solución https://gist.github.com/ypresto/8c13cb88a0973d071a64 . La mejor solución, trabaja para actualizar páginas y también reordenar. Solo en esta solución, el adaptador no lanzó IndexOutOfBoundsExeption al destruir el elemento (en el método destroyItem), que es un error conocido para otras soluciones.
Reimplementé la https://gist.github.com/ypresto/8c13cb88a0973d071a64 en Kotlin de tal manera que le permite devolver una String
lugar de una long
para la identificación del artículo. Lo puedes encontrar here o abajo:
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_"
}
}
Y la pieza de 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);`
}
}