Android меняет порядок фрагментов внутри FragmentPagerAdapter

У меня есть ~ 20 фрагментов внутри FragmentPagerAdapter, который содержит список, из которого он выбирает фрагменты, поэтому фрагменты не воссоздаются.

private List<TitledFragment> fragments;

public SectionsPagerAdapter(FragmentManager fm) {
    super(fm);

    this.fragments = new ArrayList<TitledFragment>();
}

@Override
public Fragment getItem(int i) {
    return fragments.get(i).getFragment();
}

@Override
public int getCount() {
    return fragments.size();
}

@Override
public CharSequence getPageTitle(int position) {
   return fragments.get(position).getTitle();
}

public synchronized List<TitledFragment> getFragments() {
    return fragments;
}


public synchronized void addFragment(TitledFragment fragment) {
    fragments.add(fragment);
    notifyDataSetChanged();
}

FragmentPagerAdapter устанавливается как адаптер ViewPager, и после этого я переупорядочиваю фрагменты (перетасовка была только для теста)

Collections.shuffle(mSectionsPagerAdapter.getFragments());
mSectionsPagerAdapter.notifyDataSetChanged();

и по какой-то причине меняется только порядок заголовков, даже если он возвращает другой фрагмент для getItem, поскольку порядок другой. Почему это и как я могу обойти это?


person Ruuhkis    schedule 23.09.2012    source источник


Ответы (2)


Вам также необходимо переопределить getItemPosition(). По умолчанию эта функция всегда возвращает POSITION_UNCHANGED, поэтому при вызове notifyDataSetChanged() адаптер предполагает, что все фрагменты все еще находятся в одном и том же положении. (Он не вызывает getItem() снова, поскольку считает, что все необходимые фрагменты уже созданы.) Ваш новый getItemPosition() должен возвращать новую позицию каждого фрагмента после перетасовки.

Простой хак — всегда возвращать POSITION_NONE, тогда адаптер просто отбрасывает все существующие фрагменты и воссоздает их в правильных позициях при вызове getItem(). Конечно, это неэффективно и имеет дополнительный недостаток, заключающийся в том, что текущий просматриваемый фрагмент может измениться. Однако, поскольку могут быть некоторые ошибки в переупорядочении фрагментов - см. мой вопрос здесь - подход POSITION_NONE может быть самым безопасным методом.

person UgglyNoodle    schedule 24.09.2012

Принятый ответ является неоптимальным ответом. Возврат POSITION_NONE из int getItemPosition(Object) просто разрушает любую надежду на эффективное управление фрагментами, требуя повторного создания всех фрагментов. Он также игнорирует другую проблему. FragmentPageAdapter хранит кэшированную копию фрагмента в FragmentManager и ищет эти копии при создании экземпляра нового фрагмента. Если он находит то, что считает совпадающим фрагментом, метод public Fragment getItem(int) не вызывается и используется кэшированная копия.

Например, предположим, что страницы 0 и 1 загружены, в FragmentManager будут кэшированные фрагменты с тегами 0 и 1. Теперь страница вставляется по индексу 0 (не забудьте вызвать notifyDataSetChanged()), старый индекс 0 становится 1, а 1 становится 2 (об этом сигнализирует метод public int FragmentPageAdapter.getItemPosition(Object)). Для элемента 0 было возвращено POSITION_NONE (поскольку это новая позиция), поэтому для позиции 0 вызывается метод public Object instantiateItem(ViewGroup, int):

public Object instantiateItem(ViewGroup container, int position) {
    if (mCurTransaction == null) {
        mCurTransaction = mFragmentManager.beginTransaction();
    }

    final long itemId = getItemId(position);

    // Do we already have this fragment?
    String name = makeFragmentName(container.getId(), itemId);
    Fragment fragment = mFragmentManager.findFragmentByTag(name);
    if (fragment != null) {
        if (DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment);
        mCurTransaction.attach(fragment);
    } else {
        fragment = getItem(position);
        ...

Посмотрите, что происходит, кэшированный фрагмент найден для позиции 0, и фрагмент, который вы хотели в позиции 1, теперь находится в позиции 0, фрагмент, который вы хотели в позиции 2, теперь находится в позиции 1, а в позиции 2 вы получаете новый фрагмент, возвращенный FragmentPageAdapter.getItem(int), который является дубликатом позиции 1.

Как это решить? Я видел много предложений по SO, в том числе:

  1. Всегда возвращайте POSITION_NONE из FragmentPageAdapter.getItemPosition() https://stackoverflow.com/a/7386616/2351246 - этот ответ игнорирует эффективность и управление памятью (и в конечном итоге не будет работать без очистки кэшированных фрагментов)
  2. Отслеживание тегов фрагментов https://stackoverflow.com/a/12104399/2351246 зависит от деталей реализации, которые могут измениться .
  3. И, что хуже всего, используйте магию путем обратного проектирования реализации FragmentPageAdapter https://stackoverflow.com/a/13925130/2351246, это просто ужасно.

Нет необходимости портить управление памятью или отслеживать детали внутренней реализации FragmentPagerAdapter. Отсутствующая деталь во всех ответах заключается в том, что FragmentPagerAdapter, который хочет переупорядочить фрагменты, должен также реализовать метод public long getItemId(int position):

    @Override
    public long getItemId(int position) {
        return System.identityHashCode(fragments.get(position));
    }

Это обеспечивает идентификатор, не зависящий от позиции, который можно использовать для поиска правильного фрагмента в кеше FragmentManager, даже если он перемещает страницу.

person BitByteDog    schedule 28.09.2017