Back Stack séparé pour chaque onglet dans Android à l'aide de fragments

158

J'essaie d'implémenter des onglets pour la navigation dans une application Android. Puisque TabActivity et ActivityGroup sont obsolètes, je voudrais plutôt l'implémenter en utilisant des fragments.

Je sais comment configurer un fragment pour chaque onglet, puis changer de fragment lorsqu'un onglet est cliqué. Mais comment puis-je avoir une pile arrière distincte pour chaque onglet?

Pour un exemple, les fragments A et B seraient sous l'onglet 1 et les fragments C et D sous l'onglet 2. Lorsque l'application est démarrée, le fragment A est affiché et l'onglet 1 est sélectionné. Ensuite, le fragment A peut être remplacé par le fragment B. Lorsque l'onglet 2 est sélectionné, le fragment C doit être affiché. Si l'onglet 1 est alors sélectionné, le fragment B doit à nouveau être affiché. À ce stade, il devrait être possible d'utiliser le bouton de retour pour afficher le fragment A.

En outre, il est important que l'état de chaque onglet soit conservé lorsque le périphérique est tourné.

BR Martin

mardah
la source

Réponses:

23

Le framework ne le fera actuellement pas automatiquement pour vous. Vous devrez créer et gérer vos propres backstacks pour chaque onglet.

Pour être honnête, cela semble être une chose vraiment discutable à faire. Je ne peux pas imaginer que cela se traduise par une interface utilisateur décente - si la touche arrière va faire des choses différentes en fonction de l'onglet que je suis, surtout si la touche arrière a également son comportement normal de fermeture de toute l'activité lorsqu'elle est en haut de la pile ... semble méchante.

Si vous essayez de créer quelque chose comme une interface utilisateur de navigateur Web, pour obtenir une expérience utilisateur naturelle pour l'utilisateur, cela impliquera de nombreux ajustements subtils de comportement en fonction du contexte, vous devrez donc certainement faire votre propre back stack. gestion plutôt que de compter sur une implémentation par défaut dans le cadre. Par exemple, essayez de prêter attention à la façon dont la touche de retour interagit avec le navigateur standard des différentes manières dont vous pouvez entrer et sortir. (Chaque «fenêtre» du navigateur est essentiellement un onglet.)

hackbod
la source
7
Ne fais pas ça. Et le cadre n'est guère inutile. Cela ne vous donne pas de support automatique pour ce genre de chose, qui, comme je l'ai dit, je ne peux pas imaginer aboutir à une expérience utilisateur décente, sauf dans des situations très spécialisées où vous devrez de toute façon contrôler soigneusement le comportement du dos.
hackbod
9
Ce type de navigation, puis vous avez des onglets et une hiérarchie de pages sur chaque onglet est très courant pour les applications iPhone par exemple (vous pouvez consulter les applis App Store et iPod). Je trouve leur expérience utilisateur assez décente.
Dmitry Ryadnenko
13
C'est insensé. L'iPhone n'a même pas de bouton de retour. Il existe des démos d'API montrant un code très simple pour implémenter des fragments dans des onglets. La question posée portait sur le fait d'avoir différentes piles de retour pour chaque onglet, et ma réponse est que le framework ne fournit pas cela automatiquement parce que sémantiquement pour ce que fait le bouton de retour, ce serait probablement une expérience utilisateur merdique. Cependant, vous pouvez assez facilement implémenter vous-même la sémantique arrière si vous le souhaitez.
hackbod
4
Encore une fois, l'iPhone n'a pas de bouton de retour, il n'a donc pas sémantiquement un comportement de pile arrière comme Android. De plus, «mieux vaut simplement m'en tenir aux activités et me gagner beaucoup de temps» n'a aucun sens ici, car les activités ne vous permettent pas de mettre des onglets à jour dans une interface utilisateur avec leurs propres piles de dos différentes; en fait, la gestion des activités de back-stack est moins flexible que ce qui est fourni par le framework Fragment.
hackbod
22
@hackbod J'essaie de suivre vos points, mais j'ai du mal à implémenter un comportement de back-stack personnalisé. Je me rends compte qu'ayant été impliqué dans la conception de ceci, vous auriez une idée solide de la facilité avec laquelle cela pourrait être. Est-il possible qu'il existe une application de démonstration pour le cas d'utilisation de l'OP, car c'est vraiment une situation très courante, en particulier pour ceux d'entre nous qui doivent écrire et porter des applications iOS pour les clients qui font ces demandes .... des backstacks de fragments dans chaque FragmentActivity.
Richard Le Mesurier
138

