使用 ActionBar 旋转 Android 的双片段

Posted

技术标签:

【中文标题】使用 ActionBar 旋转 Android 的双片段【英文标题】:Double fragment rotating Android with ActionBar 【发布时间】:2012-03-16 03:39:45 【问题描述】:

我制作了一个带有 ActionBar 的简单 android Activity,用于在 2 个片段之间切换。 在我旋转设备之前一切正常。事实上,当我旋转时,我有 2 个片段一个接一个:前一个活动片段和第一个片段。 为什么? 如果旋转破坏并重新创建我的活动,为什么我会获得 2 个片段?

示例代码:

活动

package rb.rfrag.namespace;

import android.app.ActionBar;
import android.app.ActionBar.Tab;
import android.app.Activity;
import android.os.Bundle;

    public class RFragActivity extends Activity 
    /** Called when the activity is first created. */
    @Override
    protected void onCreate(Bundle savedInstanceState) 
        super.onCreate(savedInstanceState);
        // Notice that setContentView() is not used, because we use the root
        // android.R.id.content as the container for each fragment

     // setup action bar for tabs
        final ActionBar actionBar = getActionBar();
        actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
        //actionBar.setDisplayShowTitleEnabled(false);

        Tab tab;
        tab = actionBar.newTab()
                .setText(R.string.VarsTab)
                .setTabListener(new TabListener<VarValues>(
                        this, "VarValues", VarValues.class));
        actionBar.addTab(tab);

        tab = actionBar.newTab()
                .setText(R.string.SecTab)
                .setTabListener(new TabListener<SecFrag>(
                        this, "SecFrag", SecFrag.class));
        actionBar.addTab(tab);
    

TabListener

package rb.rfrag.namespace;

import android.app.ActionBar;
import android.app.Activity;
import android.app.Fragment;
import android.app.FragmentManager;
import android.app.FragmentTransaction;
import android.app.ActionBar.Tab;

public class TabListener<T extends Fragment> implements ActionBar.TabListener 
    private Fragment mFragment;
    private final Activity mActivity;
    private final String mTag;
    private final Class<T> mClass;

    /** Constructor used each time a new tab is created.
      * @param activity  The host Activity, used to instantiate the fragment
      * @param tag  The identifier tag for the fragment
      * @param clz  The fragment's Class, used to instantiate the fragment
      */
    public TabListener(Activity activity, String tag, Class<T> clz) 
        mActivity = activity;
        mTag = tag;
        mClass = clz;
    

    /* The following are each of the ActionBar.TabListener callbacks */

    public void onTabSelected(Tab tab, FragmentTransaction ft)     
        // Check if the fragment is already initialized
        if (mFragment == null) 
            // If not, instantiate and add it to the activity
            mFragment = Fragment.instantiate(mActivity, mClass.getName());
            ft.add(android.R.id.content, mFragment, mTag);
         else 
            // If it exists, simply attach it in order to show it
            ft.attach(mFragment);
        
    

    public void onTabUnselected(Tab tab, FragmentTransaction ft) 
        if (mFragment != null) 
            // Detach the fragment, because another one is being attached
            ft.detach(mFragment);
        
    

【问题讨论】:

【参考方案1】:

我已经解决了在Activity中使用onSaveInstanceStateonRestoreInstanceState来维护选中的tab,修改onTabSelected如下。

最后的修改避免了 Android 重建它不会破坏的 Fragment。但是我不明白为什么 Activity 被旋转事件破坏,而当前的 Fragment 没有。 (你有什么想法吗?)

public void onTabSelected(Tab tab, FragmentTransaction ft) 
        // previous Fragment management
        Fragment prevFragment;
        FragmentManager fm = mActivity.getFragmentManager();
        prevFragment = fm.findFragmentByTag(mTag); 
        if (prevFragment != null)  
            mFragment = prevFragment; 
         // \previous Fragment management

        // Check if the fragment is already initialized
        if (mFragment == null) 
            // If not, instantiate and add it to the activity
            mFragment = Fragment.instantiate(mActivity, mClass.getName());
            ft.add(android.R.id.content, mFragment, mTag);
         else 
            // If it exists, simply attach it in order to show it
            ft.attach(mFragment);
        
    

【讨论】:

这非常有效。关于这是否是“正确”的做事方式的任何线索? 直到我发现这个,这让我发疯了......非常感谢这个答案! (仍然无法相信谷歌不会在他们的例子中解决这个问题) 有趣。只有我在纵向使用android.support.v4.view.ViewPager 并在横向显示两个片段。旋转两次,我得到两个横向片段。一个叫做 android:switcher:2131099773:1 的东西——让我抓狂。我必须找到一个飞节来摆脱僵尸。 经过大量的 r 和 d 这也解决了 mah 问题:)thanx @asclepix 我不明白这个。旋转设备时,确实在片段上调用了 onDestroy()。在那之后它怎么还能被使用和可见?【参考方案2】:

由于我使用 android.support.v4.view.ViewPager 覆盖 onTabSelected 将无济于事。但是你仍然暗示我指向了正确的方向。

android.support.v4.app.FragmentManager 将所有片段保存在android.support.v4.app.FragmentActivityonSaveInstanceState 中。 忽略setRetainInstance — 根据您的设计,这可能会导致重复片段。

最简单的解决方法是删除activity的orCreate中保存的fragment:

   @Override
   public void onCreate (final android.os.Bundle savedInstanceState)
   
      if (savedInstanceState != null)
      
         savedInstanceState.remove ("android:support:fragments");
       // if

      super.onCreate (savedInstanceState);
…
      return;
    // onCreate

【讨论】:

这很好,只是当 Fragment2 进入 Landscape 时,显示 fragment1。不知道我是不是唯一一个有这个问题的人。有什么解决办法吗? 也适用于您的纵向和横向解决方案。在标签之间切换后不再重叠。【参考方案3】:

该解决方案实际上并不需要大量工作,以下步骤可确保在旋转屏幕时保持选项卡选择。我遇到了重叠的片段,因为在屏幕旋转时选择了我的第一个选项卡,而不是在旋转屏幕之前选择的第二个选项卡,因此第一个选项卡与第二个选项卡的内容重叠。

这就是您的 Activity 的外观(我正在使用 ActionBarSherlock,但调整它应该很容易):

public class TabHostActivity extends SherlockFragmentActivity 

   private static final String SELETED_TAB_INDEX = "tabIndex";


   @Override
   protected void onCreate(Bundle savedInstanceState) 
     super.onCreate(savedInstanceState);

     // Setup the action bar
     ActionBar actionBar = getSupportActionBar();
     actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);

     // Create the Tabs you need and add them to the actionBar...

     if (savedInstanceState != null) 
        // Select the tab that was selected before orientation change
        int index = savedInstanceState.getInt(SELETED_TAB_INDEX);
        actionBar.setSelectedNavigationItem(index);
     
   

   @Override
   protected void onSaveInstanceState(Bundle outState) 
     super.onSaveInstanceState(outState);
     // Save the index of the currently selected tab 
     outState.putInt(SELETED_TAB_INDEX, getSupportActionBar().getSelectedTab().getPosition());
   

这就是我的 ActionBar.TabListener 的样子(它是上述 Activity 中的私有类):

  private class MyTabsListener<T extends Fragment> implements ActionBar.TabListener 
     private Fragment fragment;
     private final SherlockFragmentActivity host;
     private final Class<Fragment> type;
     private String tag;

     public MyTabsListener(SherlockFragmentActivity parent, String tag, Class type) 
        this.host = parent;
        this.tag = tag;
        this.type = type;
     

     @Override
     public void onTabSelected(Tab tab, FragmentTransaction transaction) 
        /*
         * The fragment which has been added to this listener may have been
         * replaced (can be the case for lists when drilling down), but if the
         * tag has been retained, we should find the actual fragment that was
         * showing in this tab before the user switched to another.
         */
        Fragment currentlyShowing = host.getSupportFragmentManager().findFragmentByTag(tag);

        // Check if the fragment is already initialised
        if (currentlyShowing == null) 
          // If not, instantiate and add it to the activity
          fragment = SherlockFragment.instantiate(host, type.getName());
          transaction.add(android.R.id.content, fragment, tag);
         else 
          // If it exists, simply attach it in order to show it
          transaction.attach(currentlyShowing);
        
     

     public void onTabUnselected(Tab tab, FragmentTransaction fragmentTransaction) 
        /*
         * The fragment which has been added to this listener may have been
         * replaced (can be the case for lists when drilling down), but if the
         * tag has been retained, we should find the actual fragment that's
         * currently active.
         */
        Fragment currentlyShowing = host.getSupportFragmentManager().findFragmentByTag(tag);
        if (currentlyShowing != null) 
          // Detach the fragment, another tab has been selected
          fragmentTransaction.detach(currentlyShowing);
         else if (this.fragment != null) 
          fragmentTransaction.detach(fragment);
        
     

     public void onTabReselected(Tab tab, FragmentTransaction fragmentTransaction) 
        // This tab is already selected
     

上述实现还允许根据标签替换选项卡中的片段。为此,在同一选项卡中切换片段时,我使用相同的标记名称,该名称用于添加到选项卡的初始框架。

【讨论】:

【参考方案4】:

感谢 Martin 和 asclepix 提供的解决方案。我有 3 个选项卡,第一个选项卡包含 2 个片段,如下所示:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_
    android:layout_
    tools:context=".MainActivity" >

    <FrameLayout 
        android:id="@+id/frActiveTask"
        android:layout_
        android:layout_
        android:layout_alignParentBottom="true" 
        />

    <FrameLayout
        android:id="@+id/frTaskList"
        android:layout_
        android:layout_
        android:layout_alignParentTop="true"
        android:layout_above="@id/frActiveTask"
        />

