在使用选项卡的操作栏中,onCreateOptionsMenu被调用的次数过多

15

这是我的问题。我有一个应用程序,其中我使用ActionBar Sherlock与选项卡、片段和选项菜单。每次我旋转模拟器���,即使隐藏或删除了某些选项卡,所有片段的菜单都会被添加(我尝试了两种方法)。

这是设置:一个FragmentActivity,它具有带选项菜单的ActionBar。

  final ActionBar bar = getSupportActionBar();

  bar.addTab(bar.newTab()
        .setText("1")
        .setTabListener(new MyTabListener(new FragmentList1())));

  bar.addTab(bar.newTab()
        .setText("2")
        .setTabListener(new MyTabListener(new FragmentList2())));

  bar.addTab(bar.newTab()
        .setText("3")
        .setTabListener(new MyTabListener(new FragmentList3())));

  bar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
  bar.setDisplayShowHomeEnabled(true);
  bar.setDisplayShowTitleEnabled(true);
所有选项卡都使用相同的监听器:
private class MyTabListener implements ActionBar.TabListener {
  private final FragmentListBase m_fragment;


  public MyTabListener(FragmentListBase fragment) {
     m_fragment = fragment;
  }


  public void onTabSelected(ActionBar.Tab tab, FragmentTransaction ft) {
     FragmentManager fragmentMgr = ActivityList.this.getSupportFragmentManager();
     FragmentTransaction transaction = fragmentMgr.beginTransaction();

        transaction.add(R.id.frmlyt_list, m_fragment, m_fragment.LIST_TAG);

     transaction.commit();
  }


  public void onTabUnselected(ActionBar.Tab tab, FragmentTransaction ft) {
     FragmentManager fragmentMgr = ActivityList.this.getSupportFragmentManager();
     FragmentTransaction transaction = fragmentMgr.beginTransaction();

     transaction.remove(m_fragment);
     transaction.commit();
  }


  public void onTabReselected(ActionBar.Tab tab, FragmentTransaction ft) {
  }
}

FragmentListBase的每个子类都有自己的菜单,因此所有三个子类都具有:

  setHasOptionsMenu(true);

并且适当的

public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
  Log.d(TAG, "OnCreateOptionsMenu");

  inflater.inflate(R.menu.il_options_menu, menu);
}
当我运行这个应用程序时,我发现onCreateOptionsMenu被多次调用,对于所有不同的片段都是如此。我完全被难住了。我尝试尽可能多地发布代码,而不会让人感到压倒性,如果您发现有遗漏的地方,请告知。[编辑]我添加了更多的日志记录,结果发现在旋转时该片段被附加了两次(或更多次)。 我注意到的一件事是,除了仅被调用一次的onCreate()方法之外,其他所有内容都被多次调用。
06.704:/WindowManager(72): Setting rotation to 0, animFlags=0
06.926:/ActivityManager(72): Config changed: { scale=1.0 imsi=310/260 loc=en_US touch=3 keys=1/1/2 nav=1/2 orien=L layout=0x10000014 uiMode=0x11 seq=35}
07.374:/FragmentList1(6880): onAttach
07.524:/FragmentList1(6880): onCreateView
07.564:/FragmentList1(6880): onAttach
07.564:/FragmentListBase(6880): onCreate
07.564:/FragmentList1(6880): OnCreateOptionsMenu
07.574:/FragmentList1(6880): OnCreateOptionsMenu
07.604:/FragmentList1(6880): onCreateView

[编辑2]

我开始追踪Android代码,并找到了这里的部分内容(我已编辑此帖以缩短长度)。

/com_actionbarsherlock/src/android/support/v4/app/FragmentManager.java

public boolean dispatchCreateOptionsMenu(Menu menu, MenuInflater inflater) {
    if (mActive != null) {
        for (int i=0; i<mAdded.size(); i++) {
            Fragment f = mAdded.get(i);
            if (f != null && !f.mHidden && f.mHasMenu) {
                f.onCreateOptionsMenu(menu, inflater);
            }
        }
    }
问题在于mAdded确实有多个FragmentList1实例,因此onCreateOptionsMenu()方法会正确地针对不同的FragmentList1类实例被调用3次。我不明白的是为什么该类会被多次添加... 但这是一个非常好的线索。
5个回答

7
我似乎已经找到了问题(们)。我说问题(们)是因为除了大量的菜单之外,现在还有一个异常。
1)调用
  bar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);

在调用addTab()后调用onTabSelected()会产生副作用。我的TabListener会向FragmentManager添加一个FragmentList1。

2)旋转设备会按预期销毁Activity,但不会销毁Fragments。当旋转后创建新的Activity时,它将执行两个操作:

  1. create another set of Fragments that it would add to the FragmentManager. This is what was causing the multitude of Menus
  2. call onTabSelected (via setNavigationMode()) which would perform the following code:

     if (null != fragmentMgr.findFragmentByTag(m_fragment.LIST_TAG)) {
        transaction.attach(m_fragment);
        transaction.show(m_fragment);
     }
     else {
        transaction.add(R.id.frmlyt_list, m_fragment, m_fragment.LIST_TAG);
     }
    