Je suis terriblement en retard pour cette question. Mais comme ce fil a été très instructif et utile pour moi, j'ai pensé que je ferais mieux de poster mes deux pence ici.

J'avais besoin d'un flux d'écran comme celui-ci (un design minimaliste avec 2 onglets et 2 vues dans chaque onglet),

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

J'avais les mêmes exigences dans le passé, et je l'ai fait en utilisant TabActivityGroup(qui était également obsolète à l'époque) et Activities. Cette fois, je voulais utiliser des fragments.

Voilà donc comment je l'ai fait.

1. Créez une classe de fragments de base

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

Tous les fragments de votre application peuvent étendre cette classe de base. Si vous souhaitez utiliser des fragments spéciaux comme ListFragmentvous devez créer une classe de base pour cela également. Vous serez clair sur l'utilisation de onBackPressed()et onActivityResult()si vous lisez le message dans son intégralité.

2. Créez des identifiants d'onglets, accessibles partout dans le projet

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

rien à expliquer ici ..

3. Ok, Activité de l'onglet principal - Veuillez lire les commentaires dans le code.

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 (au cas où quelqu'un serait intéressé.)

<?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 (premier fragment de l'onglet A, similaire pour tous les onglets)

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

Ce n'est peut-être pas la manière la plus raffinée et la plus correcte. Mais cela a très bien fonctionné dans mon cas. De plus, je n'avais cette exigence qu'en mode portrait. Je n'ai jamais eu à utiliser ce code dans un projet prenant en charge les deux orientations. Je ne peux donc pas dire à quel genre de défis je suis confronté là-bas

ÉDITER :

Si quelqu'un veut un projet complet, j'ai poussé un exemple de projet sur github .

Krishnabhadra
la source
2
Stocker les données de chaque fragment, recréer chacun d'entre eux, reconstruire les piles ... autant de travail pour un simple changement d'orientation.
Michael Eilers Smith
3
@omegatai entièrement d'accord avec vous .. Tout le problème se pose car Android ne gère pas la pile pour nous ( ce que fait iOS et le changement d'orientation ou l'onglet avec plusieurs fragments est un jeu d'enfant ) et qui nous ramène à la discussion originale dans ce Q / Un fil. Pas bon de revenir là
dessus
1
@Renjith C'est parce que le fragment est recréé à chaque fois, lorsque vous changez d'onglet .. Ne pensez pas même une fois que votre fragment est réutilisé à travers le changement d'onglet. lorsque je passe de l'onglet A à B, l'onglet A est libéré de la mémoire. Donc, enregistrez vos données dans l'activité et vérifiez à chaque fois si l'activité contient des données avant d'essayer de les obtenir du serveur.
Krishnabhadra
2
@Krishnabhadra Ok, ça sonne beaucoup mieux. Permettez-moi de corriger au cas où je me trompe. Selon votre exemple, il n'y a qu'une seule activité et donc un seul bundle. Créez des instances d'adaptateur dans BaseFragment (en référence à votre projet) et enregistrez-y les données. Utilisez-les chaque fois que la vue doit être construite.
Renjith
1
Ça marche. Merci beaucoup. Le téléchargement de l'ensemble du projet était une bonne idée! :-)
Vinay W
96

Nous avons dû mettre en œuvre exactement le même comportement que vous décrivez récemment pour une application. Les écrans et le flux global de l'application étaient déjà définis, il fallait donc s'y tenir (c'est un clone d'application iOS ...). Heureusement, nous avons réussi à nous débarrasser des boutons de retour à l'écran :)

Nous avons piraté la solution en utilisant un mélange de TabActivity, FragmentActivities (nous utilisions la bibliothèque de support pour les fragments) et Fragments. Rétrospectivement, je suis à peu près sûr que ce n'était pas la meilleure décision d'architecture, mais nous avons réussi à faire fonctionner la chose. Si je devais le refaire, j'essaierais probablement de faire une solution plus basée sur l'activité (pas de fragments), ou d'essayer de n'avoir qu'une seule activité pour les onglets et de laisser tout le reste être des vues (ce que je trouve est beaucoup plus réutilisable que les activités en général).

Les exigences étaient donc d'avoir des onglets et des écrans imbriqués dans chaque onglet:

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

etc...

Disons donc: l'utilisateur démarre dans l'onglet 1, navigue de l'écran 1 à l'écran 2 puis à l'écran 3, il passe ensuite à l'onglet 3 et navigue de l'écran 4 à 6; s'il revient à l'onglet 1, il doit revoir l'écran 3 et s'il appuie sur Retour, il doit revenir à l'écran 2; De retour et il est à l'écran 1; passez à l'onglet 3 et il est à nouveau à l'écran 6.

L'activité principale de l'application est MainTabActivity, qui étend TabActivity. Chaque onglet est associé à une activité, disons ActivityInTab1, 2 et 3. Et puis chaque écran sera un fragment:

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

Chaque ActivityInTab ne contient qu'un seul fragment à la fois et sait comment remplacer un fragment par un autre (à peu près la même chose qu'un ActvityGroup). Ce qui est cool, c'est qu'il est assez facile de conserver des piles arrière séparées pour chaque onglet de cette façon.

La fonctionnalité de chaque ActivityInTab était tout à fait la même: savoir naviguer d'un fragment à un autre et maintenir une pile arrière, nous avons donc mis cela dans une classe de base. Appelons cela simplement 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();
    }

}

