Oddzielny stos wsteczny dla każdej karty w systemie Android przy użyciu fragmentów

158

Próbuję zaimplementować karty do nawigacji w aplikacji na Androida. Ponieważ TabActivity i ActivityGroup są przestarzałe, chciałbym zaimplementować je za pomocą fragmentów.

Wiem, jak ustawić jeden fragment dla każdej karty, a następnie przełączać fragmenty po kliknięciu karty. Ale jak mogę mieć osobny stos tylny dla każdej karty?

Na przykład fragmenty A i B będą znajdować się na karcie 1, a fragment C i D w karcie 2. Po uruchomieniu aplikacji wyświetlany jest fragment A i wybrana jest karta 1. Następnie Fragment A można zastąpić fragmentem B. Po wybraniu zakładki 2 Fragment C powinien zostać wyświetlony. Jeśli zostanie wybrana zakładka 1, fragment B powinien zostać ponownie wyświetlony. W tym momencie powinno być możliwe użycie przycisku Wstecz, aby wyświetlić Fragment A.

Ponadto ważne jest, aby stan każdej klapki był zachowany, gdy urządzenie jest obracane.

BR Martin

mardah
źródło

Odpowiedzi:

23

Framework obecnie nie robi tego automatycznie. Będziesz musiał zbudować własne stosy zapasowe i zarządzać nimi dla każdej karty.

Szczerze mówiąc, wydaje się, że to naprawdę wątpliwa rzecz. Nie wyobrażam sobie, żeby to skutkowało przyzwoitym interfejsem użytkownika - jeśli klawisz wstecz ma robić różne rzeczy w zależności od karty, którą jestem, zwłaszcza jeśli klawisz wstecz ma również normalne zachowanie polegające na zamykaniu całej czynności, gdy jest na górze stos ... brzmi okropnie.

Jeśli próbujesz zbudować coś w rodzaju interfejsu użytkownika przeglądarki internetowej, uzyskanie UX, który jest naturalny dla użytkownika, będzie wymagało wielu subtelnych poprawek zachowania w zależności od kontekstu, więc na pewno będziesz musiał zrobić własny stos zarządzanie zamiast polegać na domyślnej implementacji w ramach. Na przykład spróbuj zwrócić uwagę na to, jak klawisz Wstecz współdziała ze standardową przeglądarką na różne sposoby, na jakie możesz wchodzić i wychodzić z niej. (Każde „okno” w przeglądarce jest zasadniczo kartą).

hackbod
źródło
7
Nie rób tego. Ramy nie są bezużyteczne. Nie zapewnia automatycznego wsparcia dla tego rodzaju rzeczy, których, jak powiedziałem, nie wyobrażam sobie, aby zapewnić przyzwoite wrażenia użytkownika, z wyjątkiem bardzo wyspecjalizowanych sytuacji, w których i tak będziesz musiał uważnie kontrolować zachowanie pleców.
hackbod,
9
Ten typ nawigacji, wtedy masz zakładki i hierarchię stron na każdej karcie, jest bardzo powszechny na przykład dla aplikacji na iPhone'a (możesz sprawdzić aplikacje App Store i iPod). Uważam, że ich wrażenia użytkownika są całkiem przyzwoite.
Dmitry Ryadnenko
13
To jest szalone. IPhone nie ma nawet przycisku Wstecz. Istnieją demonstracje API pokazujące bardzo prosty kod do implementacji fragmentów w zakładkach. Pytanie, które zostało zadane, dotyczyło posiadania różnych stosów tylnych dla każdej karty, a moja odpowiedź jest taka, że ​​platforma nie dostarcza tego automatycznie, ponieważ semantycznie dla tego, co robi przycisk Wstecz, najprawdopodobniej byłoby kiepskim doświadczeniem użytkownika. Jeśli chcesz, możesz dość łatwo zaimplementować semantykę wsteczną.
hackbod
4
Ponownie, iPhone nie ma przycisku Wstecz, więc nie ma semantycznego zachowania stosu wstecznego, takiego jak Android. Również „lepiej po prostu trzymać się działań i zaoszczędzić sobie dużo czasu” nie ma tu żadnego sensu, ponieważ działania nie pozwalają na umieszczanie zakładek utrzymania w interfejsie użytkownika z ich własnymi różnymi stosami tylnymi; w rzeczywistości zarządzanie działaniami wstecznego stosu jest mniej elastyczne niż to, co zapewnia struktura fragmentacji.
hackbod
22
@hackbod Próbuję postępować zgodnie z Twoimi uwagami, ale mam problem z zaimplementowaniem niestandardowego zachowania stosu wstecznego. Zdaję sobie sprawę, że będąc zaangażowanym w zaprojektowanie tego, miałbyś solidny wgląd w to, jakie to łatwe. Czy to możliwe, że istnieje aplikacja demonstracyjna dla przypadku użycia OP, ponieważ jest to naprawdę bardzo powszechna sytuacja, szczególnie dla tych z nas, którzy muszą pisać i portować aplikacje iOS dla klientów, którzy zgłaszają te żądania ... zarządzanie oddzielnymi fragment backstacks w ramach każdego FragmentActivity.
Richard Le Mesurier
138

