Проблемы с задним стеком фрагмента Android
У меня огромная проблема с тем, как работает backstack фрагмента android, и я был бы очень признателен за любую предложенную помощь.
Представьте, что у вас есть 3 фрагмента
[1] [2] [3]
Я хочу, чтобы пользователь мог перемещаться [1] > [2] > [3]
но на обратном пути (нажатием кнопки назад) [3] > [1]
,
Как я и предполагал, этого можно добиться, не звоня addToBackStack(..)
при создании транзакции, которая приносит фрагмент [2]
в держатель фрагмента, определенный в XML.
Реальность такова, что если я не хочу [2]
появляться снова, когда пользователь нажимает кнопку назад [3]
Я не должен звонить addToBackStack
в транзакции, которая показывает фрагмент [3]
, Это кажется совершенно нелогичным (возможно, из мира iOS).
Во всяком случае, если я делаю это таким образом, когда я иду из [1] > [2]
и нажмите обратно, я вернусь в [1]
как и ожидалось.
Если я пойду [1] > [2] > [3]
а затем нажмите обратно, я прыгаю назад к [1]
(как и ожидалось). Теперь странное поведение происходит, когда я пытаюсь прыгнуть на [2]
снова из [1]
, Прежде всего [3]
кратко отображается перед [2]
появляется в поле зрения. Если я вернусь в этот момент [3]
на дисплее, и если я нажму еще раз, приложение закроется.
Может ли кто-нибудь помочь мне понять, что здесь происходит?
А вот файл макета XML для моей основной деятельности:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical" >
<fragment
android:id="@+id/headerFragment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
class="com.fragment_test.FragmentControls" >
<!-- Preview: layout=@layout/details -->
</fragment>
<FrameLayout
android:id="@+id/detailFragment"
android:layout_width="match_parent"
android:layout_height="fill_parent"
/>
Обновление Это код, который я использую для сборки Nav Heirarchy
Fragment frag;
FragmentTransaction transaction;
//Create The first fragment [1], add it to the view, BUT Dont add the transaction to the backstack
frag = new Fragment1();
transaction = getSupportFragmentManager().beginTransaction();
transaction.replace(R.id.detailFragment, frag);
transaction.commit();
//Create the second [2] fragment, add it to the view and add the transaction that replaces the first fragment to the backstack
frag = new Fragment2();
transaction = getSupportFragmentManager().beginTransaction();
transaction.replace(R.id.detailFragment, frag);
transaction.addToBackStack(null);
transaction.commit();
//Create third fragment, Dont add this transaction to the backstack, because we dont want to go back to [2]
frag = new Fragment3();
transaction = getSupportFragmentManager().beginTransaction();
transaction.replace(R.id.detailFragment, frag);
transaction.commit();
//END OF SETUP CODE-------------------------
//NOW:
//Press back once and then issue the following code:
frag = new Fragment2();
transaction = getSupportFragmentManager().beginTransaction();
transaction.replace(R.id.detailFragment, frag);
transaction.addToBackStack(null);
transaction.commit();
//Now press back again and you end up at fragment [3] not [1]
Большое спасибо
8 ответов
Пояснение: что здесь происходит?
Если мы будем иметь в виду, что .replace()
равно .remove().add()
что мы знаем по документации:
Заменить существующий фрагмент, который был добавлен в контейнер. По сути это то же самое, что звонить
remove(Fragment)
для всех добавленных в настоящее время фрагментов, которые были добавлены с тем жеcontainerViewId
а потомadd(int, Fragment, String)
с теми же аргументами, приведенными здесь.
тогда происходит следующее (я добавляю числа во фрагмент, чтобы сделать его более понятным):
// transaction.replace(R.id.detailFragment, frag1);
Transaction.remove(null).add(frag1) // frag1 on view
// transaction.replace(R.id.detailFragment, frag2).addToBackStack(null);
Transaction.remove(frag1).add(frag2).addToBackStack(null) // frag2 on view
// transaction.replace(R.id.detailFragment, frag3);
Transaction.remove(frag2).add(frag3) // frag3 on view
(здесь все ложные вещи начинают происходить)
Помни что .addToBackStack()
сохраняет только транзакцию, а не фрагмент как сам! Итак, теперь у нас есть frag3
на макете:
< press back button >
// System pops the back stack and find the following saved back entry to be reversed:
// [Transaction.remove(frag1).add(frag2)]
// so the system makes that transaction backward!!!
// tries to remove frag2 (is not there, so it ignores) and re-add(frag1)
// make notice that system doesn't realise that there's a frag3 and does nothing with it
// so it still there attached to view
Transaction.remove(null).add(frag1) //frag1, frag3 on view (OVERLAPPING)
// transaction.replace(R.id.detailFragment, frag2).addToBackStack(null);
Transaction.remove(frag3).add(frag2).addToBackStack(null) //frag2 on view
< press back button >
// system makes saved transaction backward
Transaction.remove(frag2).add(frag3) //frag3 on view
< press back button >
// no more entries in BackStack
< app exits >
Возможное решение
Рассмотреть возможность реализации FragmentManager.BackStackChangedListener
следить за изменениями в заднем стеке и применять вашу логику в onBackStackChanged()
Methode:
- Отслеживать количество транзакций;
- Проверить конкретную транзакцию по имени
FragmentTransaction.addToBackStack(String name);
- И т.п.
Правильно!!! после долгих потягиваний волос я наконец-то понял, как сделать эту работу правильно.
Кажется, что фрагмент [3] не удаляется из вида при нажатии назад, поэтому вы должны сделать это вручную!
Прежде всего, не используйте replace(), а вместо этого используйте remove и добавьте отдельно. Кажется, что replace() не работает должным образом.
Следующая часть этого - переопределение метода onKeyDown и удаление текущего фрагмента при каждом нажатии кнопки "Назад".
@Override
public boolean onKeyDown(int keyCode, KeyEvent event)
{
if (keyCode == KeyEvent.KEYCODE_BACK)
{
if (getSupportFragmentManager().getBackStackEntryCount() == 0)
{
this.finish();
return false;
}
else
{
getSupportFragmentManager().popBackStack();
removeCurrentFragment();
return false;
}
}
return super.onKeyDown(keyCode, event);
}
public void removeCurrentFragment()
{
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
Fragment currentFrag = getSupportFragmentManager().findFragmentById(R.id.detailFragment);
String fragName = "NONE";
if (currentFrag!=null)
fragName = currentFrag.getClass().getSimpleName();
if (currentFrag != null)
transaction.remove(currentFrag);
transaction.commit();
}
Надеюсь это поможет!
Прежде всего, спасибо @Arvis за откровение.
Я предпочитаю другое решение принятого ответа здесь для этой проблемы. Я не люблю возиться с переопределением поведения назад больше, чем это абсолютно необходимо, и когда я попытался самостоятельно добавлять и удалять фрагменты без всплывающего стека по умолчанию, когда нажата кнопка "Назад", я обнаружил себя в аду фрагментов:) Если вы. добавьте f2 вместо f1, когда вы удалите его. f1 не вызовет ни один из методов обратного вызова, таких как onResume, onStart и т. д., и это может быть очень неудачно.
Во всяком случае, вот как я это делаю:
В настоящее время на дисплее представлен только фрагмент f1.
f1 -> f2
Fragment2 f2 = new Fragment2();
this.getActivity().getSupportFragmentManager().beginTransaction().replace(R.id.main_content,f2).addToBackStack(null).commit();
ничего необычного здесь. Чем во фрагменте f2 этот код переводит вас во фрагмент f3.
f2 -> f3
Fragment3 f3 = new Fragment3();
getActivity().getSupportFragmentManager().popBackStack();
getActivity().getSupportFragmentManager().beginTransaction().replace(R.id.main_content, f3).addToBackStack(null).commit();
Я не уверен, читая документы, если это должно сработать, этот метод транзакции всплывающего окна называется асинхронным, и, возможно, лучшим способом было бы вызвать popBackStackImmediate(). Но насколько я могу судить по моим устройствам, это работает безупречно.
Указанная альтернатива будет:
final FragmentActivity activity = getActivity();
activity.getSupportFragmentManager().popBackStackImmediate();
activity.getSupportFragmentManager().beginTransaction().replace(R.id.main_content, f3).addToBackStack(null).commit();
Здесь на самом деле будет краткий переход к f1, прежде чем перейти к f3, так что небольшой сбой там.
Это на самом деле все, что вам нужно сделать, нет необходимости переопределять поведение стека...
Я знаю, что это старый вопрос, но у меня возникла та же проблема, и я решил ее так:
Сначала добавьте Fragment1 в BackStack с именем (например, "Frag1"):
frag = new Fragment1();
transaction = getSupportFragmentManager().beginTransaction();
transaction.replace(R.id.detailFragment, frag);
transaction.addToBackStack("Frag1");
transaction.commit();
И затем, всякий раз, когда вы захотите вернуться к Fragment1 (даже после добавления 10 фрагментов над ним), просто вызовите popBackStackImmediate с именем:
getSupportFragmentManager().popBackStackImmediate("Frag1", 0);
Надеюсь, это поможет кому-то:)
После ответа @Arvis я решил копать еще глубже, и я написал техническую статью об этом здесь: http://www.andreabaccega.com/blog/2015/08/16/how-to-avoid-fragments-overlapping-due-to-backstack-nightmare-in-android/ - за к backstack-кошмар-в-андроид /
Для ленивых разработчиков вокруг. Мое решение состоит в том, чтобы всегда добавлять транзакции в backstack и выполнять дополнительные FragmentManager.popBackStackImmediate()
при необходимости (автоматически).
В коде очень мало строк кода, и в моем примере я хотел перейти от C к A, не переходя назад к "B", если пользователь не углубился в backstack (например, из C переходит к D).
Следовательно, прикрепленный код будет работать следующим образом: A -> B -> C (назад) -> A & A -> B -> C -> D (назад) -> C (назад) -> B (назад) -> A
где
fm.beginTransaction().replace(R.id.content, new CFragment()).commit()
были выданы от "B" до "C", как в вопросе.
Хорошо, вот код:)
public static void performNoBackStackTransaction(FragmentManager fragmentManager, String tag, Fragment fragment) {
final int newBackStackLength = fragmentManager.getBackStackEntryCount() +1;
fragmentManager.beginTransaction()
.replace(R.id.content, fragment, tag)
.addToBackStack(tag)
.commit();
fragmentManager.addOnBackStackChangedListener(new FragmentManager.OnBackStackChangedListener() {
@Override
public void onBackStackChanged() {
int nowCount = fragmentManager.getBackStackEntryCount();
if (newBackStackLength != nowCount) {
// we don't really care if going back or forward. we already performed the logic here.
fragmentManager.removeOnBackStackChangedListener(this);
if ( newBackStackLength > nowCount ) { // user pressed back
fragmentManager.popBackStackImmediate();
}
}
}
});
}
Если вы боретесь с addToBackStack() и popBackStack (), тогда просто используйте
FragmentTransaction ft =getSupportFragmentManager().beginTransaction();
ft.replace(R.id.content_frame, new HomeFragment(), "Home");
ft.commit();`
В своей деятельности в OnBackPressed() найдите фермент по тегу, а затем сделайте свое дело
Fragment home = getSupportFragmentManager().findFragmentByTag("Home");
if (home instanceof HomeFragment && home.isVisible()) {
// do you stuff
}
Для получения дополнительной информации https://github.com/DattaHujare/NavigationDrawer Я никогда не использую addToBackStack() для обработки фрагмента.
executePendingTransactions()
, commitNow()
не работал (
Работал в андроиде (джетпак).
private final FragmentManager fragmentManager = getSupportFragmentManager();
public void removeFragment(FragmentTag tag) {
Fragment fragmentRemove = fragmentManager.findFragmentByTag(tag.toString());
if (fragmentRemove != null) {
fragmentManager.beginTransaction()
.remove(fragmentRemove)
.commit();
// fix by @Ogbe
fragmentManager.popBackStackImmediate(tag.toString(),
FragmentManager.POP_BACK_STACK_INCLUSIVE);
}
}
Я думаю, когда я читаю вашу историю, [3] также находится на заднем плане. Это объясняет, почему вы видите, что он мигает.
Решение было бы никогда не устанавливать [3] в стеке.
У меня была похожая проблема, где у меня было 3 последовательных фрагмента в одном Activity
[M1.F0]->[M1.F1]->[M1.F2] с последующим вызовом нового Activity
[M2]. Если пользователь нажал кнопку в [M2], я хотел бы вернуться к [M1, F1] вместо [M1, F2], что и было тем, что уже было сделано при обратном нажатии.
Для этого я удаляю [M1, F2], вызываю show на [M1, F1], фиксирую транзакцию, а затем добавляю [M1, F2] обратно, вызывая ее с помощью hide. Это убрало лишнюю спину, которая в противном случае осталась бы позади
// Remove [M1.F2] to avoid having an extra entry on back press when returning from M2
final FragmentTransaction ftA = fm.beginTransaction();
ftA.remove(M1F2Fragment);
ftA.show(M1F1Fragment);
ftA.commit();
final FragmentTransaction ftB = fm.beginTransaction();
ftB.hide(M1F2Fragment);
ftB.commit();
Привет После выполнения этого кода: я не могу увидеть значение Fragment2 при нажатии клавиши возврата. Мой код:
FragmentTransaction ft = fm.beginTransaction();
ft.add(R.id.frame, f1);
ft.remove(f1);
ft.add(R.id.frame, f2);
ft.addToBackStack(null);
ft.remove(f2);
ft.add(R.id.frame, f3);
ft.commit();
@Override
public boolean onKeyDown(int keyCode, KeyEvent event){
if(keyCode == KeyEvent.KEYCODE_BACK){
Fragment currentFrag = getFragmentManager().findFragmentById(R.id.frame);
FragmentTransaction transaction = getFragmentManager().beginTransaction();
if(currentFrag != null){
String name = currentFrag.getClass().getName();
}
if(getFragmentManager().getBackStackEntryCount() == 0){
}
else{
getFragmentManager().popBackStack();
removeCurrentFragment();
}
}
return super.onKeyDown(keyCode, event);
}
public void removeCurrentFragment()
{
FragmentTransaction transaction = getFragmentManager().beginTransaction();
Fragment currentFrag = getFragmentManager().findFragmentById(R.id.frame);
if(currentFrag != null){
transaction.remove(currentFrag);
}
transaction.commit();
}