Oddzielny Tylny Stos dla każdej karty w Androidzie za pomocą fragmentów

Próbuję zaimplementować karty do nawigacji w aplikacji na Androida. Ponieważ TabActivity i ActivityGroup są przestarzałe, chciałbym zaimplementować je używając 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ć oddzielny tylny stos dla każdej karty?

Dla przykładowego fragmentu A i B znajdują się w zakładce 1, A fragmentu C i D w zakładce 2. Po uruchomieniu aplikacji wyświetlany jest Fragment A, A Karta 1 jest wybrane. Wtedy Fragment A może zostać zastąpiony fragmentem B. Po wybraniu zakładki 2 powinien zostać wyświetlony Fragment C. Jeżeli Tab 1 jest zaznaczona, Fragment B powinien zostać ponownie wyświetlony. W tym momencie powinno być możliwe użycie przycisku wstecz do wyświetlenia fragmentu A.

Ważne jest również, aby stan każdej karty był zachowany podczas obracania urządzenia.

BR Martin

Author: keyboardsurfer, 2011-08-08

12 answers

Framework nie zrobi tego za ciebie automatycznie. Będziesz musiał budować i zarządzać własnymi stosami tylnymi dla każdej karty.

Szczerze mówiąc, to wydaje się być naprawdę wątpliwa rzecz do zrobienia. Nie mogę sobie wyobrazić, że skutkuje to przyzwoitym interfejsem -- jeśli klawisz back będzie robił różne rzeczy w zależności od karty, którą jestem, zwłaszcza jeśli klawisz back ma również normalne zachowanie zamykania całej aktywności, gdy znajduje się na szczycie stosu... brzmi paskudnie.

Jeśli jesteś próba zbudowania czegoś takiego jak interfejs przeglądarki internetowej, aby uzyskać UX, który jest naturalny dla użytkownika, będzie wymagała wielu subtelnych poprawek zachowania w zależności od kontekstu, więc na pewno będziesz musiał zrobić własne zarządzanie stosem, a nie polegać na jakiejś domyślnej implementacji w ramach. Na przykład spróbuj zwrócić uwagę na to, jak klawisz back współdziała ze standardową przeglądarką na różne sposoby, w które możesz wejść i wyjść. (Każde "okno" w przeglądarce jest zasadniczo zakładką.)

 22
Author: hackbod,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-08-17 07:18:54

Jestem strasznie spóźniona na to pytanie . Ale ponieważ ten wątek był bardzo pouczający i pomocny dla mnie, pomyślałem, że lepiej opublikować moje dwa pensy tutaj.

W 2007 roku, po raz pierwszy w historii, pojawiła się nowa wersja gry, która została wydana na konsolę Xbox 360 i Xbox 360.]}
tabA
    ->  ScreenA1, ScreenA2
tabB
    ->  ScreenB1, ScreenB2

Miałem te same wymagania w przeszłości, i zrobiłem to za pomocą TabActivityGroup (który był również przestarzały w tym czasie) i działań. Tym razem chciałem użyć fragmentów.

Więc tak to zrobiłem.

1. Utwórz klasę fragmentu bazowego

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ą rozszerzyć tę klasę bazową. Jeśli chcesz użyć specjalnych fragmentów, takich jak ListFragment, powinieneś również utworzyć klasę bazową. Będziesz jasne o użyciu onBackPressed() i onActivityResult() jeśli przeczytasz post w całości..

2. Tworzenie identyfikatorów kart, dostępnych wszędzie w 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, Main Tab Activity-proszę przejrzeć komentarze w kod..

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 razie gdyby ktoś był zainteresowany.)

<?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 karcie A, simliar dla wszystkich kart)

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 działało to pięknie. Również miałem tylko ten wymóg w trybie portretowym. Nigdy nie musiałem używać tego kodu w projekcie wspierającym obie orientacje. Więc nie mogę powiedzieć, jakie wyzwania stoję tam..