Strasznie spóźniam się na to pytanie. Ale ponieważ ten wątek był dla mnie bardzo pouczający i pomocny, pomyślałem, że lepiej opublikuję tutaj moje dwa pensy.

Potrzebowałem takiego przepływu ekranu (minimalistyczny projekt z 2 zakładkami i 2 widokami w każdej zakładce),

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

W przeszłości miałem te same wymagania i robiłem to przy użyciu TabActivityGroup(co było wówczas przestarzałe) i Aktywności. Tym razem chciałem użyć Fragmentów.

Więc tak to zrobiłem.

1. Utwórz podstawową klasę fragmentów

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

Wszystkie fragmenty w Twojej aplikacji mogą rozszerzać tę klasę bazową. Jeśli chcesz używać specjalnych fragmentów, takich jak ListFragmentpowinieneś utworzyć klasę bazową również do tego. Będziesz mieć jasność co do użycia onBackPressed()i onActivityResult()przeczytania całego posta.

2. Utwórz identyfikatory zakładek, dostępne w całym projekcie

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

nie ma tu nic do wyjaśnienia ..

3. OK, Aktywność na karcie głównej - przejrzyj komentarze w kodzie.

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 (w przypadku zainteresowania.)

<?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 (pierwszy fragment w Tab A, podobny do wszystkich zakładek)

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

To może nie być najbardziej dopracowany i poprawny sposób. Ale w moim przypadku zadziałało pięknie. Miałem ten wymóg tylko w trybie portretowym. Nigdy nie musiałem używać tego kodu w projekcie wspierającym obie orientacje. Nie mogę więc powiedzieć, jakie wyzwania tam stoję.

EDYTOWAĆ :

Jeśli ktoś chce mieć pełny projekt, umieściłem przykładowy projekt na github .

Krishnabhadra
źródło
2
Przechowywanie danych dla każdego fragmentu, odtwarzanie każdego z nich, przebudowywanie stosów ... tyle pracy przy prostej zmianie orientacji.
Michael Eilers Smith
3
@omegatai całkowicie się z tobą zgadzam. Cały problem pojawia się, ponieważ Android nie zarządza stosem za nas ( co robi iOS, a zmiana orientacji lub zakładka z wieloma fragmentami to pestka ) i co przenosi nas z powrotem do pierwotnej dyskusji w tym Q / Wątek. Nie ma
sensu
1
@Renjith Dzieje się tak, ponieważ fragment jest odtwarzany za każdym razem, kiedy zmieniasz kartę. Nie myśl nawet o tym, że Twój fragment jest ponownie używany przez przełącznik tabulacji. kiedy przełączam się z zakładki A na B, karta A jest uwalniana z pamięci. Więc zapisz swoje dane w aktywności i za każdym razem sprawdzaj, czy aktywność zawiera dane, zanim spróbujesz je pobrać z serwera.
Krishnabhadra
2
@Krishnabhadra Ok, to brzmi znacznie lepiej. Pozwól mi poprawić, jeśli się mylę. Zgodnie z twoim przykładem jest tylko jedno działanie, a zatem jeden pakiet. Utwórz instancje adaptera w BaseFragment (odwołując się do swojego projektu) i zapisz tam dane. Używaj ich zawsze, gdy chcesz zbudować widok.
Renjith
1
Mam to do pracy. Wielkie dzięki. Wgranie całego projektu było dobrym pomysłem! :-)
Vinay W
96

Musieliśmy zaimplementować dokładnie to samo zachowanie, które ostatnio opisujesz dla aplikacji. Ekrany i ogólny przebieg aplikacji zostały już zdefiniowane, więc musieliśmy się tego trzymać (to klon aplikacji na iOS ...). Na szczęście udało nam się pozbyć ekranowych przycisków wstecz :)

