在Android中使用Fragment实现BottomNavigationView的每个选项卡拥有独立的后退栈

22

我正在为Android应用程序实现BottomNavigationView进行导航。我使用Fragment为每个选项卡设置内容。

我知道如何为每个选项卡设置一个碎片,然后在单击选项卡时切换碎片。但是,我如何为每个选项卡拥有单独的后退堆栈? 以下是设置一个碎片的代码:

Fragment selectedFragment = ItemsFragment.newInstance();
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
transaction.replace(R.id.content, selectedFragment);
transaction.commit();
例如,片段 A 和 B 将位于选项卡 1 下,片段 C 和 D 将位于选项卡 2 下。启动应用程序时,将显示片段 A 并选择选项卡 1。然后,片段 A 可能会被替换为片段 B。选择选项卡 2 时应显示片段 C。如果然后选择选项卡 1,则应再次显示片段 B。此时,可以使用返回按钮显示片段 A。
以下是设置在同一选项卡中下一个片段的代码:
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
不幸的是,你必须自己实现这种分段回退行为...至少我是这么认为的。 - Shark
你以前做过吗? - Rita Azar
不,我的客户从未想要这样的糟糕用户体验,但我有一个想法。介于当前backstack和https://en.wikipedia.org/wiki/Command_pattern之间,基本上是,保持`HashMap<CurrentTab, ArrayList<CommandItem>,并添加到/从你想要的后退堆栈中删除。将需要修改onBackPressed(),以及可能/可能不使用addToBackstack()`。 - Shark
1
请查看此链接,https://dev59.com/92w05IYBdhLWcg3w3VaX。 - Rajan1404930
5个回答

32

最终我找到了解决方案,它受到StackOverflow上一个先前答案的启发:在Android中使用Fragments为每个选项卡分别设置后退堆栈
我只是将TabHost替换为BottomNavigationView,并提供以下代码:
Main 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;
}

}


1
但是如何在屏幕旋转时保存HashMap的状态? - Kartik Watwani
首先感谢您的回答,但是在返回键上我遇到了一个错误,即mStacks.get(mCurrentTab)为null,在上面的goTOFragment中没有使用?请帮忙。 - Subin Babu
@Rita Azar谢谢您提供的解决方案。但是您正在替换片段。它完全破坏了以前的片段。这意味着,当我们转到已经存在的片段时,它会再次创建。这会导致从网络重新下载数据(在HTTP请求的情况下)。那么,您知道如何解决这个问题吗? - neo
1
@Athul 试试这个 - https://medium.com/@smihajlovskih/create-instagram-like-backstack-4711600c5bff - jlively
链接无法访问,无法翻译。 - Sumukha Aithal K
这正是我正在寻找的!!非常感谢您。 - J.Dragon

4
这种行为得到了新导航架构组件的支持(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

1
我认为它不会,至少默认配置下不会。Jetpack导航不会将导航片段添加到后退堆栈中,如果您从菜单1转到2再转到3,然后按返回键,它将返回到1(假设1被设置为图表中的入口片段),忽略2。这种行为似乎不是OP想要的。 - Allan Veloso
导航架构组件不支持此功能,已经有一个类似的问题在进行中,谷歌提供了使用导航架构组件的hacky解决方案,但它会忽略一些边缘情况。https://issuetracker.google.com/issues/80029773 - r4jiv007

3

4
Instagram不会清空后退栈,如果你从一个标签页切换到另一个标签页。 - Kartik Watwani
4
谷歌的应用程序并不遵循这个“指南”,例如YouTube应用程序。 - konata
5
该页面已更改。现在它说:“轻触先前访问过的部分的导航目标会将用户返回到该部分中离开的位置。” https://material.io/design/components/bottom-navigation.html#behavior - Marco Romano
Instagram是一种React Native应用程序(一种混合应用程序),而不是本地的Android应用程序。 - mithil1501

0
假设您有5个(A、B、C、D、E)BottomNavigationView菜单项,那么在Activity中创建5个FrameLayout(frmlytA、frmlytB、frmlytC、frmlytD、frmlytE),以平行重叠的方式作为每个菜单项的容器。当按下BottomNavigation菜单项A时,隐藏所有其他FrameLayout(Visibility = GONE),并仅显示(Visibility = VISIBLE)将托管FragmentA的FrameLayout“frmlytA”,并在此容器上执行进一步的事务,如(FragmentA-> FragmentX-> FragmentY)。然后,如果用户点击BottomNavigation菜单项B,则只需隐藏此(frmlytA)容器并显示'frmlytB'。然后,如果用户再次按菜单项A,则显示'frmlytA',它应保留先前的状态。因此,您可以在容器FrameLayout之间切换,并可以维护每个容器的后退堆栈。

-6

不要使用replace方法,而是使用add fragment方法:

不要使用以下方法: 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();

使用 add 而不是 replace 会导致性能差和 OOM。 - Umar Ata

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接