</RelativeLayout>

使用onRestoreInstanceStateonSaveInstanceStatesavedInstanceState.remove("android:support:fragments"); 方法和语句几乎可以正常工作,除非您的活动选项卡不是第一个并旋转并首先单击,会出现清晰的显示,并且仅在第二次单击第一个选项卡是正确的片段显示。 调试代码后,我发现第一个addTab 总是在选项卡侦听器中调用onTabSelected 事件,并使用片段add 方法,然后当从onRestoreInstanceState 调用setSelectedNavigationItem 时,将执行detach第一个选项卡和 add 另一个选项卡。 这个不必要的add 调用已在我的解决方案中修复。

我的活动

protected void onCreate(Bundle savedInstanceState) 
    boolean firstTabIsNotAdded = false;
    if (savedInstanceState != null) 
        savedInstanceState.remove("android:support:fragments");
        firstTabIsNotAdded = savedInstanceState.getInt(SELETED_TAB_INDEX) != 0;
    
    super.onCreate(savedInstanceState);

    setContentView(R.layout.activity_main);

// codes before adding tabs

    actionBar = getSupportActionBar();
    actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);


    tabStartAndStop = actionBar.newTab().setText(getString(R.string.tab_title_start_and_stop))
            .setTabListener(
                    new FragmentTabListener<StartStopFragment>(this, 
                            getString(R.string.tab_title_start_and_stop_id), 
                            StartStopFragment.class,
                            firstTabIsNotAdded));
    tabHistory = actionBar.newTab().setText(getString(R.string.tab_title_history))
            .setTabListener(
                    new FragmentTabListener<HistoryFragment>(this, 
                            getString(R.string.tab_title_history_id), 
                            HistoryFragment.class,
                            false));
    tabRiporting = actionBar.newTab().setText(getString(R.string.tab_title_reporting))
            .setTabListener(
                    new FragmentTabListener<ReportingFragment>(this, 
                            getString(R.string.tab_title_reporting_id), 
                            ReportingFragment.class,
                            false));

    actionBar.addTab(tabStartAndStop);

        actionBar.addTab(tabHistory);
        actionBar.addTab(tabRiporting);

    

    @Override
    protected void onRestoreInstanceState(Bundle savedInstanceState) 
        if (savedInstanceState != null) 
            int index = savedInstanceState.getInt(SELETED_TAB_INDEX);
            actionBar.setSelectedNavigationItem(index);
        
        super.onRestoreInstanceState(savedInstanceState);
    

    @Override
    protected void onSaveInstanceState(Bundle outState) 
        super.onSaveInstanceState(outState);
        // Save the index of the currently selected tab
        outState.putInt(SELETED_TAB_INDEX, getSupportActionBar().getSelectedTab().getPosition());
    

以及修改后的tab监听器

public class FragmentTabListener<T extends SherlockFragment> implements com.actionbarsherlock.app.ActionBar.TabListener 
    private Fragment mFragment;
    private final Activity mFragmentActivity;
    private final String mTag;
    private final Class<T> mClass;
    private boolean doNotAdd;

    /** Constructor used each time a new tab is created.
      * @param activity  The host Activity, used to instantiate the fragment
      * @param tag  The identifier tag for the fragment
      * @param clz  The fragment's Class, used to instantiate the fragment
      */
    public FragmentTabListener(Activity activity, String tag, Class<T> clz, boolean doNotAdd) 
        mFragmentActivity = activity;
        mTag = tag;
        mClass = clz;
        this.doNotAdd = doNotAdd;
    

    /* The following are each of the ActionBar.TabListener callbacks */
    public void onTabSelected(Tab tab, FragmentTransaction ft) 

        // Check if the fragment is already initialized
        if (mFragment == null) 
            // If not, instantiate and add it to the activity
            if(doNotAdd)
                doNotAdd = false;
            else
                mFragment = Fragment.instantiate(mFragmentActivity, mClass.getName());
                ft.add(android.R.id.content, mFragment, mTag);
            
         else 
            // If it exists, simply attach it in order to show it
            ft.attach(mFragment);
        
    

    public void onTabUnselected(Tab tab, FragmentTransaction ft) 
        if (mFragment != null) 
            // Detach the fragment, because another one is being attached
            ft.detach(mFragment);
        
    

    public void onTabReselected(Tab tab, FragmentTransaction ft) 
        // User selected the already selected tab. Usually do nothing.
    

【讨论】:

以上是关于使用 ActionBar 旋转 Android 的双片段的主要内容,如果未能解决你的问题,请参考以下文章

android如何去掉actionbar

Android 实现ActionBar定制

如何获得 ActionBar 的高度?

Android中活动条ActionBar的详细使用

如何使用android-support-V7包中ActionBar

Android界面编程--使用活动条(ActionBar)