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

158

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

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

Например, фрагменты A и B будут находиться под вкладкой 1, а фрагменты C и D - под вкладкой 2. При запуске приложения отображается фрагмент A и выбирается вкладка 1. Затем фрагмент A можно заменить фрагментом B. Когда выбрана вкладка 2, должен отображаться фрагмент C. Если выбрана вкладка 1, фрагмент B должен снова отображаться. На этом этапе должна быть возможность использовать кнопку «Назад» для отображения фрагмента А.

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

BR Martin

mardah
источник

Ответы:

23

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

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

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

hackbod
источник
7
Не делай этого. И рамки вряд ли бесполезны. Это не дает вам автоматической поддержки для такого рода вещей, которые, как я сказал, я не могу себе представить, приводящие к достойному взаимодействию с пользователем, за исключением очень специализированных ситуаций, где вам все равно придется тщательно контролировать поведение спины.
hackbod
9
Этот тип навигации, то есть вкладки и иерархия страниц на каждой вкладке очень распространен для приложений iPhone, например (вы можете проверить App Store и приложения iPod). Я нахожу их пользовательский опыт вполне приличным.
Дмитрий Рядненко
13
Это безумие. У iPhone даже нет кнопки возврата. Есть демонстрации API, показывающие очень простой код для реализации фрагментов во вкладках. Был задан вопрос о наличии разных стеков бэк-стека для каждой вкладки, и я отвечаю, что фреймворк не предоставляет это автоматически, потому что семантически для того, что делает кнопка «назад», это, скорее всего, будет дурацким опытом пользователя. Вы можете довольно легко реализовать обратную семантику самостоятельно, если хотите.
hackbod
4
Опять же, у iPhone нет кнопки «назад», поэтому он не имеет семантически такого поведения, как Android. Кроме того, «лучше просто придерживаться действий и сэкономить много времени» здесь не имеет никакого смысла, потому что действия не позволяют размещать вкладки поддержки в пользовательском интерфейсе с их собственными различными задними стеками; на самом деле управление задним стеком менее гибкое, чем то, что обеспечивается инфраструктурой фрагментов.
hackbod
22
@hackbod Я пытаюсь следовать вашим соображениям, но у меня возникли проблемы с реализацией собственного поведения в стеке. Я понимаю, что, приняв участие в разработке этого, вы бы имели полное представление о том, как легко это может быть. Возможно ли, что есть демо-приложение для варианта использования OP, поскольку это действительно очень распространенная ситуация, особенно для тех из нас, кому приходится писать и переносить приложения iOS для клиентов, которые делают эти запросы .... управление отдельными backstacks фрагмента в пределах каждого FragmentActivity.
Ричард Ле Мезурье
138

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

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

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

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

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

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. Создайте несколько идентификаторов табуляции, доступных везде в проекте

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, аналог для всех вкладок)

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 .

Krishnabhadra
источник
2
Хранение данных для каждого фрагмента, воссоздание каждого из них, восстановление стеков ... так много работы для простого изменения ориентации.
Майкл Эйлерс Смит
3
@omegatai полностью согласен с вами .. Вся проблема возникает из-за того, что Android не управляет стеком для нас ( что делает iOS и изменение ориентации или вкладка с несколькими фрагментами - это очень просто ), и это возвращает нас к первоначальному обсуждению в этом Q / Нить. Нет
смысла
1
@Renjith Это потому, что фрагмент воссоздается каждый раз, когда вы переключаете вкладку. Не думайте даже, что ваш фрагмент повторно используется через переключатель табуляции. когда я переключаюсь с вкладки A на B, вкладка A освобождается из памяти. Поэтому сохраняйте свои данные в активности и каждый раз проверяйте, есть ли в активности данные, прежде чем пытаться получить их с сервера.
Кришнабхадра
2
@Krishnabhadra Хорошо, звучит намного лучше. Позвольте мне исправить в случае, если я ошибаюсь. Согласно вашему примеру, есть только одно действие и, следовательно, один пакет. Создайте экземпляры адаптера в BaseFragment (ссылаясь на ваш проект) и сохраните данные там. Используйте их всякий раз, когда представление должно быть построено.
Renjith
1
Получил это на работу. Большое спасибо. Загрузка всего проекта была хорошей идеей! :-)
Vinay W
96

Нам пришлось реализовать то же поведение, которое вы недавно описали для приложения. Экраны и общий поток приложения уже были определены, поэтому нам пришлось придерживаться его (это клон приложения для iOS ...). К счастью, нам удалось избавиться от экранных кнопок назад :)

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

Поэтому требовалось, чтобы на каждой вкладке было несколько вкладок и вложенных экранов:

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

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

// 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) берет на себя все бэк-стекирование со своим собственным 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, чтобы использовать их.

