Отдельный задний стек для каждой вкладки на Android с помощью фрагментов

Я пытаюсь реализовать вкладки для навигации в приложении для Android. Поскольку TabActivity и ActivityGroup устарели, я хотел бы реализовать его, используя Fragments.

Я знаю, как настроить один фрагмент для каждой вкладки, а затем переключать фрагменты при нажатии на вкладку. Но как я могу иметь отдельный стек для каждой вкладки?

Для примера Фрагменты A и B будут находиться в разделе Tab 1 и Fragment C и D в разделе Tab 2. Когда приложение запущено, отображается фрагмент A и выбран Tab 1. Затем фрагмент A может быть заменен фрагментом B. Когда выбрана вкладка 2 Фрагмент C должен отображаться. Если выбрана вкладка 1, фрагмент B должен снова отображаться. На этом этапе можно будет использовать кнопку "Назад" для отображения фрагмента А.

Кроме того, важно, чтобы состояние для каждой вкладки поддерживалось, когда устройство повернуто.

BR Мартин

Ответ 1

В настоящее время структура не будет автоматически сделана для вас. Вам нужно будет создавать и управлять своими задними стеками для каждой вкладки.

Честно говоря, это похоже на действительно сомнительную вещь. Я не могу себе представить, что это приведет к приличному интерфейсу - если задний ключ будет делать разные вещи в зависимости от вкладки, которую я есть, особенно если у задней клавиши также есть нормальное поведение закрытия всего действия, когда наверху стек... звучит неприятно.

Если вы пытаетесь создать что-то вроде пользовательского интерфейса веб-браузера, чтобы получить UX, который является естественным для пользователя, он будет включать в себя множество тонких настроек поведения в зависимости от контекста, поэтому вам обязательно нужно будет сделать свой вместо того, чтобы полагаться на некоторую реализацию по умолчанию в рамках. В качестве примера попробуйте обратить внимание на то, как обратный ключ взаимодействует со стандартным браузером различными способами, в которые вы можете войти и выйти из него. (Каждое "окно" в браузере по существу является вкладкой.)

Ответ 2

Я очень опаздываю на этот вопрос. Но поскольку эта тема была очень информативной и полезной для меня, я подумал, что лучше разместить здесь свои два пенса.

Мне нужен поток экрана, подобный этому (минималистический дизайн с 2 вкладками и 2 представлениями на каждой вкладке),

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

У меня были те же требования в прошлом, и я сделал это, используя TabActivityGroup (который тоже был устаревшим) и Activities. На этот раз я хотел использовать Фрагменты.

Так вот как я это сделал.

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){
    }
}

Все фрагменты в вашем приложении могут расширить этот базовый класс. Если вы хотите использовать специальные фрагменты, такие как 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. Ok, Main Tab Activity - Пожалуйста, просмотрите комментарии в коде..

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 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>

пять. AppTabAFirstFragment.java(Первый фрагмент в Tab 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);
        }
    }
}

Это может быть не самый отполированный и правильный путь. Но в моем случае это прекрасно работало. Также у меня было это требование только в портретном режиме. Мне никогда не приходилось использовать этот код в проекте, поддерживающем обе ориентации. Поэтому я не могу сказать, какие проблемы я вижу там.

EDIT:

Если кому-то нужен полный проект, я подталкивал образец проекта github.

Ответ 3

