Как раздавить фрагменты на заднем стеке, которые совпадают по тегу фрагмента?

Макет моего приложения для планшетов Android состоит из списка элементов и подробного представления. Когда элемент списка выбран, связанное с ним содержимое отображается в представлении сведений.

+--------+-------------+ 
| Item 1 |             |
+--------+    Item     |
| Item 2 |   details   |
+--------+             |
| Item 3 |             |
+--------+-------------+

Представление сведений представляет собой Fragment, который программно расширяется до заполнителя FrameLayout:

<FrameLayout
    android:id="@+id/detail_fragment_placeholder"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

Вот операция Fragment:

getSupportFragmentManager()
    .beginTransaction()
    .replace(containerViewId, fragment, fragmentTag)
    .addToBackStack(backStackStateName)
    .commit();

Несколько экземпляров [Dx] из DetailsFragment добавляются в стопку, когда пользователь выбирает один элемент за другим.

                [D3]
        [D2]    [D2]
[D1] -> [D1] -> [D1]

Таким образом, пользователю необходимо несколько раз нажать кнопку НАЗАД, чтобы извлечь экземпляры из стека и очистить представление сведений.

Как я могу заменить существующий экземпляр [Dx] из DetailsFragment в бэкстеке, если fragmentTag из существующего fragment совпадает с fragmentTag из нового fragment?

[D1] -> [D2] -> [D3]

person JJD    schedule 22.01.2016    source источник
comment
Вы хотите заменить только верхний фрагмент, если он имеет такой же тег, или любой фрагмент с таким же тегом в стеке?   -  person Michael    schedule 10.02.2016
comment
@Michael Если есть группа фрагментов с одинаковым тегом, то есть. того же типа поверх стека, я хочу уменьшить/заменить их одним передаваемым фрагментом.   -  person JJD    schedule 11.02.2016
comment
Не существует простого способа добиться этого. Проверьте этот ответ stackoverflow.com/a/22125826/1320616   -  person Ankit Aggarwal    schedule 11.02.2016
comment
попробуйте найти текущий фрагмент сведений, сначала удалите его, а затем добавьте новый фрагмент сведений в тот же контейнер.   -  person Veaceslav Gaidarji    schedule 11.02.2016


Ответы (3)


Я не уверен, что правильно понял ваш вопрос. Если вы хотите заменить несколько фрагментов с некоторым тегом из верхней части стопки одним фрагментом с тем же тегом, вы можете использовать следующий подход.

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

  public void addFragment(final int containerViewId, final Fragment fragment,
      final String backStackName, final boolean replace) {
    final FragmentManager fragmentManager = getSupportFragmentManager();

    if (replace) {
      while (fragmentManager.getBackStackEntryCount() > 0) {
        final int last = fragmentManager.getBackStackEntryCount() - 1;
        final FragmentManager.BackStackEntry entry = 
            fragmentManager.getBackStackEntryAt(last);
        if (!TextUtils.equals(entry.getName(), backStackName)) {
          break;
        }

        fragmentManager.popBackStackImmediate();
      }
    }

    fragmentManager
        .beginTransaction()
        .replace(containerViewId, fragment)
        .addToBackStack(backStackName)
        .commit();
    fragmentManager.executePendingTransactions();
  }

Теперь, если вы сделаете следующие вызовы, ваш backstack будет содержать только fragment1 и fragment4.

addFragment(R.id.container, fragment1, "D2", false);
addFragment(R.id.container, fragment2, "D1", false);
addFragment(R.id.container, fragment3, "D1", false);
addFragment(R.id.container, fragment4, "D1", true);

ОБНОВЛЕНИЕ:

В данном конкретном случае было достаточно следующего кода:

getSupportFragmentManager().popBackStack(
    backStackStateName, FragmentManager.POP_BACK_STACK_INCLUSIVE);
getSupportFragmentManager()
    .beginTransaction()
    .replace(containerViewId, fragment, fragmentTag)
    .addToBackStack(backStackStateName)
    .commit();

https://github.com/tuxmobil/CampFahrplan/pull/148

