使用片段返回堆栈处理 ActionBar 标题?

Posted

技术标签:

【中文标题】使用片段返回堆栈处理 ActionBar 标题?【英文标题】:Handling ActionBar title with the fragment back stack? 【发布时间】:2012-11-08 11:08:44 【问题描述】:

我有一个Activity,我在其中加载了一个ListFragment,点击后,它会向下钻取一个级别并显示一种新类型的ListFragment,替换原来的类型(使用下面的showFragment 方法)。这被放置在后堆栈上。

一开始,Activity 在操作栏中显示默认标题(即它是根据应用程序的android:label 自动设置的)。

在显示层次结构中下一级的列表时,单击的项目名称应成为操作栏的标题。

但是,当按下 Back 时,我希望恢复原来的默认标题。这不是FragmentTransaction 知道的事情,所以不会恢复标题。

我隐约读到过FragmentBreadCrumbs,但这似乎需要使用自定义视图。我正在使用 ActionBarSherlock,并且不希望有自己的自定义标题视图。

这样做的最佳方法是什么?是否有可能无需加载样板代码并且必须跟踪沿途显示的标题?


protected void showFragment(Fragment f) 
  FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
  ft.replace(R.id.fragment_container, f);
  ft.addToBackStack(null);
  ft.commit();

【问题讨论】:

【参考方案1】:

在每个片段和每个活动中,我都会像这样更改标题。这样,活动标题将始终正确:

@Override
public void onResume() 
    super.onResume();
    // Set title
    getActivity().getActionBar()
        .setTitle(R.string.thetitle);

在某些情况下,onResume 不会在片段内部调用。在某些情况下,我们可以使用:

public void setUserVisibleHint(boolean isVisibleToUser) 
    super.setUserVisibleHint(isVisibleToUser);
    if(isVisibleToUser) 
        // Set title
        getActivity().getActionBar()
            .setTitle(R.string.thetitle);
    

【讨论】:

我不需要投射或获取操作栏(即我只需调用getActivity().setTitle(...)),但这是一种合理的方法。谢谢。 -1。这种做法是错误的!根据 Android 设计指南,当使用 Navigation Drawer 切换 Fragment 时,Action Bar 标题应仅在 Navigation Drawer onClosed 回调中更改。在导航抽屉关闭之前,您的方法会错误地更改标题。 @zyamys 如果你有更好的建议,我很乐意支持它。这是我当时能想到的最佳答案。 这对我不起作用,所以我将其更改为 getActivity().setTitle(R.string.thetitle);,然后它起作用了。 这仅在您在 FragmentTransaction 期间替换片段时才有效。如果你添加一个片段而不是替换呢?【参考方案2】:

由于原始答案已经很老了,这也可能会有所帮助。正如文档所述,可能需要注册一个listener 来监听托管Activity 中的后台堆栈更改:

getSupportFragmentManager().addOnBackStackChangedListener(
        new FragmentManager.OnBackStackChangedListener() 
            public void onBackStackChanged() 
                // Update your UI here.
            
        );

然后,在回调方法中识别情况并设置适当的标题,而不是从Fragment 访问ActionBar

这是一个更优雅的解决方案,因为Fragment 不必知道ActionBar 的存在,而Activity 通常是管理后台堆栈的地方,因此在那里处理它似乎更合适. Fragment 应始终只考虑其自身的内容,而不是周围环境。

documentation 中有关该主题的更多信息。

【讨论】:

另一个答案是旧的,但我仍然发现它是确保标题始终正确的最简单和最实用的方法。正如我的评论中提到的,您不需要直接访问 ActionBar 。此处的解决方案需要更多代码,但对于您的初始片段也无济于事,通常不会将其添加到后台堆栈中。 从片段访问操作栏也没有错。 Google 还提供了用于更改片段内的菜单项的 api。 一段时间后我得出结论,两种解决方案都很好,这取决于您的用例,有时使用 onResume 和有时使用片段管理器侦听器会更可行。所以总而言之,了解您的工具是件好事,这样您就可以将它们充分应用到您的需求中。 @Maciej Pigulski 然后呢?我们对回调中的上下文一无所知。 @caBBAlainB,上下文保存在托管活动的 FragmentManager 中,因此您必须确定标题,对管理器进行一些繁琐的检查 - 因此它不是很好。今天我不记得我在这里的理由是什么。我认为这个想法主要来自这个答案中链接的 Android 文档中所写的内容(侦听器 sn-p 上方的段落)。今天我宁愿去使用公认的答案。【参考方案3】:

让控制活动按如下方式完成所有工作:

监听 backstack 事件(在活动的 onCreate() 中):

// Change the title back when the fragment is changed
    getSupportFragmentManager().addOnBackStackChangedListener(new FragmentManager.OnBackStackChangedListener() 
        @Override
        public void onBackStackChanged() 
            Fragment fragment = getFragment();
            setTitleFromFragment(fragment);
        
    );

从容器中获取当前片段:

/**
 * Returns the currently displayed fragment.
 * @return
 *      Fragment or null.
 */
private Fragment getFragment() 
    Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.container);
    return fragment;

在内容视图中设置片段:

private void setFragment(Fragment fragment, boolean addToBackStack) 
    // Set the activity title
    setTitleFromFragment(fragment);
    .
    .
    .

【讨论】:

如果您在每个Fragment 中实现了一些接口,那么这并不是“让活动完成所有工作”,以便setTitleFromFragment() 可以检索其标题。这些似乎都不适合我的问题的“没有大量样板代码”部分。 我的解决方案中没有提到接口,您的活动可以检查片段的实例并根据该实例设置标题,从而让您的活动完成所有工作。所有这些方法都在 Activity 内部定义。 是的,从设计/关注点分离/可重用性的角度来看,这听起来可能更糟。另外,这会为添加到后台堆栈的每个片段重复更改标题两次,因为 setTitleFromFragment 方法在 setFragment 中被无条件调用,我猜?无论如何,谢谢,但我会坚持务实而简单的接受答案:) 这听起来“从设计/关注点分离/可重用性的角度来看可能更糟”?请用有效的论据支持它?您让活动成为“控制器”并根据其内容设置自己的标题。如果我想在多个地方使用一个片段,或者有多个片段的活动,他们都尝试设置标题。 很有帮助的答案【参考方案4】:

Warpzit 是对的。这也解决了更改设备方向时的标题问题。此外,如果您将 support v7 用于操作栏,您可以从片段中获取操作栏,如下所示:

@Override
public void onResume() 
    super.onResume();
    ((ActionBarActivity)getActivity()).getSupportActionBar().setTitle("Home");

【讨论】:

+1 支持 v7。我认为,每当给出 android 答案时,回答的人都应该考虑支持。到目前为止,要达到 80% 的设备覆盖率,您必须回到 API 16。在大多数情况下,这需要使用兼容库。 我把你的代码放在public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) 方法中并且工作正常。【参考方案5】:

最好让操作系统完成尽可能多的工作。 假设每个片段都使用 .addToBackStack("title") 正确命名,然后 你可以重写 onBackPressed 这样的东西来实现所需的行为:

// this example uses the AppCompat support library
// and works for dynamic fragment titles
@Override
public void onBackPressed() 
    FragmentManager fragmentManager = getSupportFragmentManager();
    int count = fragmentManager.getBackStackEntryCount();
    if (count <= 1) 
        finish();
    
    else 
        String title = fragmentManager.getBackStackEntryAt(count-2).getName();
        if (count == 2) 
            // here I am using a NavigationDrawer and open it when transitioning to the initial fragment
            // a second back-press will result in finish() being called above.
            mDrawerLayout.openDrawer(mNavigationDrawerFragment.getView());
        
        super.onBackPressed();
        Log.v(TAG, "onBackPressed - title="+title);
        getSupportActionBar().setTitle(title);
    

【讨论】:

【参考方案6】:

我使用与Lee approach 类似的解决方案,但替换为onBackStackChanged() 方法。

首先我在将事务添加到后台堆栈时设置片段名称。

getSupportFragmentManager().beginTransaction()
                .replace(R.id.frame_content, fragment)
                .addToBackStack(fragmentTitle)
                .commit();

然后我重写 onBackStackChanged() 方法并使用最后一个 backstack 条目名称调用 setTitle()

@Override
public void onBackStackChanged() 
    int lastBackStackEntryCount = getSupportFragmentManager().getBackStackEntryCount() - 1;
    FragmentManager.BackStackEntry lastBackStackEntry =
            getSupportFragmentManager().getBackStackEntryAt(lastBackStackEntryCount);

    setTitle(lastBackStackEntry.getName());

【讨论】:

使用 backstack 条目名称的问题是它不处理配置更改。处理此问题的一个有趣且正确的方法是使用 setBreadCrumbTitle(int res) 保存标题资源 id,您可以在 onBackStackChanged 中使用它来设置标题。【参考方案7】:

使用 Fragments 方法:

@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater)

在每次出现 Fragment 时都会调用它,但 onResume 不会。

【讨论】:

是的..没有一个答案没有保存我的问题..这很好用。这可能不是专业的方式..但它有点hacky。 @Bevor 要使此功能正常工作,您需要一个选项菜单,您可以通过调用“setHasOptionsMenu(true);”为片段激活它在片段的 OnCreateView(...) 方法中。【参考方案8】:

最好的办法是利用android提供的接口OnBackStackChangedListener方法onBackStackChanged()。

假设我们有一个导航抽屉,其中包含用户可以导航到的 4 个选项。在这种情况下,我们将有 4 个片段。让我们先看代码,然后我会解释工作原理。

    private int mPreviousBackStackCount = 0;
    private String[] title_name = "Frag1","Frag2","Frag3","Frag4";
    Stack<String> mFragPositionTitleDisplayed;

    public class MainActivity extends ActionBarActivity implements FragmentManager.OnBackStackChangedListener
    @Override
    protected void onCreate(Bundle savedInstanceState) 
    super.onCreate(savedInstanceState);
    ....
    ....
    ....
    getSupportFragmentManager().addOnBackStackChangedListener(this);
    mFragPositionTitleDisplayed = new Stack<>();


public void displayFragment() 
    Fragment fragment = null;
    String title = getResources().getString(R.string.app_name);
    switch (position) 
        case 0:
            fragment = new Fragment1();
            title = title_name[position];
            break;
        case 1:
            fragment = new Fragment2();
            title = title_name[position];
            break;
        case 2:
            fragment = new Fragment3();
            title = title_name[position];
            break;
        case 3:
            fragment = new Fragment4();
            title = title_name[position];
            break;
        default:
            break;
    
    if (fragment != null) 
        FragmentManager fragmentManager = getSupportFragmentManager();
        fragmentManager.beginTransaction()
                .replace(R.id.container_body, fragment)
                .addToBackStack(null)
                .commit();
        getSupportActionBar().setTitle(title);
    


@Override
public void onBackStackChanged() 
    int backStackEntryCount = getSupportFragmentManager().getBackStackEntryCount();
    if(mPreviousBackStackCount >= backStackEntryCount) 
        mFragPositionTitleDisplayed.pop();
        if (backStackEntryCount == 0)
            getSupportActionBar().setTitle(R.string.app_name);
        else if (backStackEntryCount > 0) 
            getSupportActionBar().setTitle(mFragPositionTitleDisplayed.peek());
        
        mPreviousBackStackCount--;
    
    else
        mFragPositionTitleDisplayed.push(title_name[position]);
        mPreviousBackStackCount++;
    

   

在显示的代码中,我们有 displayFragment() 方法。这里我根据从导航抽屉中选择的选项显示片段。变量位置对应于从导航抽屉中的ListView或RecyclerView单击的项目的位置。我使用 getSupportActionBar.setTitle(title) 相应地设置了操作栏标题,其中标题存储了相应的标题名称。

每当我们从导航抽屉中单击项目时,都会显示一个片段,具体取决于用户单击的项目。但是在后端,这个片段被添加到 backstack 并且方法 onBackStachChanged() 被命中。我所做的是创建了一个变量 mPreviousBackStackCount 并将其初始化为 0。我还创建了一个额外的堆栈来存储操作栏标题名称。每当我向后台堆栈添加一个新片段时,我都会将相应的标题名称添加到我创建的堆栈中。另一方面,每当我按下后退按钮时,都会调用 onBackStackChanged(),并从堆栈中弹出最后一个标题名称,并将标题设置为堆栈的 peek() 方法派生的名称。

例子:

假设我们的 android backstack 是空的:

从导航抽屉中按选择 1: 调用 onBackStachChanged() 并将 Fragment 1 添加到 android backstack,backStackEntryCount 设置为 1,并将 Frag1 推送到我的堆栈中,并且 mFragPositionTitleDisplayed 的大小变为 1。

从导航抽屉中按选择 2: 调用 onBackStachChanged() 并将 Fragment 2 添加到 android backstack 中,backStackEntryCount 设置为 2 并将 Frag2 推送到我的堆栈中,并且 mFragPositionTitleDisplayed 的大小变为 2。

