Как возобновить фрагмент из BackStack, если существует

151

Я учусь использовать фрагменты. У меня есть три экземпляра Fragment, которые инициализируются в верхней части класса. Я добавляю фрагмент к деятельности, как это:

Объявление и инициализация:

Fragment A = new AFragment();
Fragment B = new BFragment();
Fragment C = new CFragment();

Замена / Добавление:

FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
ft.replace(R.id.content_frame, A);
ft.addToBackStack(null);
ft.commit();

Эти фрагменты работают правильно. Каждый фрагмент прикрепляется к активности и без проблем сохраняется в заднем стеке.

Итак, когда я запускаю A, Cа затем B, стек выглядит так:

| |
|B|
|C|
|A|
___

И когда я нажимаю кнопку «назад», Bуничтожается и Cвозобновляется.

Но когда я запускаю фрагмент Aво второй раз, вместо возобновления из заднего стека, он добавляется вверху заднего стека

| |
|A|
|C|
|A|
___

Но я хочу возобновить Aи уничтожить все фрагменты поверх него (если есть). На самом деле, мне просто нравится поведение стека по умолчанию.

Как мне это сделать?

Ожидаемое: ( Aдолжно быть возобновлено и верхние фрагменты должны быть уничтожены)

| |
| |
| |
|A|
___

Изменить: (предложено A - C)

Это мой пробный код:

private void selectItem(int position) {
        Fragment problemSearch = null, problemStatistics = null;
        FragmentManager manager = getSupportFragmentManager();
        FragmentTransaction ft = manager.beginTransaction();
        String backStateName = null;
        Fragment fragmentName = null;
        boolean fragmentPopped = false;
        switch (position) {
        case 0:
            fragmentName = profile;
            break;
        case 1:
            fragmentName = submissionStatistics;
            break;
        case 2:
            fragmentName = solvedProblemLevel;
            break;
        case 3:
            fragmentName = latestSubmissions;
            break;
        case 4:
            fragmentName = CPExercise;
            break;
        case 5:
            Bundle bundle = new Bundle();
            bundle.putInt("problem_no", problemNo);
            problemSearch = new ProblemWebView();
            problemSearch.setArguments(bundle);
            fragmentName = problemSearch;
            break;
        case 6:
            fragmentName = rankList;
            break;
        case 7:
            fragmentName = liveSubmissions;
            break;
        case 8:
            Bundle bundles = new Bundle();
            bundles.putInt("problem_no", problemNo);
            problemStatistics = new ProblemStatistics();
            problemStatistics.setArguments(bundles);
            fragmentName = problemStatistics;
        default:
            break;
        }
        backStateName = fragmentName.getClass().getName();
        fragmentPopped = manager.popBackStackImmediate(backStateName, 0);
        if (!fragmentPopped) {
            ft.replace(R.id.content_frame, fragmentName);
        }
        ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
        ft.addToBackStack(backStateName);
        ft.commit();

        // I am using drawer layout
        mDrawerList.setItemChecked(position, true);
        setTitle(title[position]);
        mDrawerLayout.closeDrawer(mDrawerList);
    }

Проблема в том, что когда я запускаю, Aа затем Bнажимаю «назад», Bудаляется и Aвозобновляется. и нажатие «назад» во второй раз должно выйти из приложения. Но оно показывает пустое окно, и я должен нажать в третий раз, чтобы закрыть его.

Кроме того, когда я запускаю A, то B, потом C, потом Bснова ...

Ожидаемое:

| |
| |
|B|
|A|
___

Актуально:

| |
|B|
|B|
|A|
___

Должен ли я переопределить onBackPressed()какие-либо настройки или я что-то упустил?

Kaidul
источник

Ответы:

279

Читая документацию , есть способ извлечь задний стек на основе либо имени транзакции, либо идентификатора, предоставленного commit. Использование имени может быть проще, поскольку оно не должно требовать отслеживания числа, которое может измениться, и усиливает логику «уникального входа в стек назад».

Поскольку для каждой записи требуется только одна запись в стеке назад Fragment, задайте для имени состояния назад имя класса фрагмента (через getClass().getName()). Затем при замене Fragmentиспользуйте popBackStackImmediate()метод. Если он возвращает true, это означает, что в заднем стеке есть экземпляр фрагмента. Если нет, фактически выполните логику замены фрагмента.

private void replaceFragment (Fragment fragment){
  String backStateName = fragment.getClass().getName();

  FragmentManager manager = getSupportFragmentManager();
  boolean fragmentPopped = manager.popBackStackImmediate (backStateName, 0);

  if (!fragmentPopped){ //fragment not in back stack, create it.
    FragmentTransaction ft = manager.beginTransaction();
    ft.replace(R.id.content_frame, fragment);
    ft.addToBackStack(backStateName);
    ft.commit();
  }
}

РЕДАКТИРОВАТЬ