Le fichier activity_in_tab.xml est juste ceci:

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

Comme vous pouvez le voir, la disposition de la vue pour chaque onglet était la même. C'est parce que c'est juste un FrameLayout appelé contenu qui contiendra chaque fragment. Les fragments sont ceux qui ont la vue de chaque écran.

Juste pour les points bonus, nous avons également ajouté un petit code pour afficher une boîte de dialogue de confirmation lorsque l'utilisateur appuie sur Retour et qu'il n'y a plus de fragments vers lesquels revenir:

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

C'est à peu près la configuration. Comme vous pouvez le voir, chaque FragmentActivity (ou tout simplement Activity dans Android> 3) s'occupe de tout le back-stack avec son propre FragmentManager.

Une activité comme ActivityInTab1 sera vraiment simple, elle montrera juste son premier fragment (c'est-à-dire l'écran):

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

Ensuite, si un fragment a besoin de naviguer vers un autre fragment, il doit faire un petit casting méchant ... mais ce n'est pas si mal:

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

C'est donc à peu près tout. Je suis à peu près sûr que ce n'est pas une solution très canonique (et surtout pas très bonne), alors j'aimerais demander aux développeurs Android chevronnés quelle serait la meilleure approche pour obtenir cette fonctionnalité, et si ce n'est pas "comment c'est done "sous Android, j'apprécierais si vous pouviez m'indiquer un lien ou du matériel qui explique quelle est la manière Android d'aborder cela (onglets, écrans imbriqués dans les onglets, etc.). N'hésitez pas à déchirer cette réponse dans les commentaires :)

Comme signe que cette solution n'est pas très bonne, c'est que récemment, j'ai dû ajouter des fonctionnalités de navigation à l'application. Un bouton bizarre qui devrait emmener l'utilisateur d'un onglet à un autre et dans un écran imbriqué. Faire cela par programme était une douleur dans le cul, à cause des problèmes qui-sait-qui et de gérer quand les fragments et les activités sont réellement instanciés et initialisés. Je pense que cela aurait été beaucoup plus facile si ces écrans et ces onglets n'étaient vraiment que des vues.


Enfin, si vous devez survivre aux changements d'orientation, il est important que vos fragments soient créés à l'aide de setArguments / getArguments. Si vous définissez des variables d'instance dans les constructeurs de vos fragments, vous serez foutu. Mais heureusement, c'est vraiment facile à corriger: il suffit de sauvegarder tout dans setArguments dans le constructeur, puis de récupérer ces choses avec getArguments dans onCreate pour les utiliser.

