2012-09-20 13 views
21

Wierzę, że FragmentStatePagerAdapter nie zachowuje się poprawnie, przesuwając getItemPosition(Object object) w celu zmiany kolejności stron.Zmienianie kolejności stron w FragmentStatePagerAdapter przy użyciu getItemPosition (obiektu obiektu)

Poniżej znajduje się prosty przykład. W stanie początkowym kolejność stron to {A, B, C}. Po wywołaniu toggleState() kolejność stron zmienia się na {A, C, B}. Przesłaniając getItemPosition(Object object), zapewniamy, że wyświetlana strona (A, B lub C) nie ulegnie zmianie.

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

Napotkałem dwa osobne zachowania, które wydają się nieprawidłowe.

  1. Gdybym od razu zadzwonić toggleState() (podczas oglądania strona A, przed przesuwając do jakiejkolwiek innej strony), awarii aplikacji.

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

    Patrząc na źródło FragmentStatePagerAdapter ta będzie ustalona przez pierwsze sprawdzenie rozmiaru mFragments (jak w wierszach 113-115) przed wywołaniem set() w linii 136.

  2. Gdybym pierwszy swipe do strona B, następnie getItem(2) jest wywoływana, strona C jest tworzona, a mFragments ma teraz rozmiar 3 (to zapobiegnie zaistnieniu awarii powyżej). Następnie przeciągam z powrotem na stronę A, a strona C jest zniszczona, tak jak powinna być (ponieważ jest oddalona o 2 strony i używam domyślnego limitu strony 1). Teraz nazywam się toggleState(). Strona B jest teraz zniszczona. Jednak strona C NIE jest odtwarzana! Oznacza to, że gdy przesuniemy teraz w prawo, otrzymuję pustą stronę.

Po pierwsze, byłoby miło wiedzieć, czy mam rację, a są to w rzeczywistości błędy lub czy robię coś nie tak. Jeśli są to błędy, czy ktoś może zaproponować obejście (inne niż debugowanie i przebudowywanie biblioteki wsparcia osobiście)? Z pewnością ktoś musiał z powodzeniem zastąpić getItemPosition(Object object) (oprócz ustawienia wszystkiego na POSITION_NONE)?

Używam aktualnej wersji (10) biblioteki pomocy technicznej.

Odpowiedz

33

Patrząc na źródło FragmentStatePagerAdapter, odkryłem dokładnie, co się dzieje. FragmentStatePagerAdapter buforuje fragmenty i zapisane stany w ArrayLists: mFragments i mSavedState. Ale gdy fragmenty zostaną zmienione, nie ma mechanizmu do uporządkowania elementów mFragments i mSavedState. Dlatego adapter dostarczy niewłaściwe fragmenty do pagera.

Złożyłem do tego an issue i dołączyłem poprawkę do rozwiązania (NewFragmentStatePagerAdapter.java). W poprawce dodałem funkcję getItemId() do FragmentStatePagerAdapter. (Odzwierciedla to zmianę kolejności implementacji w FragmentPagerAdapter.) Tablica pozycji itemIds według pozycji adaptera jest przechowywana przez cały czas. Następnie, w notifyDataSetChanged(), adapter sprawdza, czy tablica itemIds uległa zmianie. Jeśli tak, to odpowiednio zmieniono kolejność mFragments i mSavedState. Dalsze modyfikacje można znaleźć w destroyItem(), saveState() i restoreState().

Aby użyć tej klasy, getItemPosition() i getItemId() muszą być implementowane zgodnie z getItem().

+0

użyłem swoją stałą realizację, jednak jeden problem, że jestem stoi to NIE updation fragmentów sąsiadujących w ViewPager.How mogę to naprawić? –

+1

Wydaje mi się, że jesteś na miejscu, @UgglyNoodle, chciałem tylko wskazać, że jeśli wszystko jest w porządku, gdy fragmenty zostaną zamienione i odtworzone, zwracana jest pozycja POSITION_NONE dla fragmentów, których kolejność się zmieniła, i POSTION_UNCHANGED dla fragmentów, które się nie zmieniły. osiąga również pożądany wynik bez awarii. –

1

Dla mnie działała jedna z odpowiedzi an issue. Odpowiedzi # 20 # 21. Link do rozwiązania https://gist.github.com/ypresto/8c13cb88a0973d071a64. Najlepsze rozwiązanie, działa na aktualizowanie stron, a także na zmianę kolejności. Tylko w tym rozwiązaniu Adapter nie rzucił IndexOutOfBoundsExeption podczas niszczenia elementu (w metodzie destroyItem), co jest znanym błędem dla innych rozwiązań.

0

Ponownie wdrożyłem existing solution w Kotlin, dzięki czemu można zwrócić String zamiast long dla identyfikatora przedmiotu. Można go znaleźć here lub poniżej:

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

a kawałek 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);` 
    } 
} 
Powiązane problemy