Проблема в том, что когда я запускаю A, а затем B, затем нажимаю кнопку «назад», B удаляется и A возобновляется. и повторное нажатие кнопки возврата должно выйти из приложения. Но он показывает пустое окно и нужно еще раз нажать, чтобы закрыть его.

Это связано с тем, что FragmentTransactionобъект добавляется в задний стек, чтобы гарантировать, что позже мы сможем вставить фрагменты сверху. Быстрое решение для этого - переопределение onBackPressed()и завершение Activity, если задний стек содержит только 1Fragment

@Override
public void onBackPressed(){
  if (getSupportFragmentManager().getBackStackEntryCount() == 1){
    finish();
  }
  else {
    super.onBackPressed();
  }
}

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

Примерно так должно быть ближе к тому, что вы хотите:

private void replaceFragment (Fragment fragment){
  String backStateName =  fragment.getClass().getName();
  String fragmentTag = backStateName;

  FragmentManager manager = getSupportFragmentManager();
  boolean fragmentPopped = manager.popBackStackImmediate (backStateName, 0);

  if (!fragmentPopped && manager.findFragmentByTag(fragmentTag) == null){ //fragment not in back stack, create it.
    FragmentTransaction ft = manager.beginTransaction();
    ft.replace(R.id.content_frame, fragment, fragmentTag);
    ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
    ft.addToBackStack(backStateName);
    ft.commit();
  } 
}

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

Реализация:

Я настоятельно рекомендую не разбирать обновленный replaceFragment()метод, как вы делали в своем коде. Вся логика содержится в этом методе, и перемещение деталей может вызвать проблемы.

Это означает, что вы должны скопировать обновленный replaceFragment()метод в ваш класс, а затем изменить

backStateName = fragmentName.getClass().getName();
fragmentPopped = manager.popBackStackImmediate(backStateName, 0);
if (!fragmentPopped) {
            ft.replace(R.id.content_frame, fragmentName);
}
ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
ft.addToBackStack(backStateName);
ft.commit();

так просто

replaceFragment (fragmentName);

РЕДАКТИРОВАТЬ № 2

Чтобы обновить ящик при изменении заднего стека, создайте метод, который принимает фрагмент и сравнивает имена классов. Если что-то совпадает, измените название и выбор. Также добавьте OnBackStackChangedListenerи вызовите метод обновления, если есть допустимый фрагмент.

Например, в Activity onCreate(), добавить

getSupportFragmentManager().addOnBackStackChangedListener(new OnBackStackChangedListener() {

  @Override
  public void onBackStackChanged() {
    Fragment f = getSupportFragmentManager().findFragmentById(R.id.content_frame);
    if (f != null){
      updateTitleAndDrawer (f);
    }

  }
});

И другой метод:

private void updateTitleAndDrawer (Fragment fragment){
  String fragClassName = fragment.getClass().getName();

  if (fragClassName.equals(A.class.getName())){
    setTitle ("A");
    //set selected item position, etc
  }
  else if (fragClassName.equals(B.class.getName())){
    setTitle ("B");
    //set selected item position, etc
  }
  else if (fragClassName.equals(C.class.getName())){
    setTitle ("C");
    //set selected item position, etc
  }
}

Теперь, когда меняется задний стек, заголовок и проверенная позиция будут отражать видимое Fragment.

А - С
источник
Спасибо за Ваш ответ. :) Работает частично. Я отредактировал свой вопрос с прогрессом. Пожалуйста, посмотрите)
Кайдул
2
@RishabhSrivastava имейте в виду, что «поп» обычно уничтожает самый верхний фрагмент. От того, какой метод вызывается первым, зависит - посмотрите на жизненный цикл фрагмента. В вашем случае я бы использовал, OnBackstackChangedListenerчтобы вы могли гарантировать, что имеете дело с правильным фрагментом.
A - C
2
@RishabhSrivastava Именно поэтому я и предложил OnBackstackChangedListener.
A - C
1
@AlexanderFarber Вы правы. Назад, когда я сделал этот ответ, о котором я не думал instanceof:)
A - C
2
Ваше решение звучит хорошо, но если у вас есть три фрагмента, нажмите на них A-B-C-Bи нажмите один раз назад, вы вернетесь назад, Aа не назад C. Это связано с тем, что popBackStackImmediateвыдается не только указанный тег, но и все вышеперечисленное. Там есть работа?
Raeglan
10

Я думаю, что этот метод может решить вашу проблему:

public static void attachFragment ( int fragmentHolderLayoutId, Fragment fragment, Context context, String tag ) {


    FragmentManager manager = ( (AppCompatActivity) context ).getSupportFragmentManager ();
    FragmentTransaction ft = manager.beginTransaction ();

    if (manager.findFragmentByTag ( tag ) == null) { // No fragment in backStack with same tag..
        ft.add ( fragmentHolderLayoutId, fragment, tag );
        ft.addToBackStack ( tag );
        ft.commit ();
    }
    else {
        ft.show ( manager.findFragmentByTag ( tag ) ).commit ();
    }
}

