Отдельный задний стек для каждой вкладки в Android-браузере BottomNavigationView с использованием фрагментов

Я реализую BottomNavigationView для навигации в приложении для Android. Я использую Fragments для установки содержимого для каждой вкладки.

Я знаю, как настроить один фрагмент для каждой вкладки, а затем переключать фрагменты при нажатии на вкладку. Но как я могу создать отдельный стек для каждой вкладки? Вот код для установки одного фрагмента:

Fragment selectedFragment = ItemsFragment.newInstance();
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
transaction.replace(R.id.content, selectedFragment);
transaction.commit();

Например, Fragment A и B будут находиться под вкладками 1 и Fragment C и D под вкладкой 2. Когда приложение запущено, отображается фрагмент A и выбран вкладка 1. Затем Fragment A может быть заменен фрагментом B. Когда выбрана вкладка 2 Фрагмент C должен отображаться. Если выбрана вкладка 1 Fragment, B должен снова отображаться. На этом этапе можно будет использовать кнопку "Назад", чтобы отобразить фрагмент A.

И вот код для настройки следующего Fragment на той же вкладке:

Fragment selectedFragment = ItemsFragment.newInstance();
FragmentTransaction ft = getFragmentManager().beginTransaction();
ft.replace(R.id.content, selectedFragment);
ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
ft.addToBackStack(null);
ft.commit();

Ответ 1

Наконец, я нашел решение, оно было вдохновлено предыдущим ответом на StackOverflow: Отдельный задний стек для каждой вкладки на Android с помощью фрагментов
Я только заменил TabHost на BottomNavigationView и вот код:
Основная деятельность

public class MainActivity extends AppCompatActivity {

private HashMap<String, Stack<Fragment>> mStacks;
public static final String TAB_HOME  = "tab_home";
public static final String TAB_DASHBOARD  = "tab_dashboard";
public static final String TAB_NOTIFICATIONS  = "tab_notifications";

private String mCurrentTab;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    BottomNavigationView navigation = (BottomNavigationView) findViewById(R.id.navigation);
    navigation.setOnNavigationItemSelectedListener(mOnNavigationItemSelectedListener);

    mStacks = new HashMap<String, Stack<Fragment>>();
    mStacks.put(TAB_HOME, new Stack<Fragment>());
    mStacks.put(TAB_DASHBOARD, new Stack<Fragment>());
    mStacks.put(TAB_NOTIFICATIONS, new Stack<Fragment>());

    navigation.setSelectedItemId(R.id.navigation_home);
}

private BottomNavigationView.OnNavigationItemSelectedListener mOnNavigationItemSelectedListener
        = new BottomNavigationView.OnNavigationItemSelectedListener() {

    @Override
    public boolean onNavigationItemSelected(@NonNull MenuItem item) {
        switch (item.getItemId()) {
            case R.id.navigation_home:
                selectedTab(TAB_HOME);
                return true;
            case R.id.navigation_dashboard:
                selectedTab(TAB_DASHBOARD);
                return true;
            case R.id.navigation_notifications:
                selectedTab(TAB_NOTIFICATIONS);
                return true;
        }
        return false;
    }

};

private void gotoFragment(Fragment selectedFragment)
{
    FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
    fragmentTransaction.replace(R.id.content, selectedFragment);
    fragmentTransaction.commit();
}

private void selectedTab(String tabId)
{
    mCurrentTab = tabId;

    if(mStacks.get(tabId).size() == 0){
      /*
       *    First time this tab is selected. So add first fragment of that tab.
       *    Dont need animation, so that argument is false.
       *    We are adding a new fragment which is not present in stack. So add to stack is true.
       */
        if(tabId.equals(TAB_HOME)){
            pushFragments(tabId, new HomeFragment(),true);
        }else if(tabId.equals(TAB_DASHBOARD)){
            pushFragments(tabId, new DashboardFragment(),true);
        }else if(tabId.equals(TAB_NOTIFICATIONS)){
            pushFragments(tabId, new NotificationsFragment(),true);
        }
    }else {
      /*
       *    We are switching tabs, and target tab is already has atleast one fragment.
       *    No need of animation, no need of stack pushing. Just show the target fragment
       */
        pushFragments(tabId, mStacks.get(tabId).lastElement(),false);
    }
}

public void pushFragments(String tag, Fragment fragment, boolean shouldAdd){
    if(shouldAdd)
        mStacks.get(tag).push(fragment);
    FragmentManager manager = getSupportFragmentManager();
    FragmentTransaction ft = manager.beginTransaction();
    ft.replace(R.id.content, fragment);
    ft.commit();
}