Zhakowaliśmy rozwiązanie przy użyciu mieszanki TabActivity, FragmentActivities (używaliśmy biblioteki obsługi fragmentów) i Fragments. Z perspektywy czasu jestem prawie pewien, że nie była to najlepsza decyzja dotycząca architektury, ale udało nam się to naprawić. Gdybym musiał to zrobić ponownie, prawdopodobnie spróbuję zrobić rozwiązanie bardziej oparte na aktywności (bez fragmentów) lub spróbować mieć tylko jedną aktywność dla zakładek i niech cała reszta będzie widokami (które uważam za znacznie więcej wielokrotnego użytku niż ogólnie czynności).

Tak więc wymagania były takie, aby w każdej zakładce było kilka zakładek i ekranów z możliwością zagnieżdżania:

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

itp...

Powiedzmy: użytkownik uruchamia się na karcie 1, przechodzi od ekranu 1 do ekranu 2, a następnie do ekranu 3, a następnie przechodzi do karty 3 i przechodzi od ekranu 4 do 6; jeśli przełączył się z powrotem na kartę 1, powinien ponownie zobaczyć ekran 3, a jeśli nacisnął przycisk Wstecz, powinien wrócić do ekranu 2; Znowu i jest na ekranie 1; przejdź do zakładki 3 i znowu jest na ekranie 6.

Głównym działaniem w aplikacji jest MainTabActivity, które rozszerza TabActivity. Każda zakładka jest powiązana z działaniem, powiedzmy ActivityInTab1, 2 i 3. A potem każdy ekran będzie fragmentem:

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

Każda karta ActivityInTab zawiera tylko jeden fragment na raz i wie, jak zastąpić jeden fragment innym (prawie tak samo jak ActvityGroup). Fajne jest to, że w ten sposób dość łatwo jest utrzymać oddzielne stosy tylne dla każdej karty.

Funkcjonalność każdej karty ActivityInTab była taka sama: umieć nawigować z jednego fragmentu do drugiego i utrzymywać stos, więc umieściliśmy go w klasie bazowej. Nazwijmy to po prostu 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 jest po prostu taka:

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

Jak widać, układ widoku dla każdej karty był taki sam. Dzieje się tak, ponieważ jest to tylko FrameLayout o nazwie content, który będzie zawierał każdy fragment. Fragmenty to te, które mają widok każdego ekranu.

Tylko dla punktów bonusowych dodaliśmy również mały kod, aby wyświetlić okno dialogowe potwierdzenia, gdy użytkownik naciśnie przycisk Wstecz i nie ma więcej fragmentów, do których można wrócić:

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

To prawie konfiguracja. Jak widać, każda FragmentActivity (lub po prostu aktywność w systemie Android> 3) dba o całe układanie w stos z własnym FragmentManager.

Działanie takie jak ActivityInTab1 będzie naprawdę proste, pokaże tylko swój pierwszy fragment (tj. Ekran):

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

Następnie, jeśli fragment musi przejść do innego fragmentu, musi wykonać trochę nieprzyjemnego rzucania ... ale nie jest tak źle:

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

Więc to wszystko. Jestem prawie pewien, że nie jest to bardzo kanoniczne (i na pewno niezbyt dobre) rozwiązanie, więc chciałbym zapytać doświadczonych programistów Androida, jakie byłoby lepsze podejście do uzyskania tej funkcjonalności, a jeśli nie jest to „jak to jest gotowe ”w Androidzie, byłbym wdzięczny, gdybyś wskazał mi jakiś link lub materiał wyjaśniający, w jaki sposób można to zrobić w systemie Android (karty, zagnieżdżone ekrany w kartach itp.). Zapraszam do rozdzielenia tej odpowiedzi w komentarzach :)

Znakiem, że to rozwiązanie nie jest zbyt dobre, jest to, że ostatnio musiałem dodać do aplikacji jakąś funkcjonalność nawigacyjną. Jakiś dziwaczny przycisk, który powinien przenosić użytkownika z jednej karty do drugiej i do zagnieżdżonego ekranu. Robienie tego programowo było uciążliwe, ponieważ problemy „kto-wie-kto” i radzenie sobie z tym, kiedy są fragmentami i działaniami, które zostały faktycznie zainicjowane i zainicjowane. Myślę, że byłoby znacznie łatwiej, gdyby te ekrany i karty były tak naprawdę tylko widokami.