который был первоначально размещен в этом вопросе

erluxman
источник
Пожалуйста, дайте мне знать, если такой ответ противоречит правилам, которые я просто хотел, чтобы людям было легко увидеть в этом посте. Спасибо ..
erluxman
Я не уверен, противоречит ли это правилам (вероятно, нет), но это определенно полезно
Kathir
1
Какая польза от третьей строки? -> manager.findFragmentByTag (tag); Похоже, он ничего не делает ..
Рик ван
3

Шаг 1. Реализуйте интерфейс с вашим классом активности

public class AuthenticatedMainActivity extends Activity implements FragmentManager.OnBackStackChangedListener{

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        .............
        FragmentManager fragmentManager = getFragmentManager();           
        fragmentManager.beginTransaction().add(R.id.frame_container,fragment, "First").addToBackStack(null).commit();
    }

    private void switchFragment(Fragment fragment){            
      FragmentManager fragmentManager = getFragmentManager();
      fragmentManager.beginTransaction()
        .replace(R.id.frame_container, fragment).addToBackStack("Tag").commit();
    }

    @Override
    public void onBackStackChanged() {
    FragmentManager fragmentManager = getFragmentManager();

    System.out.println("@Class: SummaryUser : onBackStackChanged " 
            + fragmentManager.getBackStackEntryCount());

    int count = fragmentManager.getBackStackEntryCount();

    // when a fragment come from another the status will be zero
    if(count == 0){

        System.out.println("again loading user data");

        // reload the page if user saved the profile data

        if(!objPublicDelegate.checkNetworkStatus()){

            objPublicDelegate.showAlertDialog("Warning"
                    , "Please check your internet connection");

        }else {

            objLoadingDialog.show("Refreshing data..."); 

            mNetworkMaster.runUserSummaryAsync();
        }

        // IMPORTANT: remove the current fragment from stack to avoid new instance
        fragmentManager.removeOnBackStackChangedListener(this);

    }// end if
   }       
}

Шаг 2: Когда вы вызываете другой фрагмент, добавьте этот метод:

String backStateName = this.getClass().getName();

FragmentManager fragmentManager = getFragmentManager();
fragmentManager.addOnBackStackChangedListener(this); 

Fragment fragmentGraph = new GraphFragment();
Bundle bundle = new Bundle();
bundle.putString("graphTag",  view.getTag().toString());
fragmentGraph.setArguments(bundle);

fragmentManager.beginTransaction()
.replace(R.id.content_frame, fragmentGraph)
.addToBackStack(backStateName)
.commit();
Винод Джоши
источник
2

Я знаю, что уже поздно отвечать на этот вопрос, но я решил эту проблему сам и подумал, что стоит поделиться ею со всеми.

public void replaceFragment(BaseFragment fragment) {
    FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
    final FragmentManager fManager = getSupportFragmentManager();
    BaseFragment fragm = (BaseFragment) fManager.findFragmentByTag(fragment.getFragmentTag());
    transaction.setCustomAnimations(R.anim.enter_from_right, R.anim.exit_to_left, R.anim.enter_from_left, R.anim.exit_to_right);

    if (fragm == null) {  //here fragment is not available in the stack
        transaction.replace(R.id.container, fragment, fragment.getFragmentTag());
        transaction.addToBackStack(fragment.getFragmentTag());
    } else { 
        //fragment was found in the stack , now we can reuse the fragment
        // please do not add in back stack else it will add transaction in back stack
        transaction.replace(R.id.container, fragm, fragm.getFragmentTag()); 
    }
    transaction.commit();
}

И в onBackPressed ()

 @Override
public void onBackPressed() {
    if(getSupportFragmentManager().getBackStackEntryCount()>1){
        super.onBackPressed();
    }else{
        finish();
    }
}
Вивек Пратап Сингх
источник
1
@ X-Black ... BaseFragment - это базовый класс для каждого фрагмента, используемого в приложении.
Вивек Пратап Сингх
0
getFragmentManager().addOnBackStackChangedListener(new FragmentManager.OnBackStackChangedListener() {

    @Override
    public void onBackStackChanged() {

        if(getFragmentManager().getBackStackEntryCount()==0) {
            onResume();
        }    
    }
});
gushedaoren
источник
0

Более простым решением будет изменение этой линии

ft.replace(R.id.content_frame, A); в ft.add(R.id.content_frame, A);

И внутри вашего XML-макета, пожалуйста, используйте

  android:background="@color/white"
  android:clickable="true"
  android:focusable="true"

Clickable означает, что на него можно нажать указательным устройством или коснуться сенсорного устройства.

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

Хоссам Хасан
источник