epidemian
источник
13
Отличный ответ, но я думаю, что очень немногие увидят это. Я выбрал точно такой же путь (как вы можете видеть из разговора в предыдущем ответе), и я не доволен им, как вы. Я думаю, что Google действительно облажался с этими фрагментами, так как этот API не охватывает основные случаи использования. Другая проблема, с которой вы можете столкнуться - это невозможность вставить фрагмент в другой фрагмент.
Дмитрий Рядненко
Спасибо за комментарий Боулдер. Да, я не мог согласиться с API фрагментов. Я уже сталкивался с проблемой вложенных фрагментов (поэтому мы пошли на подход «замените один фрагмент другим», хе-хе).
эпидемиолог
1
Я реализовал это через ВСЕ действия. Мне не понравилось то, что я получил, и я собираюсь попробовать фрагменты. Это противоположность вашего опыта! В Activity есть множество реализаций для управления жизненным циклом дочерних представлений на каждой вкладке, а также для реализации собственной кнопки возврата. Кроме того, вы не можете просто сохранить ссылку на все виды, иначе вы взорвете память. Я надеюсь, что фрагменты: 1) поддержат жизненный цикл фрагментов с четким разделением памяти и 2) помогут реализовать функциональность кнопки «назад». Плюс, если вы используете фрагменты для этого процесса, не будет ли легче работать на планшетах?
Грегм
Что происходит, когда пользователь переключает вкладки? Backstack фрагмента удаляется? Как убедиться, что backstack остается?
Грегм
1
@gregm Если вы перейдете на 1 вкладку <-> 1 действие, как я, обратный стек для каждой вкладки останется при переключении вкладок, потому что действия фактически остаются активными; они только приостановлены и возобновлены. Я не знаю, есть ли способ заставить действия быть разрушенными и воссозданными, когда вкладки переключаются в TabActivity. Однако, если вы сделаете замену фрагментов внутри действий, как я и предлагал, они будут уничтожены (и воссозданы, когда обратный стек извлечен). Таким образом, у вас будет не более одного фрагмента на вкладке в любое время.
эпидемия
6

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

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

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


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

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


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

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

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

Это допустимый сценарий использования пользовательского 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];
            }
    };
}
sergio91pt
источник
2

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


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


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

Вот описание одного возможного примера для этого подхода:

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

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

1) Создайте новый адаптер с соответствующими полями «to» и «from», которые будут соответствовать новым представлениям элементов, добавляемых в список, и столбцам, возвращаемым новым курсором.

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

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

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

С этим не связано никакого обратного стека, поскольку мы не используем транзакции, поэтому вам придется либо создавать свои собственные, либо воспроизводить запросы в обратном порядке при выходе из иерархии. Когда я попытался это сделать, запросы были достаточно быстрыми, чтобы я просто выполнял их снова в oNBackPressed () до тех пор, пока не достиг вершины иерархии, после чего платформа снова переходит на кнопку возврата.

Если вы оказались в подобной ситуации, обязательно прочитайте документы: http://developer.android.com/guide/topics/ui/layout/listview.html

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

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

courtf
источник
Если кто-то делает это, а также использует SectionIndexer (например, AlphabetIndexer), вы можете заметить, что после замены адаптера ваша быстрая прокрутка не работает. Вроде неудачной ошибки, но замена адаптера, даже на совершенно новый индексатор, не обновляет список разделов, используемых FastScroll. Есть обходной путь, пожалуйста, смотрите: описание проблемы и обходной путь
суд
2

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

https://github.com/SebastianBaltesObjectCode/PersistentFragmentTabs

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

Себастьян Балтес
источник
2

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

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

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

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

Arnaud SmartFun
источник
2

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

https://github.com/drusak/tabactivity

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

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

  • использовать библиотеку android.support.design с TabLayout;
  • каждая вкладка имеет свой собственный стек с использованием FragmentManager (без сохранения ссылок на фрагменты);
  • поддержка глубоких ссылок (когда вам нужно открыть конкретную вкладку и определенный уровень фрагмента в ней);
  • сохранение / восстановление состояний вкладок;
  • адаптивные методы жизненного цикла фрагментов во вкладках;
  • довольно легко реализовать для ваших нужд.
kasurd
источник
Спасибо, это было очень полезно. Мне нужно использовать ListFragments в дополнение к Fragments, поэтому я продублировал BaseTabFragment.java в BaseTabListFragment.java и расширил список ListFragment. Затем мне пришлось изменить различные части кода, где всегда предполагалось ожидать BaseTabFragment. Есть ли способ лучше?
Primehalo
К сожалению, не думал о ListFragment. Технически это правильное решение, но для этого потребуются дополнительные проверки для TabFragment и его instanceOf BaseTabListFragment. Другой подход к использованию Fragment с ListView внутри (точно такой же, как реализованный ListFragment). Я подумаю над этим. Спасибо, что указал Мне на это!
Касурд
1

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

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

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.

Мортен Холмгаард
источник
-1

Эта тема была очень очень интересной и полезной.
Спасибо Кришнабхадре за ваше объяснение и код, я использую ваш код и немного улучшил его, позволив сохранить стеки, 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);
    }
}

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

Sulfkain
источник
Это не верно. Когда onCreate вызывается с Bundle, эти фрагменты не будут такими же, как те, которые будут отображаться на экране, и вы теряете старые, если только вы не используете setRetainInstance. И если ActivityManager «сохраняет» вашу активность, поскольку фрагмент не может быть сериализуемым или разбираемым, когда пользователь вернется к вашей активности, он потерпит крах.
sergio91pt
-1

Я бы порекомендовал не использовать backstack на основе HashMap> в режиме «не держать действия» много ошибок. Он не будет правильно восстанавливать состояние, если вы глубоко в стеке фрагмента. А также будет разбит на фрагмент вложенной карты (за исключением: фрагмент не найден для идентификатора). Coz HashMap> после того, как приложение background \ foreground будет нулевым

Я оптимизирую код выше для работы с 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'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>

введите описание изображения здесь

Flinbor
источник