person Michael    schedule 11.02.2016
comment
Спасибо за подход. Однако у меня это не работает: нажмите на элемент 1 - отображается деталь 1, нажмите на элемент 2 - подробные представления пусты. Обратите внимание, что вы можете протестируйте патч самостоятельно. - Кроме того, почему вы заканчиваете с executePendingTransactions()? - person JJD; 12.02.2016
comment
Пример кода из ответа работает нормально, но вы просто использовали его неправильно. Вызовите sidePane.setVisibility(View.VISIBLE); после извлечения фрагментов из стопки, непосредственно перед началом транзакции. executePendingTransactions() необходимо, чтобы избежать условий гонки при извлечении и отправке фрагментов. Мы выталкиваем фрагменты синхронно, поэтому нам нужно и выталкивать их синхронно. - person Michael; 13.02.2016
comment
Интересный. Я также выпустил решение Daniel Nugent 2. работает, если я добавлю sidePane.setVisibility(View.VISIBLE);. Есть ли конкретная причина, по которой вы предлагаете переключать видимость после извлечения фрагментов/перед началом транзакции? Это также сработало, когда я вызываю его после завершения всех транзакций. - person JJD; 13.02.2016
comment
Вы можете показать представление после совершения транзакции. Единственное, что необходимо, это показать его после извлечения фрагментов. Решение Даниэля не будет работать для фрагментов с разными тегами. И он содержит состояние гонки, о котором я упоминал в предыдущем комментарии. Поэтому при его использовании вы можете получить несколько фрагментов деталей в стеке. - person Michael; 13.02.2016
comment
Отлично. Я спросил, потому что проще выделить оставшиеся вызовы в отдельный метод, когда я могу добавить sidePane.setVisibility(View.VISIBLE); Можете ли вы указать, почему представление не отображается автоматически, когда происходит замена? - person JJD; 13.02.2016
comment
Вы скрываете вид в onBackStackChanged(). Фрагменты извлекаются синхронно, поэтому этот метод вызывается и скрывает только что показанное представление. - person Michael; 13.02.2016
comment
Отличная находка (большую часть кода я не писал). Так что я могу переместить sidePane.setVisibility(View.VISIBLE); в onBackStackChanged() - только что проверил - вроде работает. - person JJD; 13.02.2016
comment
Немного проанализировав ваш код, я могу предположить, что вам не нужна вся эта сложность, чтобы выталкивать фрагменты деталей из стека. Просто используйте отдельное имя обратного стека для всех фрагментов деталей и выполните popBackStack(FragmentStack.DETAIL, FragmentManager.POP_BACK_STACK_INCLUSIVE);. В этом случае executePendingTransactions() не требуется. - person Michael; 13.02.2016
comment
Упрощенный код еще лучше. Если хотите, вы можете напрямую предложить pull request против tuxmobil/master для решения связанная проблема. Поскольку вы придумали решение, это было бы самым справедливым. - person JJD; 13.02.2016
comment
Мне нравится давать флаг ответа, хотя окончательное решение кажется намного короче. Благодарю вас! Обратите внимание, что tuxmobil отвечает за слияние запросов на вытягивание — это может занять некоторое время. - person JJD; 14.02.2016
comment
Я добавил окончательное решение к ответу. - person Michael; 14.02.2016

Вы можете использовать тег и обрабатывать его в onBackPressed(), но я думаю, что было бы более чистым решением обрабатывать его при построении заднего стека. Выборочно добавляйте в задний стек для каждой FragmentTransaction и добавляйте в задний стек только в том случае, если это первый экземпляр DetailsFragment.

Вот простой пример, который предотвращает добавление любого заданного фрагмента в задний стек дважды подряд:

public void replaceFragment(Fragment frag) {
    FragmentManager fm = getSupportFragmentManager();

    if (fm != null){
        FragmentTransaction t = fm.beginTransaction();
        //you could also use containerViewId in place of R.id.detail_fragment_placeholder
        Fragment currentFrag = fm.findFragmentById(R.id.detail_fragment_placeholder);
        if (currentFrag != null && currentFrag.getClass().equals(frag.getClass())) {
            t.replace(R.id.detail_fragment_placeholder, frag).commit();
        } else {
            t.replace(R.id.detail_fragment_placeholder, frag).addToBackStack(null).commit();
        }
    }
}
person Daniel Nugent    schedule 22.01.2016
comment
Соответствует ли ваш content_frame моему detail_fragment_placeholder? Если да, обновите фрагмент кода, чтобы его было легче понять. Означает ли ваше решение, что один или несколько экземпляров класса DetailFragment представлены как одна запись обратного стека, как в n:1? - person JJD; 23.01.2016
comment
@jjd да, content_frame соответствует вашему идентификатору detail_fragment_placeholder, и вы можете просто использовать containerViewId, как сейчас. Я обновлю ответ! Что касается того, как это работает, он добавляет только первый фрагмент того же типа в задний стек, поэтому теперь независимо от того, на сколько уровней вы переходите с DetailsFragments, если вы нажмете назад, вы вернетесь на верхний уровень, прежде чем перейти к DetailsFragment для первый раз. - person Daniel Nugent; 24.01.2016
comment
Ни один из обоих подходов не делает именно то, что я хочу. Ваш 1-й подход — щелкните элемент 1 — отображается деталь 1, щелкните элемент 2 — отображается деталь 2, нажмите НАЗАД — деталь 2 все еще отображается, щелкните НАЗАД - приложение переходит в фоновый режим. --- Ваш второй подход: щелкните элемент 1 - отображается деталь 1, щелкните элемент 2 - подробные представления пусты. - person JJD; 25.01.2016
comment
@JJD, это странно, у меня хорошо работает первый подход. Я не тестировал второй подход, я посмотрю, смогу ли я заставить его работать, когда у меня будет время. - person Daniel Nugent; 25.01.2016
comment
Вы можете протестировать его здесь сами. Выполнить ./gradlew clean assembleCcc32c3Debug. - person JJD; 26.01.2016

Просто найдите фрагмент на заднем стеке и замените его:

Fragment fragment = getSupportFragmentManager().findFragmentByTag("your tag");
FragmentTransaction transaction = fm.beginTransaction();
transaction.replace(R.id.fragment_container, fragment, fragment.getClass().getName());
transaction.addToBackStack("your tag");
transaction.commit();

И в событии OnClick проверьте, не соответствует ли позиция элемента уже отображаемому текущему элементу. Если это так, ничего не делайте.

person gilgil28    schedule 24.01.2016
comment
Вы ответили тем же фрагментом кода, который вы можете найти в моем вопросе. Более того, я не нажимаю один и тот же элемент, но каждый элемент связан с экземпляром класса DetailsFragment. - person JJD; 24.01.2016
comment
Возможно я не понял вашего вопроса. Была ли ваша проблема не в том, чтобы извлечь определенный фрагмент из стопки? - person gilgil28; 24.01.2016
comment
Проблема в том, что пользователю нужно несколько раз нажать кнопку BACK. - person JJD; 24.01.2016
comment
как насчет использования fragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE); - person gilgil28; 24.01.2016
comment
Это вытолкнет все записи обратного стека. Я просто хочу извлекать экземпляры класса DetailsFragment. - person JJD; 24.01.2016
comment
ну, это противоречило бы цели структуры данных стека. обходным путем будет извлекать каждую транзакцию из стека и добавлять обратно те, которые не являются экземплярами DetailsFragment - person gilgil28; 24.01.2016
comment
Отчасти правда, но в моем случае эти экземпляры являются самыми популярными. - Из интереса: как бы вы получили доступ к всплывающим записям, чтобы добавить их обратно? - person JJD; 24.01.2016
comment
поиск их с помощью findFragmentByTag, а затем popBackStack - person gilgil28; 24.01.2016
comment
Использование findFragmentByTag потребует отслеживания всех типов тегов, которые можно добавить в задний стек. На самом деле вместо подробного представления показаны и другие экземпляры, кроме DetailsFragment экземпляров. - person JJD; 25.01.2016