首页 文章

使用Fragments为Android中的每个选项卡分隔Back Stack

提问于
浏览
154

我正在尝试在Android应用中实现导航标签 . 由于不推荐使用TabActivity和ActivityGroup,我想使用Fragments来实现它 .

我知道如何为每个选项卡设置一个片段,然后在单击选项卡时切换片段 . 但是,如何为每个选项卡分别设置一个后备堆栈?

例如,片段A和B将位于选项卡1下,片段C和D位于选项卡2下 . 启动应用程序时,将显示片段A并选择选项卡1 . 然后片段A可能被片段B替换 . 当选择标签2时,应显示片段C.如果选择了选项卡1,则应再次显示片段B.此时应该可以使用后退按钮显示片段A.

此外,重要的是在旋转设备时保持每个标签的状态 .

BR马丁

12 回答

  • 2

    该框架目前不会自动为您执行此操作 . 您需要为每个选项卡构建和管理自己的后台堆栈 .

    说实话,这似乎是一个值得怀疑的事情 . 我无法想象它会产生一个像样的UI - 如果后面的键会根据我的选项卡做不同的事情,特别是如果后面的键也有正常的行为关闭整个活动的时候在顶部堆栈......听起来很讨厌 .

    如果您正在尝试构建类似Web浏览器UI的东西,那么获取对用户来说很自然的用户体验将会涉及很多细微的行为调整,这取决于上下文,所以你肯定需要做自己的后台栈管理而不是依赖于框架中的一些默认实现 . 举个例子,尝试注意后面的键如何以各种方式与标准浏览器进行交互 . (浏览器中的每个“窗口”本质上都是一个选项卡 . )

  • 134

    我对这个问题非常迟 . 但是因为这个帖子非常有用,对我有帮助,所以我觉得我最好在这里发两个便士 .

    我需要这样的屏幕流程(一个简约的设计,每个标签有2个标签和2个视图),

    tabA
        ->  ScreenA1, ScreenA2
    tabB
        ->  ScreenB1, ScreenB2
    

    我过去有同样的要求,我使用 TabActivityGroup (当时也已弃用)和活动 . 这次我想使用Fragments .

    所以我就这样做了 .

    1.创建基础片段类

    public class BaseFragment extends Fragment {
        AppMainTabActivity mActivity;
    
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            mActivity = (AppMainTabActivity) this.getActivity();
        }
    
        public void onBackPressed(){
        }
    
        public void onActivityResult(int requestCode, int resultCode, Intent data){
        }
    }
    

    应用中的所有片段都可以扩展此Base类 . 如果你想使用像 ListFragment 这样的特殊片段,你也应该为它创建一个基类 . 如果您完整阅读帖子,您将清楚 onBackPressed()onActivityResult() 的用法 .

    2.创建一些Tab标识符,可在项目中的任何位置访问

    public class AppConstants{
        public static final String TAB_A  = "tab_a_identifier";
        public static final String TAB_B  = "tab_b_identifier";
    
        //Your other constants, if you have them..
    }
    

    没什么可解释的..

    3.好的,主要标签活动 - 请在代码中查看评论..

    public class AppMainFragmentActivity extends FragmentActivity{
        /* Your Tab host */
        private TabHost mTabHost;
    
        /* A HashMap of stacks, where we use tab identifier as keys..*/
        private HashMap<String, Stack<Fragment>> mStacks;
    
        /*Save current tabs identifier in this..*/
        private String mCurrentTab;
    
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.app_main_tab_fragment_layout);
    
            /*  
             *  Navigation stacks for each tab gets created.. 
             *  tab identifier is used as key to get respective stack for each tab
             */
            mStacks             =   new HashMap<String, Stack<Fragment>>();
            mStacks.put(AppConstants.TAB_A, new Stack<Fragment>());
            mStacks.put(AppConstants.TAB_B, new Stack<Fragment>());
    
            mTabHost                =   (TabHost)findViewById(android.R.id.tabhost);
            mTabHost.setOnTabChangedListener(listener);
            mTabHost.setup();
    
            initializeTabs();
        }
    
    
        private View createTabView(final int id) {
            View view = LayoutInflater.from(this).inflate(R.layout.tabs_icon, null);
            ImageView imageView =   (ImageView) view.findViewById(R.id.tab_icon);
            imageView.setImageDrawable(getResources().getDrawable(id));
            return view;
        }
    
        public void initializeTabs(){
            /* Setup your tab icons and content views.. Nothing special in this..*/
            TabHost.TabSpec spec    =   mTabHost.newTabSpec(AppConstants.TAB_A);
            mTabHost.setCurrentTab(-3);
            spec.setContent(new TabHost.TabContentFactory() {
                public View createTabContent(String tag) {
                    return findViewById(R.id.realtabcontent);
                }
            });
            spec.setIndicator(createTabView(R.drawable.tab_home_state_btn));
            mTabHost.addTab(spec);
    
    
            spec                    =   mTabHost.newTabSpec(AppConstants.TAB_B);
            spec.setContent(new TabHost.TabContentFactory() {
                public View createTabContent(String tag) {
                    return findViewById(R.id.realtabcontent);
                }
            });
            spec.setIndicator(createTabView(R.drawable.tab_status_state_btn));
            mTabHost.addTab(spec);
        }
    
    
        /*Comes here when user switch tab, or we do programmatically*/
        TabHost.OnTabChangeListener listener    =   new TabHost.OnTabChangeListener() {
          public void onTabChanged(String tabId) {
            /*Set current tab..*/
            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(AppConstants.TAB_A)){
                pushFragments(tabId, new AppTabAFirstFragment(), false,true);
              }else if(tabId.equals(AppConstants.TAB_B)){
                pushFragments(tabId, new AppTabBFirstFragment(), false,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,false);
            }
          }
        };
    
    
        /* Might be useful if we want to switch tab programmatically, from inside any of the fragment.*/
        public void setCurrentTab(int val){
              mTabHost.setCurrentTab(val);
        }
    
    
        /* 
         *      To add fragment to a tab. 
         *  tag             ->  Tab identifier
         *  fragment        ->  Fragment to show, in tab identified by tag
         *  shouldAnimate   ->  should animate transaction. false when we switch tabs, or adding first fragment to a tab
         *                      true when when we are pushing more fragment into navigation stack. 
         *  shouldAdd       ->  Should add to fragment navigation stack (mStacks.get(tag)). false when we are switching tabs (except for the first time)
         *                      true in all other cases.
         */
        public void pushFragments(String tag, Fragment fragment,boolean shouldAnimate, boolean shouldAdd){
          if(shouldAdd)
              mStacks.get(tag).push(fragment);
          FragmentManager   manager         =   getSupportFragmentManager();
          FragmentTransaction ft            =   manager.beginTransaction();
          if(shouldAnimate)
              ft.setCustomAnimations(R.anim.slide_in_right, R.anim.slide_out_left);
          ft.replace(R.id.realtabcontent, 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.setCustomAnimations(R.anim.slide_in_left, R.anim.slide_out_right);
          ft.replace(R.id.realtabcontent, 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;
            }
    
            /*  Each fragment represent a screen in application (at least in my requirement, just like an activity used to represent a screen). So if I want to do any particular action
             *  when back button is pressed, I can do that inside the fragment itself. For this I used AppBaseFragment, so that each fragment can override onBackPressed() or onActivityResult()
             *  kind of events, and activity can pass it to them. Make sure just do your non navigation (popping) logic in fragment, since popping of fragment is done here itself.
             */
            ((AppBaseFragment)mStacks.get(mCurrentTab).lastElement()).onBackPressed();
    
            /* Goto previous fragment in navigation stack of this tab */
                popFragments();
        }
    
    
        /*
         *   Imagine if you wanted to get an image selected using ImagePicker intent to the fragment. Ofcourse I could have created a public function
         *  in that fragment, and called it from the activity. But couldn't resist myself.
         */
        @Override
        protected void onActivityResult(int requestCode, int resultCode, Intent data) {
            if(mStacks.get(mCurrentTab).size() == 0){
                return;
            }
    
            /*Now current fragment on screen gets onActivityResult callback..*/
            mStacks.get(mCurrentTab).lastElement().onActivityResult(requestCode, resultCode, data);
        }
    }
    

    4. app_main_tab_fragment_layout.xml(万一有兴趣 . )

    <?xml version="1.0" encoding="utf-8"?>
    <TabHost
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@android:id/tabhost"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent">
    
        <LinearLayout
            android:orientation="vertical"
            android:layout_width="fill_parent"
            android:layout_height="fill_parent">
    
            <FrameLayout
                android:id="@android:id/tabcontent"
                android:layout_width="0dp"
                android:layout_height="0dp"
                android:layout_weight="0"/>
    
            <FrameLayout
                android:id="@+android:id/realtabcontent"
                android:layout_width="fill_parent"
                android:layout_height="0dp"
                android:layout_weight="1"/>
    
            <TabWidget
                android:id="@android:id/tabs"
                android:orientation="horizontal"
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                android:layout_weight="0"/>
    
        </LinearLayout>
    </TabHost>
    

    5. AppTabAFirstFragment.java(标签A中的第一个片段,适用于所有标签的simliar)

    public class AppTabAFragment extends BaseFragment {
        private Button mGotoButton;
    
        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container,
                Bundle savedInstanceState) {
            View view       =   inflater.inflate(R.layout.fragment_one_layout, container, false);
    
            mGoToButton =   (Button) view.findViewById(R.id.goto_button);
            mGoToButton.setOnClickListener(listener);
    
            return view;
        }
    
        private OnClickListener listener        =   new View.OnClickListener(){
            @Override
            public void onClick(View v){
                /* Go to next fragment in navigation stack*/
                mActivity.pushFragments(AppConstants.TAB_A, new AppTabAFragment2(),true,true);
            }
        }
    }
    

    这可能不是最完美和正确的方式 . 但它在我的案例中运作得很好 . 此外,我只在纵向模式下有此要求 . 我从来不必在支持两种方向的项目中使用此代码 . 所以不能说我面临什么样的挑战..

    编辑:

    如果有人想要一个完整的项目,我已将示例项目推送到github .

  • 6

    我们必须实现与您最近为应用描述的完全相同的行为 . 应用程序的屏幕和整体流程已经定义,因此我们必须坚持使用它(这是一个iOS应用程序克隆...) . 幸运的是,我们设法摆脱了屏幕上的后退按钮:)

    我们使用TabActivity,FragmentActivities(我们使用片段支持库)和片段的混合物来破解解决方案 . 回顾过去,我很确定这不是最好的架构决策,但我们设法让事情发挥作用 . 如果我不得不再次这样做,我可能会尝试做一个更基于活动的解决方案(没有片段),或尝试只有一个活动的标签,让所有其余的视图(我发现更多可重复使用而不是整体活动) .

    因此要求是在每个选项卡中都有一些选项卡和可嵌套屏幕:

    tab 1
      screen 1 -> screen 2 -> screen 3
    tab 2
      screen 4
    tab 3
      screen 5 -> 6
    

    等等...

    所以说:用户在标签1中开始,从屏幕1导航到屏幕2然后导航到屏幕3,然后他切换到标签3并从屏幕4导航到6;如果切换回标签1,他应该再次看到屏幕3,如果他按下了回来他应该回到屏幕2;再回来,他在屏幕1;切换到标签3,然后再次进入屏幕6 .

    应用程序中的主要Activity是MainTabActivity,它扩展了TabActivity . 每个选项卡都与一个活动相关联,比如说ActivityInTab1,2和3.然后每个屏幕都是一个片段:

    MainTabActivity
      ActivityInTab1
        Fragment1 -> Fragment2 -> Fragment3
      ActivityInTab2
        Fragment4
      ActivityInTab3
        Fragment5 -> Fragment6
    

    每个ActivityInTab一次只保存一个片段,并且知道如何将一个片段替换为另一个片段(与ActvityGroup几乎相同) . 很酷的是,通过这种方式为每个标签保留单独的后备堆很容易 .

    每个ActivityInTab的功能都完全相同:知道如何从一个片段导航到另一个片段并维护一个后台,所以我们将它放在一个基类中 . 我们简单地称它为ActivityInTab:

    abstract class ActivityInTab extends FragmentActivity { // FragmentActivity is just Activity for the support library.
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_in_tab);
        }
    
        /**
         * Navigates to a new fragment, which is added in the fragment container
         * view.
         * 
         * @param newFragment
         */
        protected void navigateTo(Fragment newFragment) {
            FragmentManager manager = getSupportFragmentManager();
            FragmentTransaction ft = manager.beginTransaction();
    
            ft.replace(R.id.content, newFragment);
    
            // Add this transaction to the back stack, so when the user presses back,
            // it rollbacks.
            ft.addToBackStack(null);
            ft.commit();
        }
    
    }
    

    activity_in_tab.xml就是这样的:

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/content"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:isScrollContainer="true">
    </RelativeLayout>
    

    如您所见,每个选项卡的视图布局都是相同的 . 那是因为它只是一个名为内容的FrameLayout,它将保存每个片段 . 片段是具有每个屏幕视图的片段 .

    只是为了奖励积分,我们还添加了一些小代码,以便在用户按下Back时显示确认对话框,并且没有更多片段可以返回:

    // In ActivityInTab.java...
    @Override
    public void onBackPressed() {
        FragmentManager manager = getSupportFragmentManager();
        if (manager.getBackStackEntryCount() > 0) {
            // If there are back-stack entries, leave the FragmentActivity
            // implementation take care of them.
            super.onBackPressed();
        } else {
            // Otherwise, ask user if he wants to leave :)
            showExitDialog();
        }
    }
    

    这就是设置 . 正如您所看到的,每个FragmentActivity(或者只是Android中的Activity> 3)都会使用它自己的FragmentManager来处理所有后向堆叠 .

    像ActivityInTab1这样的活动非常简单,它只会显示它的第一个片段(即屏幕):

    public class ActivityInTab1 extends ActivityInTab {
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            navigateTo(new Fragment1());
        }
    }
    

    然后,如果一个片段需要导航到另一个片段,它必须做一些讨厌的演员...但它并没有那么糟糕:

    // In Fragment1.java for example...
    // Need to navigate to Fragment2.
    ((ActivityIntab) getActivity()).navigateTo(new Fragment2());
    

    所以's pretty much it. I' m非常确定这不是一个非常规范(并且大多数肯定不是很好)的解决方案,所以如果你能指出一些链接或材料来解释哪种是Android方法来解决这个问题,我会很感激(制表符,标签中的嵌套屏幕等) . 请在评论中随意撕开这个答案:)

    作为这个解决方案不是很好的标志,最近我不得不在应用程序中添加一些导航功能 . 一些奇怪的按钮,应该将用户从一个选项卡带到另一个选项卡并进入嵌套屏幕 . 以编程方式执行此操作是一个痛苦的屁股,因为谁知道谁的问题和处理何时实际实例化和初始化的片段和活动 . 我认为如果这些屏幕和标签都只是视图真的会更容易 .


    最后,如果您需要更改方向更改,则使用setArguments / getArguments创建片段非常重要 . 如果在片段的构造函数中设置实例变量,则会被搞砸 . 但幸运的是,这很容易解决:只需在构造函数中的setArguments中保存所有内容,然后在onCreate中使用getArguments检索这些内容以使用它们 .

  • 5

    存储对片段的强引用不是正确的方法 .

    FragmentManager提供putFragment(Bundle, String, Fragment)saveFragmentInstanceState(Fragment) .

    任何一个都足以实现一个后端堆栈 .


    使用 putFragment ,而不是替换片段,分离旧片段并添加新片段 . 这就是框架对替换添加到backstack的事务所做的工作 . putFragment 存储当前活动碎片列表的索引,并且这些碎片在方向更改期间由框架保存 .

    第二种方法,使用 saveFragmentInstanceState ,将整个片段状态保存到Bundle,允许您真正删除它,而不是分离 . 使用这种方法可以更容易地操作后堆栈,因为您可以随时弹出Fragment .


    我在这个用例中使用了第二种方法:

    SignInFragment ----> SignUpFragment ---> ChooseBTDeviceFragment
                   \                          /
                    \------------------------/
    

    我不希望用户通过按后退按钮从第三个屏幕返回到“注册”屏幕 . 我也在他们之间翻转动画(使用 onCreateAnimation ),所以hacky解决方案将无法工作,至少没有用户明确注意到某些事情是不对的 .

    这是自定义backstack的有效用例,执行用户期望的...

    private static final String STATE_BACKSTACK = "SetupActivity.STATE_BACKSTACK";
    
    private MyBackStack mBackStack;
    
    @Override
    protected void onCreate(Bundle state) {
        super.onCreate(state);
    
        if (state == null) {
            mBackStack = new MyBackStack();
    
            FragmentManager fm = getSupportFragmentManager();
            FragmentTransaction tr = fm.beginTransaction();
            tr.add(R.id.act_base_frg_container, new SignInFragment());
            tr.commit();
        } else {
            mBackStack = state.getParcelable(STATE_BACKSTACK);
        }
    }
    
    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putParcelable(STATE_BACKSTACK, mBackStack);
    }
    
    private void showFragment(Fragment frg, boolean addOldToBackStack) {
        final FragmentManager fm = getSupportFragmentManager();
        final Fragment oldFrg = fm.findFragmentById(R.id.act_base_frg_container);
    
        FragmentTransaction tr = fm.beginTransaction();
        tr.replace(R.id.act_base_frg_container, frg);
        // This is async, the fragment will only be removed after this returns
        tr.commit();
    
        if (addOldToBackStack) {
            mBackStack.push(fm, oldFrg);
        }
    }
    
    @Override
    public void onBackPressed() {
        MyBackStackEntry entry;
        if ((entry = mBackStack.pop()) != null) {
            Fragment frg = entry.recreate(this);
    
            FragmentManager fm = getSupportFragmentManager();
            FragmentTransaction tr = fm.beginTransaction();
            tr.replace(R.id.act_base_frg_container, frg);
            tr.commit();
    
            // Pop it now, like the framework implementation.
            fm.executePendingTransactions();
        } else {
            super.onBackPressed();
        }
    }
    

    public class MyBackStack implements Parcelable {
    
        private final List<MyBackStackEntry> mList;
    
        public MyBackStack() {
            mList = new ArrayList<MyBackStackEntry>(4);
        }
    
        public void push(FragmentManager fm, Fragment frg) {
            push(MyBackStackEntry.newEntry(fm, frg);
        }
    
        public void push(MyBackStackEntry entry) {
            if (entry == null) {
                throw new NullPointerException();
            }
            mList.add(entry);
        }
    
        public MyBackStackEntry pop() {
            int idx = mList.size() - 1;
            return (idx != -1) ? mList.remove(idx) : null;
        }
    
        @Override
        public int describeContents() {
            return 0;
        }
    
        @Override
        public void writeToParcel(Parcel dest, int flags) {
            final int len = mList.size();
            dest.writeInt(len);
            for (int i = 0; i < len; i++) {
                // MyBackStackEntry's class is final, theres no
                // need to use writeParcelable
                mList.get(i).writeToParcel(dest, flags);
            }
        }
    
        protected MyBackStack(Parcel in) {
            int len = in.readInt();
            List<MyBackStackEntry> list = new ArrayList<MyBackStackEntry>(len);
            for (int i = 0; i < len; i++) {
                list.add(MyBackStackEntry.CREATOR.createFromParcel(in));
            }
            mList = list;
        }
    
        public static final Parcelable.Creator<MyBackStack> CREATOR =
            new Parcelable.Creator<MyBackStack>() {
    
                @Override
                public MyBackStack createFromParcel(Parcel in) {
                    return new MyBackStack(in);
                }
    
                @Override
                public MyBackStack[] newArray(int size) {
                    return new MyBackStack[size];
                }
        };
    }
    

    public final class MyBackStackEntry implements Parcelable {
    
        public final String fname;
        public final Fragment.SavedState state;
        public final Bundle arguments;
    
        public MyBackStackEntry(String clazz, 
                Fragment.SavedState state,
                Bundle args) {
            this.fname = clazz;
            this.state = state;
            this.arguments = args;
        }
    
        public static MyBackStackEntry newEntry(FragmentManager fm, Fragment frg) {
            final Fragment.SavedState state = fm.saveFragmentInstanceState(frg);
            final String name = frg.getClass().getName();
            final Bundle args = frg.getArguments();
            return new MyBackStackEntry(name, state, args);
        }
    
        public Fragment recreate(Context ctx) {
            Fragment frg = Fragment.instantiate(ctx, fname);
            frg.setInitialSavedState(state);
            frg.setArguments(arguments);
            return frg;
        }
    
        @Override
        public int describeContents() {
            return 0;
        }
    
        @Override
        public void writeToParcel(Parcel dest, int flags) {
            dest.writeString(fname);
            dest.writeBundle(arguments);
    
            if (state == null) {
                dest.writeInt(-1);
            } else if (state.getClass() == Fragment.SavedState.class) {
                dest.writeInt(0);
                state.writeToParcel(dest, flags);
            } else {
                dest.writeInt(1);
                dest.writeParcelable(state, flags);
            }
        }
    
        protected MyBackStackEntry(Parcel in) {
            final ClassLoader loader = getClass().getClassLoader();
            fname = in.readString();
            arguments = in.readBundle(loader);
    
            switch (in.readInt()) {
                case -1:
                    state = null;
                    break;
                case 0:
                    state = Fragment.SavedState.CREATOR.createFromParcel(in);
                    break;
                case 1:
                    state = in.readParcelable(loader);
                    break;
                default:
                    throw new IllegalStateException();
            }
        }
    
        public static final Parcelable.Creator<MyBackStackEntry> CREATOR =
            new Parcelable.Creator<MyBackStackEntry>() {
    
                @Override
                public MyBackStackEntry createFromParcel(Parcel in) {
                    return new MyBackStackEntry(in);
                }
    
                @Override
                public MyBackStackEntry[] newArray(int size) {
                    return new MyBackStackEntry[size];
                }
        };
    }
    
  • 2

    使用ChildFragmentManager可以轻松实现这一点

    这是与相关项目有关的帖子 . 看一看,

    http://tausiq.wordpress.com/2014/06/06/android-multiple-fragments-stack-in-each-viewpager-tab/

  • -1

    免责声明:


    我觉得这是发布一个相关解决方案的最佳位置,我已经研究过类似的问题,似乎是非常标准的Android内容 . 它不会为每个人解决问题,但它可能有所帮助 .


    如果是主要的您的片段之间的差异只是备份它们的数据(即,没有很多大的布局差异),那么您可能不需要实际替换片段,而只需交换基础数据并刷新视图 .

    以下是此方法的一个可能示例的说明:

    我有一个使用ListViews的应用程序 . 列表中的每个项目都是具有一定数量子项的父项 . 点击该项目时,需要在与原始列表相同的ActionBar选项卡中打开这些子项的新列表 . 这些嵌套列表具有非常相似的布局(这里或那里可能有一些条件调整),但数据不同 .

    这个应用程序在初始父列表下面有几层后代,当用户尝试访问第一个以外的任何特定深度时,我们可能有也可能没有来自服务器的数据 . 因为列表是从数据库游标构造的,并且片段使用游标加载器和游标适配器用列表项填充列表视图,所以在注册点击时需要发生的是:

    1)使用适当的“to”和“from”字段创建一个新的适配器,该字段将匹配添加到列表中的新项目视图和新游标返回的列 .

    2)将此适配器设置为ListView的新适配器 .

    3)根据单击的项构建新URI,并使用新URI(和投影)重新启动游标加载器 . 在此示例中,URI通过从UI传递的选择args映射到特定查询 .

    4)当从URI加载新数据时,将与适配器关联的光标交换到新光标,然后列表将刷新 .

    由于我们没有使用事务,因此没有与此相关的backstack,因此您必须构建自己的事务,或者在退出层次结构时反向播放查询 . 当我尝试这个时,查询速度足够快,我只是在oNBackPressed()中再次执行它们直到我处于层次结构的顶部,此时框架再次接管后退按钮 .

    如果您发现自己处于类似情况,请务必阅读文档:http://developer.android.com/guide/topics/ui/layout/listview.html

    http://developer.android.com/reference/android/support/v4/app/LoaderManager.LoaderCallbacks.html

    我希望这可以帮助别人!

  • 2

    我有完全相同的问题,并实现了一个开源github项目,涵盖堆叠选项卡,后退和向上导航,并经过充分测试和记录:

    https://github.com/SebastianBaltesObjectCode/PersistentFragmentTabs

    这是一个简单的小框架,用于导航选项卡和片段切换以及上下导航的处理 . 每个选项卡都有自己的一堆片段 . 它使用ActionBarSherlock并与API级别8兼容 .

  • 23

    我想建议我自己的解决方案,以防有人正在寻找并想要尝试为他/她的需求选择最好的解决方案 .

    https://github.com/drusak/tabactivity

    创建库的目的非常平庸 - 像iPhone一样实现它 .

    主要优点:

    • 使用带有TabLayout的android.support.design库;

    • 每个选项卡都有自己的堆栈使用FragmentManager(不保存片段的引用);

    • 支持深层链接(当你需要打开特定标签和特定片段的级别时);

    • 保存/恢复选项卡的状态;

    • 标签中片段的自适应生命周期方法;

    • 非常容易实现满足您的需求 .

    希望它可以帮助某人 .

    谢谢!

  • -1

    这是一个复杂的问题,因为Android只处理1个后栈,但这是可行的 . 我花了几天时间创建一个名为Tab Stacker的库,它完全符合您的要求:每个选项卡的片段历史记录 . 它是开源的,完整记录,可以轻松地与gradle包含在内 . 你可以在github上找到这个库:https://github.com/smart-fun/TabStacker

    您还可以下载示例应用程序,以查看该行为是否符合您的需求:

    https://play.google.com/apps/testing/fr.arnaudguyon.tabstackerapp

    如果您有任何疑问,请随时发送邮件 .

  • 2

    简单的解决方案:

    每次更改选项卡/根视图调用时:

    fragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
    

    它将清除BackStack . 请记住在更改根片段之前调用此方法 .

    并添加片段:

    FragmentTransaction transaction = getFragmentManager().beginTransaction();
    NewsDetailsFragment newsDetailsFragment = NewsDetailsFragment.newInstance(newsId);
    transaction.add(R.id.content_frame, newsDetailsFragment).addToBackStack(null).commit();
    

    注意 .addToBackStack(null)transaction.add 可以例如用 transaction.replace 改变 .

  • 93

    这个帖子非常有趣和有用 .
    感谢Krishnabhadra的解释和代码,我使用你的代码并进行了一些改进,允许从更改配置(主要是旋转)中保留堆栈,currentTab等 .
    在真正的4.0.4和2.3.6设备上测试,未在模拟器上测试

    我在“AppMainTabActivity.java”上更改了这部分代码,其余部分保持不变 . 也许Krishnabhadra会加上这个在他的代码上 .

    恢复关于创建的数据:

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.app_main_tab_fragment_layout);
    
        /*  
         *  Navigation stacks for each tab gets created..
         *  tab identifier is used as key to get respective stack for each tab
         */
    
      //if we are recreating this activity...
        if (savedInstanceState!=null) {
             mStacks = (HashMap<String, Stack<Fragment>>) savedInstanceState.get("stack");
             mCurrentTab = savedInstanceState.getString("currentTab");
        }
        else {
        mStacks = new HashMap<String, Stack<Fragment>>();
        mStacks.put(AppConstants.TAB_A, new Stack<Fragment>());
        mStacks.put(AppConstants.TAB_B, new Stack<Fragment>());
    
        }
    
        mTabHost = (TabHost)findViewById(android.R.id.tabhost);
        mTabHost.setup();
    
        initializeTabs();
    
      //set the listener the last, to avoid overwrite mCurrentTab everytime we add a new Tab
        mTabHost.setOnTabChangedListener(listener);
    }
    

    保存变量并放入Bundle:

    //Save variables while recreating
    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putSerializable("stack", mStacks);
        outState.putString("currentTab", mCurrentTab);
        //outState.putInt("tabHost",mTabHost);
    }
    

    如果存在以前的CurrentTab,请设置此项,否则创建一个新的Tab_A:

    public void initializeTabs(){
        /* Setup your tab icons and content views.. Nothing special in this..*/
        TabHost.TabSpec spec    =   mTabHost.newTabSpec(AppConstants.TAB_A);
    
        spec.setContent(new TabHost.TabContentFactory() {
            public View createTabContent(String tag) {
                return findViewById(R.id.realtabcontent);
            }
        });
        spec.setIndicator(createTabView(R.drawable.tab_a_state_btn));
        mTabHost.addTab(spec);
    
    
        spec                    =   mTabHost.newTabSpec(AppConstants.TAB_B);
        spec.setContent(new TabHost.TabContentFactory() {
            public View createTabContent(String tag) {
                return findViewById(R.id.realtabcontent);
            }
        });
        spec.setIndicator(createTabView(R.drawable.tab_b_state_btn));
        mTabHost.addTab(spec);
    
    //if we have non default Tab as current, change it
        if (mCurrentTab!=null) {
            mTabHost.setCurrentTabByTag(mCurrentTab);
        } else {
            mCurrentTab=AppConstants.TAB_A;
            pushFragments(AppConstants.TAB_A, new AppTabAFirstFragment(), false,true);
        }
    }
    

    我希望这有助于其他人 .

  • 1

    我建议不要使用基于HashMap的backstack>“不要保持活动”模式中有很多错误 . 如果您深入到片段的堆栈中,它将无法正确恢复状态 . 并且还将嵌套在嵌套的 Map 片段中(使用exeption:Fragment没有为ID找到视图) . Coz HashMap>在background \ foreground app之后将为null

    我使用fragment的backstack优化上面的代码

    It is bottom TabView

    Main activity Class

    import android.app.Activity;
    import android.app.Fragment;
    import android.app.FragmentManager;
    import android.app.FragmentTransaction;
    import android.content.Intent;
    import android.os.Bundle;
    import android.view.LayoutInflater;
    import android.view.View;
    import android.view.Window;
    import android.widget.ImageView;
    import android.widget.TabHost;
    import android.widget.TextView;
    
    import com.strikersoft.nida.R;
    import com.strikersoft.nida.abstractActivity.BaseActivity;
    import com.strikersoft.nida.screens.tags.mapTab.MapContainerFragment;
    import com.strikersoft.nida.screens.tags.searchTab.SearchFragment;
    import com.strikersoft.nida.screens.tags.settingsTab.SettingsFragment;
    
    public class TagsActivity extends BaseActivity {
        public static final String M_CURRENT_TAB = "M_CURRENT_TAB";
        private TabHost mTabHost;
        private String mCurrentTab;
    
        public static final String TAB_TAGS = "TAB_TAGS";
        public static final String TAB_MAP = "TAB_MAP";
        public static final String TAB_SETTINGS = "TAB_SETTINGS";
    
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            getWindow().requestFeature(Window.FEATURE_ACTION_BAR);
            getActionBar().hide();
            setContentView(R.layout.tags_activity);
    
            mTabHost = (TabHost) findViewById(android.R.id.tabhost);
    
            mTabHost.setup();
    
            if (savedInstanceState != null) {
                mCurrentTab = savedInstanceState.getString(M_CURRENT_TAB);
                initializeTabs();
                mTabHost.setCurrentTabByTag(mCurrentTab);
                /*
                when resume state it's important to set listener after initializeTabs
                */
                mTabHost.setOnTabChangedListener(listener);
            } else {
                mTabHost.setOnTabChangedListener(listener);
                initializeTabs();
            }
        }
    
        private View createTabView(final int id, final String text) {
            View view = LayoutInflater.from(this).inflate(R.layout.tabs_icon, null);
            ImageView imageView = (ImageView) view.findViewById(R.id.tab_icon);
            imageView.setImageDrawable(getResources().getDrawable(id));
            TextView textView = (TextView) view.findViewById(R.id.tab_text);
            textView.setText(text);
            return view;
        }
    
        /*
        create 3 tabs with name and image
        and add it to TabHost
         */
        public void initializeTabs() {
    
            TabHost.TabSpec spec;
    
            spec = mTabHost.newTabSpec(TAB_TAGS);
            spec.setContent(new TabHost.TabContentFactory() {
                public View createTabContent(String tag) {
                    return findViewById(R.id.realtabcontent);
                }
            });
            spec.setIndicator(createTabView(R.drawable.tab_tag_drawable, getString(R.string.tab_tags)));
            mTabHost.addTab(spec);
    
            spec = mTabHost.newTabSpec(TAB_MAP);
            spec.setContent(new TabHost.TabContentFactory() {
                public View createTabContent(String tag) {
                    return findViewById(R.id.realtabcontent);
                }
            });
            spec.setIndicator(createTabView(R.drawable.tab_map_drawable, getString(R.string.tab_map)));
            mTabHost.addTab(spec);
    
    
            spec = mTabHost.newTabSpec(TAB_SETTINGS);
            spec.setContent(new TabHost.TabContentFactory() {
                public View createTabContent(String tag) {
                    return findViewById(R.id.realtabcontent);
                }
            });
            spec.setIndicator(createTabView(R.drawable.tab_settings_drawable, getString(R.string.tab_settings)));
            mTabHost.addTab(spec);
    
        }
    
        /*
        first time listener will be trigered immediatelly after first: mTabHost.addTab(spec);
        for set correct Tab in setmTabHost.setCurrentTabByTag ignore first call of listener
        */
        TabHost.OnTabChangeListener listener = new TabHost.OnTabChangeListener() {
            public void onTabChanged(String tabId) {
    
                mCurrentTab = tabId;
    
                if (tabId.equals(TAB_TAGS)) {
                    pushFragments(SearchFragment.getInstance(), false,
                            false, null);
                } else if (tabId.equals(TAB_MAP)) {
                    pushFragments(MapContainerFragment.getInstance(), false,
                            false, null);
                } else if (tabId.equals(TAB_SETTINGS)) {
                    pushFragments(SettingsFragment.getInstance(), false,
                            false, null);
                }
    
            }
        };
    
    /*
    Example of starting nested fragment from another fragment:
    
    Fragment newFragment = ManagerTagFragment.newInstance(tag.getMac());
                    TagsActivity tAct = (TagsActivity)getActivity();
                    tAct.pushFragments(newFragment, true, true, null);
     */
        public void pushFragments(Fragment fragment,
                                  boolean shouldAnimate, boolean shouldAdd, String tag) {
            FragmentManager manager = getFragmentManager();
            FragmentTransaction ft = manager.beginTransaction();
            if (shouldAnimate) {
                ft.setCustomAnimations(R.animator.fragment_slide_left_enter,
                        R.animator.fragment_slide_left_exit,
                        R.animator.fragment_slide_right_enter,
                        R.animator.fragment_slide_right_exit);
            }
            ft.replace(R.id.realtabcontent, fragment, tag);
    
            if (shouldAdd) {
                /*
                here you can create named backstack for realize another logic.
                ft.addToBackStack("name of your backstack");
                 */
                ft.addToBackStack(null);
            } else {
                /*
                and remove named backstack:
                manager.popBackStack("name of your backstack", FragmentManager.POP_BACK_STACK_INCLUSIVE);
                or remove whole:
                manager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
                 */
                manager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
            }
            ft.commit();
        }
    
        /*
        If you want to start this activity from another
         */
        public static void startUrself(Activity context) {
            Intent newActivity = new Intent(context, TagsActivity.class);
            newActivity.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            context.startActivity(newActivity);
            context.finish();
        }
    
        @Override
        public void onSaveInstanceState(Bundle outState) {
            outState.putString(M_CURRENT_TAB, mCurrentTab);
            super.onSaveInstanceState(outState);
        }
    
        @Override
        public void onBackPressed(){
            super.onBackPressed();
        }
    }
    

    tags_activity.xml

    <

    ?xml version="1.0" encoding="utf-8"?>
    <TabHost
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@android:id/tabhost"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    
        <LinearLayout
            android:orientation="vertical"
            android:layout_width="match_parent"
            android:layout_height="match_parent">
    
            <FrameLayout
                android:id="@android:id/tabcontent"
                android:layout_width="0dp"
                android:layout_height="0dp"
                android:layout_weight="0"/>
            <FrameLayout
                android:id="@+android:id/realtabcontent"
                android:background="@drawable/bg_main_app_gradient"
                android:layout_width="match_parent"
                android:layout_height="0dp"
                android:layout_weight="1"/>
            <TabWidget
                android:id="@android:id/tabs"
                android:background="#EAE7E1"
                android:orientation="horizontal"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_weight="0"/>
        </LinearLayout>
    </TabHost>
    

    tags_icon.xml

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:id="@+id/tabsLayout"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:background="@drawable/bg_tab_gradient"
        android:gravity="center"
        android:orientation="vertical"
        tools:ignore="contentDescription" >
    
        <ImageView
            android:id="@+id/tab_icon"
            android:layout_marginTop="4dp"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
        <TextView 
            android:id="@+id/tab_text"
            android:layout_marginBottom="3dp"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textColor="@color/tab_text_color"/>
    
    </LinearLayout>
    

    enter image description here

相关问题