Na koniec, jeśli chcesz przetrwać zmiany orientacji, ważne jest, aby Twoje fragmenty były tworzone za pomocą setArguments / getArguments. Jeśli ustawisz zmienne instancji w konstruktorach twoich fragmentów, będziesz spieprzony. Ale na szczęście to naprawdę łatwe do naprawienia: po prostu zapisz wszystko w setArguments w konstruktorze, a następnie pobierz te rzeczy za pomocą getArguments w onCreate, aby z nich skorzystać.

epidemia
źródło
13
Świetna odpowiedź, ale myślę, że niewielu to zobaczy. Wybrałem dokładnie tę samą ścieżkę (co widać z rozmowy w poprzedniej odpowiedzi) i nie jestem z niej zadowolony tak jak Ty. Myślę, że Google naprawdę spieprzyło te fragmenty, ponieważ ten interfejs API nie obejmuje głównych przypadków użycia. Inną kwestią, na którą możesz się natknąć, jest niemożność osadzenia fragmentu w innym fragmencie.
Dmitry Ryadnenko
Dzięki za komentarz. Tak, nie mogłem się bardziej zgodzić na temat API fragmentów. Napotkałem już problem zagnieżdżonych fragmentów (dlatego zdecydowaliśmy się na „zastąpienie jednego fragmentu innym” podejście hehe).
epidemia
1
Wdrożyłem to poprzez WSZYSTKIE działania. Nie podobało mi się to, co dostałem i zamierzam wypróbować Fragments. To przeciwieństwo twojego doświadczenia! Istnieje wiele implementacji z działaniami, które obsługują cykl życia widoków podrzędnych na każdej karcie, a także implementują własny przycisk Wstecz. Nie możesz też po prostu zachować odniesienia do wszystkich poglądów, bo wysadzisz pamięć. Mam nadzieję, że fragmenty: 1) wspierają cykl życia fragmentów dzięki wyraźnej separacji pamięci i 2) pomagają zaimplementować funkcjonalność przycisku wstecz. Dodatkowo, jeśli użyjesz fragmentów do tego procesu, czy nie będzie łatwiej uruchomić na tabletach?
gregm
Co się stanie, gdy użytkownik przełączy karty? Czy backstack Fragmentów zostanie usunięty? Jak upewnić się, że backstack pozostanie?
gregm
1
@gregm Jeśli przejdziesz do 1 karty <-> 1 działanie, tak jak ja, stos dla każdej karty pozostanie po przełączeniu kart, ponieważ działania są faktycznie utrzymywane przy życiu; są tylko wstrzymywane i wznawiane. Nie wiem, czy istnieje sposób na zniszczenie i ponowne utworzenie działań po przełączeniu kart w TabActivity. Jeśli jednak sprawisz , że fragmenty wewnątrz działań zostaną zastąpione, tak jak zasugerowałem, zostaną one zniszczone (i ponownie utworzone po przebiciu backstacka). Więc w każdej chwili będziesz mieć co najwyżej jeden żywy fragment na kartę.
epidemia
6

Przechowywanie silnych odniesień do fragmentów nie jest właściwym sposobem.

FragmentManager zapewnia putFragment(Bundle, String, Fragment)i saveFragmentInstanceState(Fragment).

Aby zaimplementować backstack, wystarczy jeden.


Używając putFragmentzamiast zastępowania fragmentu, odłączasz stary i dodajesz nowy. To właśnie robi framework z transakcją zamiany, która jest dodawana do backstack. putFragmentprzechowuje indeks do bieżącej listy aktywnych fragmentów, a te fragmenty są zapisywane przez strukturę podczas zmian orientacji.

Drugi sposób, za pomocą saveFragmentInstanceState, zapisuje stan całego fragmentu w paczce, co pozwala ci go naprawdę usunąć, zamiast odłączać. Takie podejście ułatwia manipulowanie tylnym stosem, ponieważ możesz wrzucić fragment, kiedy tylko chcesz.


Użyłem drugiej metody w tym przypadku:

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

Nie chcę, aby użytkownik wracał do ekranu rejestracji, z trzeciego, naciskając przycisk Wstecz. Wykonuję również animacje między nimi (używając onCreateAnimation), więc hackerskie rozwiązania nie zadziałają, przynajmniej bez wyraźnego zauważenia przez użytkownika, że ​​coś jest nie tak.

To jest prawidłowy przypadek użycia niestandardowego backstacka, robiącego to, czego oczekuje użytkownik ...

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
źródło
2