Edytuj :

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

 132
Author: Krishnabhadra,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-09-25 18:54:59

Musieliśmy zaimplementować dokładnie to samo zachowanie, które ostatnio opisałeś dla aplikacji. Ekrany i ogólny przepływ aplikacji zostały już zdefiniowane ,więc musieliśmy się jej trzymać (jest to klon aplikacji na iOS...). Na szczęście udało nam się pozbyć wyświetlanych na ekranie tylnych przycisków:)]}

Zhakowaliśmy rozwiązanie za pomocą mieszanki TabActivity, FragmentActivities (korzystaliśmy z biblioteki wsparcia dla fragmentów) i Fragments. W retrospektywie jestem pewien, że nie była to najlepsza Architektura decyzja, ale udało nam się uruchomić. Gdybym miał zrobić to jeszcze raz, prawdopodobnie spróbowałbym zrobić rozwiązanie bardziej oparte na aktywności (bez fragmentów) lub spróbować mieć tylko jedną aktywność dla kart i pozwolić, aby cała reszta była widokami (które uważam, że są znacznie bardziej wielokrotnego użytku niż działania ogółem).

Tak więc wymagania miały mieć kilka zakładek i zagnieżdżalne ekrany w każdej zakładce:

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

Itd...

Tak powiedzmy: użytkownik zaczyna w zakładce 1, przechodzi z ekranu 1 do ekranu 2, a następnie do ekranu 3, on następnie przełącza się na kartę 3 i przechodzi z ekranu 4 do 6; jeśli przełączony z powrotem do karty 1, powinien zobaczyć ekran 3 Ponownie, a jeśli nacisnął z powrotem powinien wrócić do ekranu 2; z powrotem i jest na ekranie 1; przełącz się na kartę 3 i jest na ekranie 6 ponownie.

Główną aktywnością w aplikacji jest MainTabActivity, która rozszerza TabActivity. Każda karta jest powiązana z aktywnością, powiedzmy ActivityInTab1, 2 i 3. I wtedy każdy ekran będzie fragmentem:

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

Każda Aktywność przechowuje tylko jeden fragment na raz i wie, jak zastąpić jeden fragment na inny (prawie tak samo jak ActvityGroup). Fajną rzeczą jest to, że dość łatwo jest utrzymać oddzielne stosy dla każdej karty w ten sposób.

Funkcjonalność dla każdej aktywności była taka sama: wiemy, jak poruszać się z jednego fragmentu do drugiego i utrzymywać tylny 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 to:

<?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. To dlatego, że jest to tylko FrameLayout o nazwie content, który będzie zawierał każdy fragment. Fragmenty są tymi, które mają widok każdego ekranu.

Tylko dla punktów bonusowych, dodaliśmy również mały kod, aby pokazać okno dialogowe potwierdzenia, gdy użytkownik naciśnie Wstecz i nie ma już 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 w zasadzie konfiguracja. Jak widać, każdy fragment (lub po prostu Aktywność w Androidzie > 3) zajmuje się całym back-stackingiem za pomocą własnego Fragmentmanagera.

Działanie takie jak ActivityInTab1 będzie naprawdę proste, po prostu pokaże 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 zrobić trochę paskudnego odlewu... ale nie jest tak źle:

// In Fragment1.java for example...
// Need to navigate to Fragment2.
((ActivityIntab) getActivity()).navigateTo(new Fragment2());
Więc to tyle. Jestem prawie pewien, że nie jest to zbyt kanoniczne (a w większości na pewno niezbyt dobre) rozwiązanie, więc chciałbym zapytaj doświadczonych programistów Androida, co byłoby lepszym podejściem do osiągnięcia tej funkcjonalności, a jeśli nie jest to "jak to się robi" w Androidzie, byłbym wdzięczny, gdybyś mógł wskazać mi jakiś link lub materiał, który wyjaśnia, który jest Android sposób podejścia do tego (karty, zagnieżdżone ekrany w kartach, itp). Zapraszam do rozdarcia tej odpowiedzi w komentarzach:)