épidémien
la source
13
Excellente réponse mais je pense que très peu le verront. J'ai choisi exactement le même chemin (comme vous pouvez le voir dans la conversation dans la réponse précédente) et je ne suis pas content comme vous. Je pense que Google a vraiment merdé avec ces fragments car cette API ne couvre pas les cas d'utilisation majeurs. Un autre problème que vous pouvez rencontrer est l'impossibilité d'intégrer un fragment dans un autre fragment.
Dmitry Ryadnenko
Merci pour le commentaire boulder. Ouais, je ne pourrais pas être plus d'accord sur l'API des fragments. J'ai déjà rencontré le problème des fragments imbriqués (c'est pourquoi nous avons opté pour l'approche "remplacer un fragment par un autre" hehe).
epidemian le
1
J'ai mis en œuvre cela via TOUTES les activités. Je n'ai pas aimé ce que j'ai et je vais essayer Fragments. C'est le contraire de votre expérience! Il existe de nombreuses implémentations avec les activités pour gérer le cycle de vie des vues enfants dans chaque onglet et également pour implémenter votre propre bouton de retour. De plus, vous ne pouvez pas simplement garder une référence à toutes les vues ou vous ferez exploser la mémoire. J'espère que les fragments: 1) prendront en charge le cycle de vie des fragments avec une séparation claire de la mémoire et 2) aideront à implémenter la fonctionnalité du bouton retour De plus, si vous utilisez des fragments pour ce processus, ne sera-t-il pas plus facile à exécuter sur les tablettes?
gregm
Que se passe-t-il lorsque l'utilisateur change d'onglet? La backstack Fragment est-elle supprimée? Comment s'assurer que la backstack reste?
gregm
1
@gregm Si vous allez à 1 onglet <-> 1 activité comme je l'ai fait, la backstack de chaque onglet restera lorsque les onglets sont commutés car les activités sont en fait maintenues en vie; ils sont seulement mis en pause et repris. Je ne sais pas s'il existe un moyen de détruire et de recréer les activités lorsque les onglets sont commutés dans un TabActivity. Cependant, si vous faites remplacer les fragments à l'intérieur des activités comme je l'ai suggéré, ils sont détruits (et recréés lorsque la backstack est sautée). Ainsi, vous aurez au plus un fragment vivant par onglet à tout moment.
epidemian le
6

Le stockage de références fortes à des fragments n'est pas la bonne manière.

FragmentManager fournit putFragment(Bundle, String, Fragment)et saveFragmentInstanceState(Fragment).

L'un ou l'autre suffit pour implémenter une backstack.


En utilisant putFragment, au lieu de remplacer un fragment, vous détachez l'ancien et ajoutez le nouveau. C'est ce que fait le framework à une transaction de remplacement qui est ajoutée à la backstack. putFragmentstocke un index de la liste actuelle des fragments actifs et ces fragments sont enregistrés par l'infrastructure lors des changements d'orientation.

La deuxième façon, en utilisant saveFragmentInstanceState, enregistre l'état du fragment entier dans un Bundle, ce qui vous permet de vraiment le supprimer, plutôt que de le détacher. L'utilisation de cette approche facilite la manipulation de la pile arrière, car vous pouvez afficher un fragment quand vous le souhaitez.


J'ai utilisé la deuxième méthode pour ce cas d'utilisation:

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

Je ne veux pas que l'utilisateur revienne à l'écran d'inscription, à partir du troisième, en appuyant sur le bouton retour. Je fais aussi des animations inversées entre eux (en utilisant onCreateAnimation), donc les solutions hacky ne fonctionneront pas, du moins sans que l'utilisateur ne remarque clairement que quelque chose ne va pas.

Il s'agit d'un cas d'utilisation valide pour une backstack personnalisée, faisant ce que l'utilisateur attend ...

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
la source
2

Avertissement:


Je pense que c'est le meilleur endroit pour publier une solution connexe sur laquelle j'ai travaillé pour un type de problème similaire qui semble être un problème Android assez standard. Cela ne résoudra pas le problème pour tout le monde, mais cela peut en aider certains.