Нам пришлось реализовать именно то же поведение, которое вы описываете для приложения в последнее время. Экран и общий поток приложения уже были определены, поэтому нам пришлось придерживаться его (это клон приложения 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.

Основной актив в приложении - 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 называется содержимым, которое будет содержать каждый фрагмент. Фрагменты - это те, которые имеют каждый вид экрана.

Только для бонусных очков мы также добавили немного кода, чтобы отобразить диалог подтверждения, когда пользователь нажимает "Назад", и больше нет фрагментов, чтобы вернуться к:

// 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 (или просто Activity в Android > 3) заботится обо всех back-stacking с собственным 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());

Так что в значительной степени это. Я уверен, что это не очень каноническое решение (и в основном это не очень хорошее), поэтому я хотел бы спросить опытных разработчиков Android, что было бы лучшим подходом к достижению этой функциональности, и если это не так, сделанный "в Android, я был бы признателен, если бы вы могли указать мне на какую-то ссылку или материал, объясняющий, какой способ Android подходит для этого (вкладки, вложенные экраны в вкладках и т.д.). Не стесняйтесь оторвать этот ответ в комментариях:)

В знак того, что это решение не очень хорошо, недавно мне пришлось добавить в приложение некоторые функции навигации. Некоторая причудливая кнопка, которая должна выводить пользователя с одной вкладки на другую и на вложенный экран. Делать это программно было болью в прикладе, из-за того, кто-знает, кто проблемы и когда дело касается фрагментов и действий, фактически созданных и инициализированных. Я думаю, что было бы намного проще, если бы эти экраны и вкладки были всего лишь видами на самом деле.


Наконец, если вам нужно пережить изменения ориентации, важно, чтобы ваши фрагменты создавались с помощью setArguments/getArguments. Если вы зададите переменные экземпляра в конструкторах ваших фрагментов, вы будете ввернуты. Но, к счастью, это очень легко исправить: просто сохраните все в setArguments в конструкторе, а затем извлеките эти вещи с помощью getArguments в onCreate, чтобы использовать их.

Ответ 4

Сохранение сильных ссылок на фрагменты не является правильным способом.

FragmentManager предоставляет putFragment(Bundle, String, Fragment) и saveFragmentInstanceState(Fragment),

Либо одного достаточно, чтобы реализовать backstack.


Используя putFragment, вместо замены фрагмента вы отделите старый и добавьте новый. Это то, что делает фреймворк для замены транзакции, которая добавляется в backstack. putFragment хранит индекс в текущем списке активных фрагментов, и эти фрагменты сохраняются каркасом во время изменений ориентации.

Второй способ, используя saveFragmentInstanceState, сохраняет полное состояние фрагмента в Bundle, позволяя вам действительно удалить его, а не отсоединять. Использование этого подхода упрощает управление задним стеком, поскольку вы можете поместить фрагмент, когда захотите.


Я использовал второй метод для этой утилиты:

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

Я не хочу, чтобы пользователь возвращался на экран регистрации, начиная с третьего, нажав кнопку "Назад". Я также делаю флип анимации между ними (используя onCreateAnimation), поэтому взломанные решения не будут работать, по крайней мере, без того, чтобы пользователь явно заметил, что что-то не так.

Это допустимый прецедент для пользовательской блокировки, выполняющий то, что ожидает пользователь...

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 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];
            }
    };
}

Ответ 6

Отказ от ответственности:


Я считаю, что это лучшее место для публикации связанного с ним решения, над которым я работал, для аналогичного типа проблем, который кажется довольно стандартным для Android. Это не решит проблему для всех, но это может помочь некоторым.


Если основное различие между вашими фрагментами - это только резервное копирование данных (т.е. не много больших различий в макетах), вам может не понадобиться фактически заменить фрагмент, а просто заменить базовые данные и обновить вид.

Здесь приведено описание одного из возможных примеров такого подхода:

У меня есть приложение, которое использует ListViews. Каждый элемент в списке является родителем с некоторым количеством детей. Когда вы нажимаете на элемент, новый список должен открываться с этими дочерними элементами на той же вкладке ActionBar, что и в исходном списке. Эти вложенные списки имеют очень похожий макет (некоторые условные настройки здесь и там возможно), но данные разные.