现在我们在 android 堆栈和我的堆栈中都有 2 个元素。当您按下返回按钮时,会调用 onBackStackChanged() 并且 backStackEntryCount 的值为 1。代码进入 if 部分并从我的堆栈中弹出最后一个条目。因此,android backstack 只有 1 个片段 - “Fragment 1”,而我的堆栈只有 1 个标题 - “Frag1”。现在我只是从我的堆栈中 peek() 标题并将操作栏设置为该标题。

记住:要设置动作蝙蝠标题使用 peek() 而不是 pop() 否则当您打开超过 2 个片段并尝试按返回按钮返回时,您的应用程序将崩溃。

【讨论】:

这是一个荒谬的代码量(特别是与接受的答案相比),并且忽略了我在问题中的要求,即在没有样板代码负载且无需跟踪显示的标题。 是的,如果您不想要过多的代码,那就是您的问题。你必须在某个地方妥协。此外,代码中唯一重要的部分是 OnBackStackChanged()。【参考方案9】:

您可以使用 onKeyDown 解决! 我有一个布尔 mainisopen=true

这是我的代码:

public boolean onKeyDown(int keyCode, KeyEvent event) 
    if (keyCode == KeyEvent.KEYCODE_BACK && mainisopen == false) 
        mainisopen = true;
        HomeFrag fragment = new HomeFrag();
        FragmentTransaction fragmentTransaction =
                getSupportFragmentManager().beginTransaction();
        fragmentTransaction.replace(R.id.fragmet_cont, fragment);
        fragmentTransaction.commit();
        navigationView = (NavigationView) findViewById(R.id.nav_view);
        navigationView.getMenu().findItem(R.id.nav_home).setChecked(true);
        navigationView.setNavigationItemSelectedListener(this);
        this.setTitle("Digi - Home"); //Here set the Title back
        return true;
     else 
        if (keyCode == KeyEvent.KEYCODE_BACK && mainisopen == true) 
            AlertDialog.Builder builder = new AlertDialog.Builder(this);
            builder.setMessage("Wollen sie die App schliessen!");
            builder.setCancelable(true);

            builder.setPositiveButton("Ja!", new DialogInterface.OnClickListener() 
                public void onClick(DialogInterface dialog, int which) 
                    System.exit(1);
                
            );

            builder.setNegativeButton("Nein!", new DialogInterface.OnClickListener() 
                public void onClick(DialogInterface dialog, int which) 
                    Toast.makeText(getApplicationContext(), "Applikation wird fortgesetzt", Toast.LENGTH_SHORT).show();
                
            );

            AlertDialog dialog = builder.create();
            dialog.show();

            return true;
        
        return super.onKeyDown(keyCode, event);
    


【讨论】:

【参考方案10】:

正如here 所述,我的解决方案是将此代码添加到 MainActivity onCreate method(): 并更改操作栏标题

FragmentManager fragmentManager=getSupportFragmentManager();
fragmentManager.addOnBackStackChangedListener(new FragmentManager.OnBackStackChangedListener() 
    @Override
    public void onBackStackChanged() 
        Fragment currentFragment = fragmentManager.findFragmentById(R.id.My_Container_1_ID);
        currentFragment.onResume();
    
);

并在片段的 onResume() 方法中更改操作栏标题

@Override
public void onResume() 
    super.onResume();
    AppCompatActivity activity = (AppCompatActivity) getActivity();
    ActionBar actionBar = activity.getSupportActionBar();
    if(actionBar!=null) 
        actionBar.setTitle("Fragment Title");
        actionBar.setSubtitle("Subtitle");
    


【讨论】:

【参考方案11】:

在后按时更新操作栏标题。简单地说

getActivity.setTitle("title")

在 onCreateView 方法中。

【讨论】:

onCreateView 仅在...创建视图时被调用。如果你按下 Back 并且之前的 activity/fragment 还在栈上,那么这个方法将不会被调用。

以上是关于使用片段返回堆栈处理 ActionBar 标题?的主要内容,如果未能解决你的问题,请参考以下文章

替换或删除后台堆栈上现有片段的代码不起作用

导航抽屉backstack,如何让actionbar标题在点击后随片段改变

使用 ActionBar 旋转 Android 的双片段

启动片段时隐藏ActionBar

使用androidx获取片段内的actionBar

ClassCastException片段android使用ActionBar