Si la principale différence entre vos fragments réside uniquement dans les données qui les sauvegardent (c'est-à-dire qu'il n'y a pas beaucoup de grandes différences de mise en page), vous n'aurez peut-être pas besoin de remplacer réellement le fragment, mais simplement d'échanger les données sous-jacentes et d'actualiser la vue.

Voici une description d'un exemple possible pour cette approche:

J'ai une application qui utilise ListViews. Chaque élément de la liste est un parent avec un certain nombre d'enfants. Lorsque vous appuyez sur l'élément, une nouvelle liste doit s'ouvrir avec ces enfants, dans le même onglet ActionBar que la liste d'origine. Ces listes imbriquées ont une présentation très similaire (quelques ajustements conditionnels ici et là peut-être), mais les données sont différentes.

Cette application a plusieurs couches de progéniture sous la liste parente initiale et nous pouvons ou non avoir des données du serveur au moment où un utilisateur tente d'accéder à une certaine profondeur au-delà de la première. Étant donné que la liste est construite à partir d'un curseur de base de données et que les fragments utilisent un chargeur de curseur et un adaptateur de curseur pour remplir la vue de liste avec des éléments de liste, tout ce qui doit se produire lorsqu'un clic est enregistré est:

1) Créez un nouvel adaptateur avec les champs «à» et «de» appropriés qui correspondent aux nouvelles vues d'élément ajoutées à la liste et aux colonnes renvoyées par le nouveau curseur.

2) Définissez cet adaptateur comme nouvel adaptateur pour ListView.

3) Créez un nouvel URI basé sur l'élément sur lequel vous avez cliqué et redémarrez le chargeur de curseur avec le nouvel URI (et la nouvelle projection). Dans cet exemple, l'URI est mappé à des requêtes spécifiques avec les arguments de sélection transmis depuis l'interface utilisateur.

4) Lorsque les nouvelles données ont été chargées à partir de l'URI, permutez le curseur associé à l'adaptateur sur le nouveau curseur, et la liste sera alors actualisée.

Il n'y a pas de backstack associé à cela car nous n'utilisons pas de transactions, vous devrez donc soit créer les vôtres, soit lire les requêtes à l'envers lors du retrait de la hiérarchie. Lorsque j'ai essayé cela, les requêtes étaient suffisamment rapides pour que je les exécute à nouveau dans oNBackPressed () jusqu'à ce que je sois en haut de la hiérarchie, auquel cas le framework reprend le bouton Précédent.

Si vous vous trouvez dans une situation similaire, assurez-vous de lire la documentation: http://developer.android.com/guide/topics/ui/layout/listview.html

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

J'espère que ça aidera quelqu'un!

courtf
la source
Au cas où quelqu'un ferait cela, et utiliserait également un SectionIndexer (tel que AlphabetIndexer), vous remarquerez peut-être qu'après le remplacement de l'adaptateur, votre défilement rapide ne fonctionne pas. Une sorte de bogue malheureux, mais le remplacement de l'adaptateur, même par un tout nouvel indexeur, ne met pas à jour la liste des sections utilisées par FastScroll. Il existe une solution de contournement, veuillez consulter: description du problème et solution de contournement
courtf
2

J'ai eu exactement le même problème et j'ai implémenté un projet github open source qui couvre les onglets empilés, la navigation arrière et supérieure et qui est bien testé et documenté:

https://github.com/SebastianBaltesObjectCode/PersistentFragmentTabs

Il s'agit d'un cadre simple et petit pour les onglets de navigation, la commutation de fragments et la gestion de la navigation ascendante et arrière. Chaque onglet a sa propre pile de fragments. Il utilise ActionBarSherlock et est compatible avec le niveau API 8.

Sébastien Baltes
la source
2

C'est un problème complexe car Android ne gère qu'une seule pile arrière, mais c'est faisable. Il m'a fallu des jours pour créer une bibliothèque appelée Tab Stacker qui fait exactement ce que vous recherchez: un historique des fragments pour chaque onglet. Il est open source et entièrement documenté, et peut être facilement inclus avec gradle. Vous pouvez trouver la bibliothèque sur github: https://github.com/smart-fun/TabStacker

