使用片段为 BottomNavigationView Android 中的每个选项卡单独返回堆栈

Separate Back Stack for each tab in BottomNavigationView Android using Fragments

我正在 Android 应用中实施 BottomNavigationView 导航。我正在使用片段来设置每个选项卡的内容。

我知道如何为每个选项卡设置一个片段,然后在单击选项卡时切换片段。但是我怎样才能为每个选项卡都有一个单独的后退堆栈呢? 这是设置一个片段的代码:

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 应该再次显示。此时应该可以使用返回按钮显示Fragment 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();

不使用替换方法,而是使用添加片段,

代替这个方法 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();

值得注意的是,您描述的行为违反了 Google 准则。 https://material.io/guidelines/components/bottom-navigation.html#bottom-navigation-behavior

Navigation through the bottom navigation bar should reset the task state.

换句话说,有片段 A 和片段 B "inside" 选项卡 1 没问题,但如果用户打开片段 B,单击选项卡 2,然后再次单击选项卡 1,他们应该会看到片段 A。

最后,我找到了解决方案,它的灵感来自于以前在 Whosebug 上的回答:Separate Back Stack for each tab in Android using Fragments
我只用 BottomNavigationView 替换了 TabHost,下面是代码:
主要Activity

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's 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;
}

}

新的导航架构组件 (https://developer.android.com/topic/libraries/architecture/navigation/) 支持此行为。

本质上,可以使用NavHostFragment,这是一个片段,它控制自己的返回堆栈:

Each NavHostFragment has a NavController that defines valid navigation within the navigation host. This includes the navigation graph as well as navigation state such as current location and back stack that will be saved and restored along with the NavHostFragment itself. https://developer.android.com/reference/androidx/navigation/fragment/NavHostFragment

这是一个例子:https://github.com/deisold/navigation


编辑:事实证明,导航架构组件无论如何都不支持单独的返回堆栈,正如评论者所指出的那样。但正如@r4jiv007 提到的,他们正在努力并同时提供了 "official hack":https://github.com/googlesamples/android-architecture-components/tree/master/NavigationAdvancedSample

假设你有 5(A, B, C, D, E) 个 BottomNavigationView 菜单项,然后在 Activity 中创建 5 个 FrameLayout(frmlytA, frmlytB, frmlytC, frmlytD, frmlytE) 以平行重叠的方式作为每个菜单项的容器。当按下 BottomNavigation 菜单项 A 时,然后隐藏所有其他 FrameLayouts(Visibility = GONE)并仅显示(Visibility = VISIBLE)将托管 FragmentA 的 FrameLayout 'frmlytA' 并在此容器上进行进一步的交易,如(FragmentA - > 片段 X -> 片段 Y)。然后,如果用户单击 BottomNavigation 菜单项 B,则只需隐藏此 (frmlytA) 容器并显示 'frmlytB'。然后,如果用户再次按下菜单项 A 然后显示 'frmlytA' 它应该保留较早的状态。所以,像这样你可以在容器FrameLayouts之间切换,并且可以维护每个容器的返回堆栈。