Zrzeczenie się:


Uważam, że jest to najlepsze miejsce, aby opublikować podobne rozwiązanie, nad którym pracowałem, w przypadku podobnego problemu, który wydaje się być dość standardowym problemem dla Androida. To nie rozwiąże problemu dla wszystkich, ale może niektórym pomóc.


Jeśli podstawową różnicą między twoimi fragmentami są tylko dane, z których są wykonane kopie zapasowe (tj. Nie ma wielu dużych różnic w układzie), może nie być konieczne zastępowanie fragmentu, a jedynie zamiana podstawowych danych i odświeżenie widoku.

Oto opis jednego możliwego przykładu tego podejścia:

Mam aplikację, która używa ListViews. Każda pozycja na liście to rodzic z pewną liczbą dzieci. Po stuknięciu elementu nowa lista musi zostać otwarta z tymi elementami podrzędnymi na tej samej karcie ActionBar, co oryginalna lista. Te zagnieżdżone listy mają bardzo podobny układ (być może niektóre warunkowe poprawki tu i tam), ale dane są inne.

Ta aplikacja ma kilka warstw potomstwa pod początkową listą nadrzędną i możemy, ale nie muszą, mieć dane z serwera do czasu, gdy użytkownik spróbuje uzyskać dostęp do określonej głębokości poza pierwszą. Ponieważ lista jest zbudowana z kursora bazy danych, a fragmenty używają programu ładującego kursor i adaptera kursora do zapełniania widoku listy elementami listy, wszystko, co musi się wydarzyć po zarejestrowaniu kliknięcia, to:

1) Utwórz nowy adapter z odpowiednimi polami „do” i „z”, które będą pasować do nowych widoków pozycji dodawanych do listy i kolumn zwracanych przez nowy kursor.

2) Ustaw ten adapter jako nowy adapter dla ListView.

3) Zbuduj nowy URI na podstawie elementu, który został kliknięty i zrestartuj program ładujący kursor z nowym URI (i projekcją). W tym przykładzie identyfikator URI jest mapowany na określone zapytania z argumentami wyboru przekazywanymi z interfejsu użytkownika.

4) Po załadowaniu nowych danych z identyfikatora URI zamień kursor powiązany z adapterem na nowy kursor, a lista zostanie odświeżona.

Nie ma z tym powiązania, ponieważ nie używamy transakcji, więc będziesz musiał albo zbudować własne, albo odtwarzać zapytania w odwrotnej kolejności, gdy wycofujesz się z hierarchii. Kiedy próbowałem tego, zapytania były na tyle szybkie, że po prostu wykonałem je ponownie w onBackPressed (), aż znajdę się na szczycie hierarchii, w którym to momencie struktura ponownie przejmuje przycisk Wstecz.

Jeśli znajdziesz się w podobnej sytuacji, koniecznie przeczytaj dokumentację: http://developer.android.com/guide/topics/ui/layout/listview.html

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

Mam nadzieję, że to komuś pomoże!

Courtf
źródło
W przypadku, gdy ktoś to robi, a także używa SectionIndexer (takiego jak AlphabetIndexer), możesz zauważyć, że po wymianie adaptera szybkie przewijanie nie działa. Trochę niefortunny błąd, ale wymiana adaptera, nawet na zupełnie nowy indeksator, nie aktualizuje listy sekcji używanych przez FastScroll. Istnieje obejście, zobacz: opis problemu i obejście
sąd:
2

Miałem dokładnie ten sam problem i zaimplementowałem projekt github o otwartym kodzie źródłowym, który obejmuje ułożone w stosy karty, nawigację po kopii zapasowej i wstecz oraz jest dobrze przetestowany i udokumentowany:

https://github.com/SebastianBaltesObjectCode/PersistentFragmentTabs

Jest to prosta i niewielka struktura dla kart nawigacyjnych i przełączania fragmentów oraz obsługi nawigacji w górę i w tył. Każda karta ma swój własny stos fragmentów. Używa ActionBarSherlock i jest kompatybilny z powrotem do poziomu API 8.

Sebastian Baltes
źródło
2

Jest to złożony problem, ponieważ Android obsługuje tylko 1 stos tylny, ale jest to wykonalne. Stworzenie biblioteki o nazwie Tab Stacker zajęło mi kilka dni, która robi dokładnie to, czego szukasz: historię fragmentów dla każdej karty. Jest open source, w pełni udokumentowany i można go łatwo dołączyć do gradle. Bibliotekę można znaleźć na github: https://github.com/smart-fun/TabStacker

