2012-09-20 6 views
21

Credo che FragmentStatePagerAdapter non si comporti correttamente quando si esegue l'override di getItemPosition(Object object) allo scopo di riordinare le pagine.riordina le pagine in FragmentStatePagerAdapter utilizzando getItemPosition (oggetto Object)

Di seguito è riportato un semplice esempio. Nello stato iniziale, l'ordine delle pagine è {A, B, C}. Alla chiamata toggleState(), l'ordine delle pagine cambia in {A, C, B}. Ignorando getItemPosition(Object object), ci assicuriamo che la pagina corrente visualizzata (A, B o C) non cambi.

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

Ho riscontrato due comportamenti separati che sembrano errati.

  1. Se chiamo subito toggleState() (durante la visualizzazione di pagina A, prima di strisciare a qualsiasi altra pagina), l'applicazione si blocca.

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

    Guardando la fonte di FragmentStatePagerAdapter, questo sarebbe stato risolto dalla prima controllando la dimensione del mFragments (come in linee 113-115) prima di chiamare set() in linea 136.

  2. Se ho magnetica per pagina B, quindi viene chiamato getItem(2), viene creata la pagina C e mFragments ha ora una dimensione pari a 3 (ciò impedirà l'arresto anomalo di cui sopra in un momento). Poi torno alla pagina A, e la pagina C viene distrutta, come dovrebbe essere (dato che è a 2 pagine di distanza, e sto usando il limite predefinito della pagina fuori schermo di 1). Ora chiamo toggleState(). La pagina B è ora distrutta. Tuttavia, la pagina C NON viene ricreata! Ciò significa che quando passo il dito verso destra, ottengo una pagina vuota.

In primo luogo, sarebbe bello sapere se sono corretto e questi sono in realtà bug o se sto facendo qualcosa di sbagliato. Se sono bug, qualcuno può suggerire una soluzione alternativa (a parte il debug e la ricostruzione della libreria di supporto da solo)? Sicuramente qualcuno deve aver sostituito con successo getItemPosition(Object object) (a parte impostare tutto su POSITION_NONE)?

Sto utilizzando la revisione corrente (10) della libreria di supporto.

risposta

33

Guardando la fonte di FragmentStatePagerAdapter, ho capito esattamente cosa non va. FragmentStatePagerAdapter memorizza nella cache i frammenti e gli stati salvati in ArrayLists: mFragments e mSavedState. Ma quando i frammenti vengono riordinati, non esiste alcun meccanismo per riordinare gli elementi di mFragments e mSavedState. Pertanto, l'adattatore fornirà i frammenti errati al cercapersone.

Ho archiviato an issue per questo e ho allegato un'implementazione fissa (NewFragmentStatePagerAdapter.java) al problema. Nella correzione, ho aggiunto una funzione getItemId() a FragmentStatePagerAdapter. (Questo rispecchia l'implementazione del riordino in FragmentPagerAdapter.) Un array di itemIds per posizione dell'adattatore è memorizzato in ogni momento. Quindi, in notifyDataSetChanged(), l'adattatore controlla se l'array itemIds è stato modificato. Se lo ha, quindi mFragments e mSavedState vengono riordinati di conseguenza. Ulteriori modifiche possono essere trovate in destroyItem(), saveState() e restoreState().

Per utilizzare questa classe, getItemPosition() e getItemId() devono essere implementati in modo coerente con getItem().

+0

Ho usato l'implementazione fissa, ma un problema che sto affrontando è NO updation di frammenti prossimo in ViewPager.How posso aggiustarlo? –

+1

Penso che tu sia azzeccato, @UgglyNoodle, volevo solo far notare che se stai bene con i frammenti riordinati distrutti e ricreati, restituisci POSITION_NONE per i frammenti il ​​cui ordine è cambiato e POSTION_UNCHANGED per quei frammenti che non sono cambiati ottiene il risultato desiderato anche senza schiantarsi. –

1

Per me ha funzionato una delle risposte di an issue. Risposte # 20 # 21. Link alla soluzione https://gist.github.com/ypresto/8c13cb88a0973d071a64. La soluzione migliore, funziona per l'aggiornamento delle pagine e anche il riordino. Solo in questa soluzione Adapter non ha lanciato IndexOutOfBoundsExeption quando ha distrutto l'oggetto (nel metodo destroyItem), che è un bug noto per altre soluzioni.

0

Ho reimplementato lo existing solution in Kotlin in modo che sia possibile restituire un String anziché un long per l'ID oggetto. Si può trovare here o al di sotto:

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

E il pezzo 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);` 
    } 
} 
Problemi correlati