public void popFragments(){
  /*
   *    Select the second last fragment in current tab stack..
   *    which will be shown after the fragment transaction given below
   */
    Fragment fragment = mStacks.get(mCurrentTab).elementAt(mStacks.get(mCurrentTab).size() - 2);

  /*pop current fragment from stack.. */
    mStacks.get(mCurrentTab).pop();

  /* We have the target fragment in hand.. Just show it.. Show a standard navigation animation*/
    FragmentManager manager = getSupportFragmentManager();
    FragmentTransaction ft = manager.beginTransaction();
    ft.replace(R.id.content, fragment);
    ft.commit();
}

@Override
public void onBackPressed() {
    if(mStacks.get(mCurrentTab).size() == 1){
        // We are already showing first fragment of current tab, so when back pressed, we will finish this activity..
        finish();
        return;
    }

    /* Goto previous fragment in navigation stack of this tab */
    popFragments();
}

}

Пример домашнего фрагмента

public class HomeFragment extends Fragment {
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
    View view = inflater.inflate(R.layout.fragment_home, container, false);
    Button gotoNextFragment = (Button) view.findViewById(R.id.gotoHome2);

    gotoNextFragment.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            ((MainActivity)getActivity()).pushFragments(MainActivity.TAB_HOME, new Home2Fragment(),true);
        }
    });
    return view;
}

}

Ответ 2

Стоит отметить, что описанное вами поведение противоречит рекомендациям Google. https://material.io/guidelines/components/bottom-navigation.html#bottom-navigation-behavior

Навигация через нижнюю навигационную панель должен reset состояние задачи.

Другими словами, если Fragment A и Fragment B "inside" Tab 1 в порядке, но если пользователь открывает Fragment B, щелкает Tab 2, а затем снова нажимает Tab 1, они должны видеть фрагмент A.

Ответ 3

Это поведение поддерживается новым компонентом архитектуры навигации (https://developer.android.com/topic/libraries/architecture/navigation/).

По сути, можно использовать NavHostFragment, который является фрагментом, который управляет его собственным задним стеком:

Каждый NavHostFragment имеет NavController, который определяет допустимую навигацию в хосте навигации. Это включает в себя граф навигации, а также состояние навигации, такое как текущее местоположение и задний стек, которые будут сохранены и восстановлены вместе с самим NavHostFragment. https://developer.android.com/reference/androidx/navigation/fragment/NavHostFragment

Вот пример: https://github.com/deisold/navigation


Редактировать: Оказывается, Компонент Навигационной Архитектуры не поддерживает отдельные задние стеки в любом случае, как отмечают комментаторы. Но, как упомянул @r4jiv007, они работают над этим и тем временем предложили "официальный взлом": https://github.com/googlesamples/android-architecture-components/tree/master/NavigationAdvancedSample

Ответ 4

Предположим, у вас есть 5 (A, B, C, D, E) пункт меню BottomNavigationView, затем в Activity создайте 5 FrameLayout (frmlytA, frmlytB, frmlytC, frmlytD, frmlytE) в параллельном порядке в качестве контейнера для каждого из этих элементов меню. При нажатии пункта меню BottomNavigation A затем скрыть все остальные FrameLayouts (Visibility = GONE) и просто показать (Visibility = VISIBLE) FrameLayout 'frmlytA', в котором будет размещен FragmentA и над этим контейнером, выполнять дальнейшие транзакции, такие как (FragmentA → FragmentX) → Фрагмент Y). И затем, если пользователь щелкает пункт B BottomNavigation, затем просто скрыть этот (frmlytA) контейнер и показать "frmlytB". Затем, если пользователь снова нажимает на пункт меню A, затем показывает "frmlytA", он должен сохранить прежнее состояние. Таким образом, вы можете переключаться между контейнером FrameLayouts и поддерживать задний стек каждого контейнера.

Ответ 5

Вместо использования метода замены используйте add фрагмент,

Вместо этого метода ft.replace(R.id.content, selectedFragment);

Используйте этот ft.add(R.id.content, selectedFragment);

    Fragment selectedFragment = ItemsFragment.newInstance();
    FragmentTransaction ft = getFragmentManager().beginTransaction();
    ft.(R.id.content, selectedFragment);
    ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
    ft.addToBackStack(null);
    ft.commit();