基本上,如果片段已经在FragmentManager中,则无需添加它,只需显示它即可。但问题在于,这不是相同的片段!这是早期Activity实例创建的片段。因此,它会尝试附加和显示这个新创建的片段,这将导致异常。
解决方案。
要解决所有这些问题,需要完成以下几件事情:
1)我将setNavigationMode()移到了addTab()之前。
2)这是我现在创建选项卡的方式:
  FragmentListBase fragment = (FragmentListBase)fragmentMgr.findFragmentByTag(FragmentList1.LIST_TAG_STATIC);
  if (null == fragment) {
     fragment = new FragmentList1();
  }
  bar.addTab(bar.newTab()
        .setText("1")
        .setTabListener(new MyTabListener(fragment)));

在创建Activity时,我需要检查Fragment是否已经在FragmentManager中。如果是,则使用这些实例;如果不是,则创建新的实例。这将针对所有三个选项卡执行。你可能已经注意到有两个相似的标签:m_fragment.LIST_TAG和FragmentList1.LIST_TAG_STATIC。啊,这真是太美妙了...(< - 讽刺)。为了能够多态地使用我的TagListener,我在基类中声明了以下非静态变量:
public class FragmentListBase extends Fragment {
   public String LIST_TAG = null;
}

它是从内部分配的,并允许我在FragmentManager中查找FragmentListBase的不同子项。

但是,在创建特定的子项之前(因为我需要知道是否必须创建它们),我还需要搜索它们,所以我也必须声明以下静态变量。

public class FragmentList1 extends FragmentListBase {
   public final static String LIST_TAG_STATIC = "TAG_LIST_1";

   public FragmentList1() {
      LIST_TAG = LIST_TAG_STATIC;
   };
}

可以说我很失望,没有人想出这个简单而优雅的解决方案(<-更多的讽刺)

非常感谢Jake Wharton抽出时间为我解决此问题 :)


1
这个答案过于复杂。只需在向 ActionBar 添加选项卡之前调用 bar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS) 即可解决我的问题。 - Phil

6
public FragmentListBase() {
    setRetainInstance(true);
    setHasOptionsMenu(true);
}

在旋转屏幕时,这将保存/恢复每个片段的各自状态。


另一个简单的更改是在选中选项卡回调中调用transaction.replace(R.id.frmlyt_list, m_fragment, m_fragment.LIST_TAG)并且在取消选中回调中清除内容。


谢谢你的回答,Jake。但是:没有用。我已经将它添加到代码中,但没有任何变化。不过,我在我的问题中添加了更多信息... - MikeWallaceDev

3

我曾经遇到过类似的问题,当菜单旋转时,“可堆叠”菜单出现了问题。虽然我没有使用选项卡,但我使用了带有FragmentStatePagerAdapter的ViewPager,因此无法重用我的Fragments。在苦苦思索了两天后,我找到了一个非常简单的解决方案。实际上,问题似乎是由于onCreateOptionsMenu被多次调用而引起的。这段小代码片段可以处理(或掩盖)所有问题:

/** to prevent multiple calls to inflate menu */
private boolean menuIsInflated;

@Override
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
    if (!menuIsInflated) {
        inflater.inflate(R.menu.job_details_fragment_menu, menu);
        menuIsInflated = true;
    }
}

很遗憾我们不得不这样做...我发誓这是他们那边的一个漏洞。 - reidisaki

1
我的解决方法是将setHasMenuOptions(true)移动到调用片段(fragment)的活动(activity),也就是声明该片段的活动中。我之前是将其放在片段的onCreate方法中。
以下是代码片段:
@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        FragmentManager fragmentManager = getFragmentManager();
        FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();

        ForecastFragment forecastFragment = new ForecastFragment();
        forecastFragment.setHasOptionsMenu(true);
        fragmentTransaction.add(R.id.fragment, forecastFragment);
        fragmentTransaction.commit();
    }

以上的答案都不适用于我,但是您的回答很有效。谢谢! - Kaveesh Kanwal

0

关于多态标签的问题,我有一个小提示。

这样声明您的基类:

public abstract class ListFragmentBase {
  protected abstract String getListTag();
}

现在声明你的子类,就像这样:

public class FragmentList1 extends ListFragmentBase {
    public static final String LIST_TAG = "TAG_LIST_1";

    @Override
    protected String getListTag() {
        return LIST_TAG;
    }
}

现在获取实例标签的多态方式如下:

ListFragmentBase frag = new FragmentList1();
frag.getListTag();

像这样静态地获取标签:

FragmentList1.LIST_TAG;

1
你甚至可以将静态的LIST_TAG字符串设置为私有,以避免对使用哪个属性产生混淆。 - Dominik Weber
@DominikvonWeber 你可以将LIST_TAG设置为私有的,但是这样你就无法静态访问它了。我想这取决于你的需求。在这个解决方案中,用户是通过静态方式访问它的。 - Dave

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