Vous pouvez également télécharger l'exemple d'application pour voir que le comportement correspond à vos besoins:

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

Si vous avez des questions, n'hésitez pas à nous envoyer un mail.

Arnaud SmartFun
la source
2

Je voudrais suggérer ma propre solution au cas où quelqu'un chercherait et voudrait essayer de choisir la meilleure pour ses besoins.

https://github.com/drusak/tabactivity

Le but de la création de la bibliothèque est assez banal - implémentez-la comme un iPhone.

Les principaux avantages:

  • utilisez la bibliothèque android.support.design avec TabLayout;
  • chaque onglet a sa propre pile en utilisant FragmentManager (sans enregistrer les références des fragments);
  • prise en charge des liens profonds (lorsque vous devez ouvrir un onglet spécifique et un niveau de fragment spécifique);
  • sauvegarde / restauration des états des onglets;
  • méthodes de cycle de vie adaptatives des fragments dans les onglets;
  • assez facile à mettre en œuvre pour vos besoins.
Kasurd
la source
Merci, cela a été très utile. J'ai besoin d'utiliser ListFragments en plus de Fragments donc j'ai dupliqué BaseTabFragment.java en BaseTabListFragment.java et l'ai fait étendre ListFragment. Ensuite, j'ai dû changer différentes parties du code où il supposait toujours s'attendre à un BaseTabFragment. Y a-t-il un meilleur moyen?
primehalo
Malheureusement, je n'ai pas pensé à ListFragment. Techniquement, c'est la bonne solution, mais elle nécessitera des vérifications supplémentaires pour TabFragment et son instanceOf BaseTabListFragment. Une autre approche pour utiliser Fragment avec ListView à l'intérieur (exactement la même chose que ListFragment implémentée). Je vais y réfléchir. Merci de m'avoir pointé là-dessus!
kasurd
1

Une solution simple:

Chaque fois que vous changez d'appel d'onglet / vue racine:

fragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);

Cela effacera le BackStack. N'oubliez pas d'appeler cela avant de modifier le fragment racine.

Et ajoutez des fragments avec ceci:

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

Notez que le .addToBackStack(null)et le transaction.addpeuvent être modifiés par exemple avec transaction.replace.

Morten Holmgaard
la source
-1

Ce fil était très très intéressant et utile.
Merci Krishnabhadra pour votre explication et votre code, j'utilise votre code et je l'ai amélioré un peu, permettant de persister les stacks, currentTab, etc ... à partir du changement de configuration (rotation principalement).
Testé sur de vrais appareils 4.0.4 et 2.3.6, non testé sur émulateur

Je change cette partie de code sur "AppMainTabActivity.java", le reste reste le même. Peut-être que Krishnabhadra ajoutera ceci sur son code.

Récupérer des données surCréer:

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

Enregistrez les variables et mettez-les dans Bundle:

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

S'il existe un CurrentTab précédent, définissez ceci, sinon créez un nouveau 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);
    }
}

J'espère que cela aide d'autres personnes.

Sulfkain
la source
C'est faux. Lorsque onCreate est appelé avec un Bundle, ces fragments ne seront pas les mêmes que ceux qui seront affichés à l'écran et vous perdez les anciens, sauf si vous utilisez setRetainInstance. Et si le ActivityManager «enregistre» votre activité, puisqu'un fragment n'est ni sérialisable ni parcellable, lorsque l'utilisateur revient à votre activité, il plante.
sergio91pt
-1

Je recommanderais de ne pas utiliser de backstack basé sur HashMap> il y a beaucoup de bogues en mode "ne pas garder les activités". Il ne restaurera pas correctement l'état au cas où vous seriez profondément dans la pile du fragment. Et sera également créé dans un fragment de carte imbriqué (à l'exception: Fragment aucune vue trouvée pour l'ID). Coz HashMap> après l'arrière-plan \ l'application de premier plan sera nulle

J'optimise le code ci-dessus pour travailler avec la backstack de fragment

C'est en bas TabView

Classe d'activité principale

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>

entrez la description de l'image ici

Flinbor
la source