Jako znak, że to rozwiązanie nie jest zbyt dobre jest to, że ostatnio musiałem dodać kilka funkcjonalności nawigacji do podanie. Jakiś dziwaczny przycisk, który powinien przenieść użytkownika z jednej karty do drugiej i do zagnieżdżonego ekranu. Robienie tego programowo było wrzodem na tyłku, z powodu problemów kto-wie-kto i radzenia sobie z tym, kiedy fragmenty i działania są faktycznie inicjowane i inicjowane. Myślę, że byłoby o wiele łatwiej, gdyby te ekrany i karty były tylko widokami.


Wreszcie, jeśli chcesz przetrwać zmiany orientacji, ważne jest, aby Twoje fragmenty zostały utworzone używanie setArguments / getArguments. Jeśli ustawisz zmienne instancji w konstruktorach fragmentów, będziesz miał przerąbane. Ale na szczęście jest to naprawdę łatwe do naprawienia: po prostu zapisz wszystko w setarguments w konstruktorze, a następnie pobierz te rzeczy z getArguments in onCreate, aby je użyć.

 93
Author: epidemian,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-12-05 03:57:37

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

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

Jeden z nich wystarczy, aby zaimplementować backstack.


Używając putFragment, zamiast zastępować Fragment, odłączasz stary i dodajesz nowy. To właśnie robi framework z transakcją zastępczą, która jest dodawana do backstacka. putFragment przechowuje indeks do bieżącej listy aktywnych fragmentów, a te fragmenty są zapisywane przez framework podczas zmian orientacji.

Drugi sposób, używając saveFragmentInstanceState, zapisuje cały stan fragmentu do paczki, pozwalając na jego usunięcie, a nie odłączanie. Użycie tego podejścia sprawia, że tylny stos jest łatwiejszy do manipulowania, ponieważ możesz wyskakiwać Fragment, kiedy tylko chcesz.


Użyłem drugiej metody dla tego zastosowania:

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

Nie chcę, aby użytkownik wracał do ekranu rejestracji, z trzeciego, naciskając przycisk Wstecz. Ja też. flip animacje między nimi (za pomocą onCreateAnimation), więc hacky rozwiązania nie będą działać, przynajmniej bez użytkownika wyraźnie zauważając coś jest nie tak.

Jest to prawidłowy przypadek użycia niestandardowego backstacka, wykonują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];
            }
    };
}
 6
Author: sergio91pt,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-05-11 18:13:16

Można to łatwo osiągnąć za pomocą ChildFragmentManager

Oto post o tym z powiązanym projektem. spójrz,

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

 5
Author: tausiq,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-06-17 02:57:30

Zastrzeżenie:


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 materiałem na Androida. To nie rozwiąże problemu dla wszystkich, ale może pomóc niektórym.


Jeśli podstawową różnicą między fragmentami jest tylko kopia zapasowa danych (tj. nie ma dużych różnic w układzie), to może nie być konieczne zastąpienie fragmentu, a jedynie Zamiana podstawowych danych i odświeżyć widok.

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

Mam aplikację, która używa ListViews. Każda pozycja na liście jest rodzicem z pewną liczbą dzieci. Po dotknięciu elementu należy otworzyć nową listę z tymi dziećmi na tej samej karcie paska czynności, co oryginalna lista. Te zagnieżdżone listy mają bardzo podobny układ (być może niektóre modyfikacje warunkowe tu i tam), ale dane są inne.

Ta aplikacja ma kilka warstw potomstwo poniżej początkowej listy rodzicielskiej i możemy lub nie możemy mieć danych z serwera do czasu, gdy użytkownik próbuje uzyskać dostęp do pewnej 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 wypełnienia widoku listy elementami listy, wszystko, co musi się zdarzyć, gdy kliknięcie jest zarejestrowane, to:

1) Utwórz nowy adapter z odpowiednimi polami " to " I "from", które będą pasowały do nowych widoków pozycji dodane do listy i kolumny zwrócone 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 uruchom ponownie ładowarkę kursora z nowym URI (i projekcją). W tym przykładzie URI jest mapowane do określonych zapytań z argami wyboru przekazywanymi z interfejsu użytkownika.

4) gdy nowe dane zostaną załadowane z URI, zamień kursor związany z adapterem na nowy kursor i lista zostanie odświeżona.

Nie ma backstacka związanego z tym, 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 wykonuję je ponownie w onbackpressed (), dopóki nie znajduję się na szczycie hierarchii, w którym to momencie framework ponownie przejmuje przycisk Wstecz.

Jeśli znajdziesz się w podobnej sytuacji, upewnij się, że przeczytaj dokumenty: 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!

 2
Author: courtf,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-08-11 19:45:22

Miałem dokładnie ten sam problem i zaimplementowałem projekt open source github, który obejmuje ułożone karty, Backup i up nawigacji i jest dobrze przetestowany i udokumentowany:

Https://github.com/SebastianBaltesObjectCode/PersistentFragmentTabs

Jest to prosty i mały framework do zakładek nawigacji i przełączania fragmentów oraz obsługi nawigacji w górę iw tył. Każda karta ma swój własny stos fragmentów. Używa ActionBarSherlock i jest kompatybilny z powrotem do poziomu API 8.

 2
Author: Sebastian Baltes,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-03-27 18:07:20

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

Https://github.com/drusak/tabactivity

Cel stworzenia biblioteki jest dość banalny - zaimplementuj ją jak iPhone.

Główne zalety:

  • użyj Androida.wsparcie.design library with TabLayout;
  • każda karta ma swój własny stos za pomocą Fragmentmanagera (bez zapisywania fragmentów' referencje);
  • wsparcie dla głębokiego łączenia (gdy trzeba otworzyć określoną kartę i określony poziom fragmentu w niej);
  • zapisywanie / przywracanie Stanów tabulatorów;
  • adaptacyjne metody cyklu życia fragmentów w kartach;
  • dość łatwe do wdrożenia dla Twoich potrzeb.

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

Dzięki!

 2
Author: kasurd,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-12-24 12:40:36

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

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

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

Jeśli masz jakieś pytania, nie wahaj się wysłać maila.

 2
Author: Arnaud SmartFun,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-12-09 23:16:10

Proste rozwiązanie:

Za każdym razem, gdy zmienisz wywołanie tab / root view:

fragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);

Oczyś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();

Uwaga .addToBackStack(null) i transaction.add można np. zmienić za pomocą transaction.replace.

 1
Author: Morten Holmgaard,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-10-19 18:22:13

Ten wątek był bardzo ciekawy i przydatny.
Dzięki Krishnabhadra za wyjaśnienie i Kod, używam Twojego kodu i poprawiłem trochę, pozwalając na utrzymywanie stosów, currentTab, itp... od zmiany konfiguracji (głównie obrotowej).
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 Krishnabhadra doda to do swojego kodu.

Odzyskiwanie danych 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 poprzedni CurrentTab, ustaw to, 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.
 -1
Author: Sulfkain,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-09-13 15:26:38

Polecam nie używać backstacka opartego na HashMap> w trybie "nie zachowuj aktywności" jest dużo błędów. Nie przywróci poprawnie stanu w przypadku, gdy będziesz głęboko w stosie fragmentu. A także będzie buforowany w zagnieżdżonym fragmencie mapy (z Exception: Fragment no view found for ID). Coz HashMap > after background\foreground app will be null

Optymalizuję powyższy kod do pracy z backstackiem fragmentu

Jest to bottom 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>

Tutaj wpisz opis obrazka

 -1
Author: Flinbor,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-04-24 19:26:14