Это приложение имеет несколько слоев потомства под первоначальным родительским списком, и мы можем или не можем иметь данные с сервера к тому времени, когда пользователь попытается получить доступ к любой определенной глубине за пределами первой. Поскольку список создается из курсора базы данных, а в фрагментах используется курсор-загрузчик и адаптер курсора для заполнения списка с элементами списка, все, что должно произойти при клике:

1) Создайте новый адаптер с соответствующими полями "Кому" и "От", которые будут соответствовать новым представлениям элементов, добавляемым в список, и столбцам, возвращаемым новым курсором.

2) Установите этот адаптер в качестве нового адаптера для ListView.

3) Создайте новый URI на основе элемента, который был нажат, и перезапустите загрузчик курсора с новым URI (и проекцией). В этом примере URI сопоставляется с конкретными запросами с аргументами выбора, переданными из пользовательского интерфейса.

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

Надеюсь, это поможет кому-то!

Ответ 7

У меня была точно такая же проблема, и я реализовал проект github с открытым исходным кодом, который охватывает сложную вкладку, назад и вверх по навигации и хорошо протестирован и документирован:

https://github.com/SebastianBaltesObjectCode/PersistentFragmentTabs

Это простая и малая структура для вкладок навигации и переключения фрагментов и обработки навигации вверх и назад. Каждая вкладка имеет свой собственный стек фрагментов. Он использует ActionBarSherlock и совместим с уровнем API 8.

Ответ 8

Я хотел бы предложить свое собственное решение на тот случай, если кто-то ищет и хочет попробовать выбрать лучшее для его/ее нужд.

https://github.com/drusak/tabactivity

Цель создания библиотеки довольно банальна - реализовать ее как iPhone.

Основные преимущества:

  • использовать библиотеку android.support.design с TabLayout;
  • каждая вкладка имеет свой собственный стек с использованием FragmentManager (без сохранения ссылок на фрагменты);
  • поддержка глубоких ссылок (когда вам нужно открыть определенную вкладку и определенный уровень фрагмента в ней);
  • сохранение/восстановление состояний вкладок;
  • адаптивные методы жизненного цикла фрагментов во вкладках;
  • довольно легко реализовать для ваших нужд.

Ответ 9

Это сложная проблема, поскольку Android обрабатывает только один задний стек, но это возможно. Мне потребовалось несколько дней, чтобы создать библиотеку под названием Tab Stacker, которая делает именно то, что вы ищете: историю фрагментов для каждой вкладки. Он является открытым исходным кодом и полностью документирован, и его можно легко включить с помощью gradle. Вы можете найти библиотеку на github: https://github.com/smart-fun/TabStacker

Вы также можете загрузить пример приложения, чтобы увидеть, что поведение соответствует вашим потребностям:

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

Если у вас есть какие-либо вопросы, не стесняйтесь бросить почту.

Ответ 10

Простое решение:

Каждый раз, когда вы меняете вызов вкладки/корневого представления:

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.

Ответ 11

Эта тема была очень интересной и полезной.
Благодаря Krishnabhadra для вашего объяснения и кода, я использую ваш код и немного улучшаю, позволяя сохранять стеки, currentTab и т.д. Из конфигурации изменений (в основном, вращающихся).
Протестировано на реальных устройствах 4.0.4 и 2.3.6, не протестированных на эмуляторе

Я изменяю эту часть кода на "AppMainTabActivity.java", остальные остаются неизменными. Возможно, Кришнабхадра добавит это в свой код.

Восстановить данные по созданию:

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);
    }
}

Я надеюсь, что это поможет другим людям.

Ответ 12

Я бы порекомендовал не использовать backstack на основе HashMap > есть много ошибок в режиме "не продолжать". Он не будет правильно восстанавливать состояние, если вы глубоко в стеке фрагментов. А также будет трепетать в вложенном фрагменте карты (с exeption: Fragment no view found for ID). Coz HashMap > после фонового\фонового приложения будет null

Я оптимизирую код выше для работы с фрагментом backstack

Это внизу TabView

Основной класс деятельности

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 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