Możesz również pobrać przykładową aplikację, aby sprawdzić, czy zachowanie odpowiada Twoim potrzebom:

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

Jeśli masz jakieś pytanie, nie wahaj się i napisz.

Arnaud SmartFun
źródło
2

Chciałbym zaproponować własne rozwiązanie na wypadek, gdyby ktoś szukał i chciałby spróbować wybrać najlepsze dla swoich potrzeb.

https://github.com/drusak/tabactivity

Cel tworzenia biblioteki jest dość banalny - zaimplementuj ją jak iPhone'a.

Główne zalety:

  • użyj biblioteki android.support.design z TabLayout;
  • każda karta ma swój własny stos za pomocą FragmentManagera (bez zapisywania odniesień do fragmentów);
  • obsługa głębokiego linkowania (kiedy trzeba otworzyć konkretną zakładkę i poziom w niej określonego fragmentu);
  • zapisywanie / przywracanie stanów zakładek;
  • adaptacyjne metody cyklu życia fragmentów w zakładkach;
  • dość łatwe do wdrożenia dla Twoich potrzeb.
kasurd
źródło
Dziękuję, to było bardzo pomocne. Muszę użyć ListFragments oprócz Fragments, więc zduplikowałem BaseTabFragment.java do BaseTabListFragment.java i kazałem mu rozszerzyć ListFragment. Następnie musiałem zmienić różne części w kodzie, w których zawsze zakładano, że oczekuję BaseTabFragment. Czy jest lepszy sposób?
primehalo
Niestety, nie pomyślałem o ListFragment. Technicznie jest to właściwe rozwiązanie, ale będzie wymagało dodatkowego sprawdzenia TabFragment i jego instancji BaseTabListFragment. Inne podejście do używania fragmentu z ListView wewnątrz (dokładnie tak samo, jak ListFragment implemeted). Przemyślę to. Dzięki za wskazanie mi tego!
kasurd
1

Proste rozwiązanie:

Za każdym razem, gdy zmieniasz wywołanie widoku karty / katalogu głównego:

fragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);

To wyczyści BackStack. Pamiętaj, aby wywołać to przed zmianą fragmentu głównego.

I dodaj fragmenty z tym:

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

Zauważ, że .addToBackStack(null)i transaction.addmożna np. Zmienić za pomocą transaction.replace.

Morten Holmgaard
źródło
-1

Ten wątek był bardzo interesujący i przydatny.
Dzięki Krishnabhadra za wyjaśnienie i kod, używam twojego kodu i poprawiłem go trochę, pozwalając zachować stosy, currentTab, itp ... ze zmiany konfiguracji (głównie rotacji).
Testowane na prawdziwych urządzeniach 4.0.4 i 2.3.6, nie testowane na emulatorze

Zmieniam tę część kodu na „AppMainTabActivity.java”, reszta pozostaje taka sama. Może Krysznabhadra doda to do swojego kodu.

Odzyskaj dane onCreate:

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

Zapisz zmienne i umieść w pakiecie:

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

Jeśli istnieje poprzednia CurrentTab, ustaw ją, w przeciwnym razie utwórz nową 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);
    }
}

Mam nadzieję, że to pomoże innym ludziom.

Sulfkain
źródło
To jest źle. Kiedy onCreate jest wywoływane z paczką, te fragmenty nie będą tymi samymi, które będą wyświetlane na ekranie, a ty wyciekasz stare, chyba że używasz setRetainInstance. A jeśli ActivityManager „zapisuje” Twoją aktywność, ponieważ fragmentu nie można serializować ani parcelować, po powrocie użytkownika do działania nastąpi awaria.
sergio91pt
-1

Zalecałbym nie używać backstacka opartego na HashMap> jest wiele błędów w trybie „nie zachowuj aktywności”. Nie przywróci poprawnie stanu w przypadku, gdy jesteś głęboko w stosie fragmentów. A także zostanie zgnieciony w zagnieżdżonym fragmencie mapy (z wyjątkiem: fragmentu nie znaleziono widoku dla ID). Coz HashMap> po aplikacji w tle \ na pierwszym planie będzie miała wartość NULL

Powyższy kod optymalizuję pod kątem pracy z backstackiem fragmentu

To jest dolny TabView

Główna działalność Klasa

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>

wprowadź opis obrazu tutaj

Flinbor
źródło