Separare lo stack posteriore per ogni scheda in Android usando Frammenti


158

Sto cercando di implementare le schede per la navigazione in un'app Android. Dato che TabActivity e ActivityGroup sono obsoleti, vorrei implementarlo utilizzando invece i frammenti.

So come impostare un frammento per ogni scheda e quindi cambiare i frammenti quando si fa clic su una scheda. Ma come posso avere uno stack posteriore separato per ogni scheda?

Ad esempio, il frammento A e B si trova nella scheda 1 e il frammento C e D nella scheda 2. All'avvio dell'app, viene visualizzato il frammento A e la scheda 1 è selezionata. Quindi il frammento A potrebbe essere sostituito con il frammento B. Quando è selezionata la scheda 2, il frammento C dovrebbe essere visualizzato. Se si seleziona la scheda 1, il frammento B dovrebbe essere nuovamente visualizzato. A questo punto dovrebbe essere possibile utilizzare il pulsante Indietro per mostrare il frammento A.

Inoltre, è importante che lo stato di ciascuna scheda venga mantenuto quando il dispositivo viene ruotato.

BR Martin

Risposte:


23

Al momento il framework non lo farà automaticamente. Dovrai creare e gestire i tuoi stack posteriori per ogni scheda.

Ad essere sincero, questa sembra una cosa davvero discutibile da fare. Non riesco a immaginarlo con un'interfaccia utente decente - se il tasto Indietro farà cose diverse a seconda della scheda che sono, specialmente se il tasto Indietro ha anche il suo normale comportamento di chiudere l'intera attività quando si è in cima a lo stack ... sembra cattivo.

Se stai cercando di creare qualcosa di simile a un'interfaccia utente del browser Web, ottenere una UX naturale per l'utente comporterà molte sottili modifiche del comportamento a seconda del contesto, quindi dovrai sicuramente fare il tuo back stack gestione piuttosto che fare affidamento su alcune implementazioni predefinite nel framework. Per un esempio, prova a prestare attenzione a come il tasto Indietro interagisce con il browser standard nei vari modi in cui puoi entrare e uscire da esso. (Ogni "finestra" nel browser è essenzialmente una scheda.)


7
Non farlo. E il quadro non è quasi inutile. Non ti dà supporto automatico per questo tipo di cose, che come ho detto non riesco a immaginare di ottenere un'esperienza utente decente se non in situazioni molto specializzate in cui dovrai controllare attentamente il comportamento della schiena.
hackbod,

9
Questo tipo di navigazione, quindi hai schede e gerarchia di pagine su ogni scheda è molto comune, ad esempio, per le applicazioni iPhone (puoi controllare App Store e app iPod). Trovo la loro esperienza utente abbastanza decente.
Dmitry Ryadnenko l'

13
Questo è folle. L'iPhone non ha nemmeno un pulsante indietro. Ci sono demo API che mostrano un codice molto semplice per implementare frammenti nelle schede. La domanda che si poneva riguardava la presenza di back stack diversi per ciascuna scheda e la mia risposta è che il framework non lo fornisce automaticamente perché semanticamente per ciò che fa il pulsante back sarebbe molto probabilmente un'esperienza utente scadente. Puoi implementare facilmente la semantica posteriore se vuoi.
hackbod,

4
Ancora una volta, l'iPhone non ha un pulsante Indietro, quindi non ha semanticamente un comportamento di back stack come Android. Anche "meglio attenersi semplicemente alle attività e risparmiarmi un sacco di tempo" non ha alcun senso qui, perché le attività non ti permettono di mettere schede di mantenimento in un'interfaccia utente con i loro diversi back stack; infatti la gestione back stack delle attività è meno flessibile rispetto a quella fornita dal framework Fragment.
hackbod,

22
@hackbod Sto cercando di seguire i tuoi punti, ma ho riscontrato problemi nell'implementazione del comportamento di back-stack personalizzato. Mi rendo conto che dopo essere stato coinvolto nella progettazione di questo, avresti una solida visione di quanto potrebbe essere facile. È possibile che ci sia un'app demo per il caso d'uso dell'OP, in quanto è davvero una situazione molto comune, specialmente per quelli di noi che devono scrivere e trasferire app iOS per i clienti che effettuano queste richieste .... gestione separata zaini di frammenti all'interno di ogni FragmentActivity.
Richard Le Mesurier,

138

Sono terribilmente in ritardo a questa domanda. Ma poiché questa discussione è stata molto istruttiva e utile per me, ho pensato di pubblicare qui i miei due penny.

Avevo bisogno di un flusso di schermo come questo (un design minimalista con 2 schede e 2 visualizzazioni in ogni scheda),

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

Avevo gli stessi requisiti in passato e l'ho fatto usando TabActivityGroup(che a quel tempo era deprecato) e Attività. Questa volta ho voluto usare i frammenti.

Quindi è così che l'ho fatto.

1. Creare una classe di frammenti di 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){
    }
}

Tutti i frammenti nella tua app possono estendere questa classe Base. Se si desidera utilizzare frammenti speciali come quelli, ListFragmentè necessario creare anche una classe base. Sarai chiaro sull'uso di onBackPressed()e onActivityResult()se leggi il post per intero ..

2. Creare alcuni identificativi Tab, accessibili ovunque nel progetto

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

niente da spiegare qui ..

3. Ok, attività della scheda principale- Ti preghiamo di leggere i commenti nel codice.

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 (Nel caso qualcuno fosse interessato.)

<?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 (primo frammento nella scheda A, simliar per tutte le schede)

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

Questo potrebbe non essere il modo più lucido e corretto. Ma ha funzionato magnificamente nel mio caso. Inoltre avevo questo requisito solo in modalità verticale. Non ho mai dovuto usare questo codice in un progetto a supporto di entrambi gli orientamenti. Quindi non posso dire che tipo di sfide devo affrontare lì ..

MODIFICARE :

Se qualcuno vuole un progetto completo, ho inviato un progetto campione a github .


2
Memorizzare i dati per ogni frammento, ricrearli tutti, ricostruire le pile ... così tanto lavoro per un semplice cambio di orientamento.
Michael Eilers Smith,

3
@omegatai è completamente d'accordo con te .. Tutto il problema sorge dal momento che Android non gestisce lo stack per noi ( cosa che iOS fa e il cambio di orientamento o la scheda con più frammenti è un gioco da ragazzi ) e che ci riporta alla discussione originale in questo Q / Un filo. Non va bene tornarci adesso.
Krishnabhadra il

1
@Renjith Questo perché il frammento viene ricreato ogni volta, quando si cambia scheda. Non pensare nemmeno una volta, il frammento viene riutilizzato attraverso il cambio di scheda. quando passo da una scheda a B, una scheda viene liberata dalla memoria. Quindi salva i tuoi dati in attività e ogni volta controlla se l'attività contiene dati prima di provare a recuperarli dal server.
Krishnabhadra

2
@Krishnabhadra Ok, sembra molto meglio. Fammi correggere nel caso in cui mi sbagli. Come nel tuo esempio, c'è solo un'attività e quindi un pacchetto. Crea istanze dell'adattatore in BaseFragment (facendo riferimento al tuo progetto) e salva i dati al suo interno. Usali ogni volta che devi costruire la vista.
Renjith

1
Ha funzionato. Molte grazie. Il caricamento dell'intero progetto è stata una buona idea! :-)
Vinay W

96

Abbiamo dovuto implementare esattamente lo stesso comportamento descritto di recente per un'app. Le schermate e il flusso generale dell'applicazione erano già definiti, quindi dovevamo attenerci (è un clone di app iOS ...). Fortunatamente, siamo riusciti a sbarazzarci dei pulsanti indietro sullo schermo :)

Abbiamo hackerato la soluzione usando una combinazione di TabActivity, FragmentActivities (stavamo usando la libreria di supporto per i frammenti) e Fragments. In retrospettiva, sono abbastanza sicuro che non sia stata la migliore decisione sull'architettura, ma siamo riusciti a far funzionare la cosa. Se dovessi farlo di nuovo, probabilmente proverei a fare una soluzione più basata sulle attività (senza frammenti), o provare ad avere solo un'attività per le schede e lasciare che tutte le altre siano viste (che trovo siano molto più riutilizzabile rispetto alle attività complessive).

Quindi i requisiti erano avere alcune schede e schermate annidabili in ciascuna scheda:

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

eccetera...

Quindi dire: l'utente inizia nella scheda 1, passa dalla schermata 1 alla schermata 2 quindi alla schermata 3, quindi passa alla scheda 3 e naviga dalla schermata 4 alla 6; se è tornato alla scheda 1, dovrebbe vedere di nuovo la schermata 3 e se ha premuto Indietro dovrebbe tornare alla schermata 2; Torna di nuovo ed è nella schermata 1; passa alla scheda 3 ed è di nuovo nella schermata 6.

L'attività principale nell'applicazione è MainTabActivity, che estende TabActivity. Ogni scheda è associata a un'attività, diciamo ActivityInTab1, 2 e 3. E quindi ogni schermata sarà un frammento:

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

Ogni ActivityInTab contiene solo un frammento alla volta e sa come sostituire un frammento con un altro (praticamente uguale a un ActvityGroup). La cosa bella è che è abbastanza facile mantenere stack posteriori separati per ogni scheda in questo modo.

Le funzionalità di ciascun ActivityInTab erano piuttosto le stesse: sapere come spostarsi da un frammento all'altro e mantenere uno stack posteriore, quindi lo inseriamo in una classe base. Chiamiamolo semplicemente 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 è proprio questo:

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

Come puoi vedere, il layout della vista per ogni scheda era lo stesso. Questo perché è solo un FrameLayout chiamato contenuto che conterrà ogni frammento. I frammenti sono quelli che hanno la vista di ogni schermo.

Solo per i punti bonus, abbiamo anche aggiunto un po 'di codice per mostrare una finestra di dialogo di conferma quando l'utente preme Indietro e non ci sono più frammenti su cui tornare:

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

Questo è praticamente il setup. Come puoi vedere, ogni FragmentActivity (o semplicemente semplicemente Activity in Android> 3) si occupa di tutto il back-stacking con il suo FragmentManager.

Un'attività come ActivityInTab1 sarà davvero semplice, mostrerà solo il suo primo frammento (cioè lo schermo):

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

Quindi, se un frammento deve navigare verso un altro frammento, deve eseguire un casting un po 'brutto ... ma non è poi così male:

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

Quindi è praticamente tutto. Sono abbastanza sicuro che questa non sia una soluzione molto canonica (e per lo più sicuramente non molto buona), quindi vorrei chiedere agli sviluppatori Android esperti quale sarebbe un approccio migliore per raggiungere questa funzionalità, e se questo non è "come è fatto "in Android, ti sarei grato se potessi indicarmi qualche link o materiale che spieghi quale sia il modo Android di avvicinarsi a questo (schede, schermate nidificate nelle schede, ecc.). Sentiti libero di dividere questa risposta nei commenti :)

Come segno che questa soluzione non è molto buona è che recentemente ho dovuto aggiungere alcune funzionalità di navigazione all'applicazione. Qualche bizzarro pulsante che dovrebbe portare l'utente da una scheda all'altra e in una schermata nidificata. Fare questo a livello di programmazione è stato un dolore nel sedere, a causa di chissà chi e chi affronta quando sono frammenti e attività effettivamente istanziati e inizializzati. Penso che sarebbe stato molto più semplice se quelle schermate e schede fossero davvero tutte viste.


Infine, se devi sopravvivere ai cambiamenti di orientamento, è importante che i tuoi frammenti vengano creati usando setArguments / getArguments. Se imposti le variabili di istanza nei costruttori dei tuoi frammenti sarai fregato. Ma per fortuna è davvero facile da risolvere: basta salvare tutto in setArguments nel costruttore e quindi recuperare quelle cose con getArguments in onCreate per usarle.


13
Ottima risposta ma penso che pochissimi lo vedranno. Ho scelto esattamente lo stesso percorso (come puoi vedere dalla conversazione nella risposta precedente) e non sono contento proprio come te. Penso che Google abbia davvero rovinato questi frammenti poiché questa API non copre i principali casi d'uso. Un altro problema che potresti incontrare è l'impossibilità di incorporare il frammento in un altro frammento.
Dmitry Ryadnenko,

Grazie per il commento boulder. Sì, non potrei essere più d'accordo sull'API dei frammenti. Ho già incontrato il problema dei frammenti nidificati (ecco perché abbiamo optato per l'approccio "sostituisci un frammento con un altro" hehe).
epidemia,

1
L'ho implementato tramite TUTTE le attività. Non mi è piaciuto quello che ho e proverò Fragments. Questo è l'opposto della tua esperienza! C'è molta implementazione con Attività per gestire il ciclo di vita delle viste figlio in ogni scheda e anche per implementare il tuo pulsante Indietro. Inoltre, non puoi semplicemente tenere un riferimento a tutte le viste o farai esplodere la memoria. Spero che i frammenti: 1) Supportino il ciclo di vita dei frammenti con una chiara separazione della memoria e 2) aiutino a implementare la funzionalità del pulsante Indietro Inoltre, se si utilizzano frammenti per questo processo, non sarà più facile eseguirli su tablet?
Greg

Cosa succede quando l'utente cambia scheda? Il backstack di frammenti viene eliminato? Come assicurarsi che lo zaino rimanga?
Greg

1
@gregm Se vai a 1 scheda <-> 1 attività come ho fatto io, il backstack per ogni scheda rimarrà quando le schede vengono cambiate perché le attività vengono effettivamente mantenute in vita; vengono solo messi in pausa e ripresi. Non so se esiste un modo per distruggere e ricreare le attività quando le schede vengono commutate in TabActivity. Tuttavia, se si fanno sostituire i frammenti all'interno delle attività come da me suggerito, vengono distrutti (e ricreati quando viene fatto scattare lo backstack). Quindi avrai al massimo un frammento in vita per scheda in qualsiasi momento.
epidemia il


6

La memorizzazione di riferimenti forti ai frammenti non è il modo corretto.

FragmentManager fornisce putFragment(Bundle, String, Fragment)e saveFragmentInstanceState(Fragment).

O uno è sufficiente per implementare uno zaino.


Usando putFragment, invece di sostituire un frammento, si stacca quello vecchio e si aggiunge quello nuovo. Questo è ciò che fa il framework per una transazione di sostituzione che viene aggiunta al backstack. putFragmentmemorizza un indice nell'elenco corrente di frammenti attivi e tali frammenti vengono salvati dal framework durante i cambiamenti di orientamento.

Il secondo modo, usando saveFragmentInstanceState, salva l'intero stato del frammento in un Bundle permettendoti di rimuoverlo davvero, piuttosto che staccarlo. L'uso di questo approccio semplifica la manipolazione del back stack, in quanto è possibile estrarre un frammento quando lo si desidera.


Ho usato il secondo metodo per questo caso d'uso:

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

Non voglio che l'utente ritorni alla schermata Iscriviti, dalla terza, premendo il pulsante Indietro. Faccio anche capovolgere le animazioni tra loro (usando onCreateAnimation), quindi le soluzioni hacky non funzioneranno, almeno senza che l'utente si accorga chiaramente che qualcosa non va.

Questo è un caso d'uso valido per uno zaino personalizzato, facendo ciò che l'utente si aspetta ...

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

2

Disclaimer:


Penso che questo sia il posto migliore per pubblicare una soluzione correlata su cui ho lavorato per un tipo di problema simile che sembra essere roba Android piuttosto standard. Non risolverà il problema per tutti, ma potrebbe aiutare alcuni.


Se la differenza principale tra i tuoi frammenti è solo il dato che li esegue il backup (cioè, non molte differenze di layout), potrebbe non essere necessario sostituire effettivamente il frammento, ma semplicemente scambiare i dati sottostanti e aggiornare la vista.

Ecco una descrizione di un possibile esempio per questo approccio:

Ho un'app che utilizza ListViews. Ogni elemento nell'elenco è un genitore con un numero di figli. Quando si tocca l'elemento, è necessario aprire un nuovo elenco con tali elementi secondari, all'interno della stessa scheda ActionBar dell'elenco originale. Questi elenchi nidificati hanno un layout molto simile (alcune modifiche condizionali qua e là forse), ma i dati sono diversi.

Questa app ha diversi livelli di discendenza sotto l'elenco genitore iniziale e potremmo avere o meno dati dal server quando un utente tenta di accedere a una certa profondità oltre la prima. Poiché l'elenco è costruito da un cursore del database e i frammenti utilizzano un caricatore e un adattatore cursore per popolare la vista elenco con elementi di elenco, tutto ciò che deve accadere quando si registra un clic è:

1) Creare un nuovo adattatore con i campi "da" e "da" appropriati che corrisponderanno alle nuove viste degli elementi aggiunte all'elenco e alle colonne restituite dal nuovo cursore.

2) Impostare questo adattatore come nuovo adattatore per ListView.

3) Creare un nuovo URI basato sull'elemento su cui è stato fatto clic e riavviare il caricatore del cursore con il nuovo URI (e la proiezione). In questo esempio, l'URI è associato a query specifiche con gli argomenti di selezione trasmessi dall'interfaccia utente.

4) Quando i nuovi dati sono stati caricati dall'URI, scambiare il cursore associato all'adattatore con il nuovo cursore e l'elenco verrà quindi aggiornato.

Non vi è alcun backstack associato a questo dato che non stiamo usando le transazioni, quindi dovrai creare il tuo o giocare le domande al contrario quando esci dalla gerarchia. Quando ho provato questo, le query sono state abbastanza veloci che le ho eseguite nuovamente in oNBackPressed () fino a quando non sono in cima alla gerarchia, a quel punto il framework riprende il pulsante Indietro.

Se ti trovi in ​​una situazione simile, assicurati di leggere i documenti: http://developer.android.com/guide/topics/ui/layout/listview.html

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

Spero che questo aiuti qualcuno!


Nel caso qualcuno lo stia facendo, e anche usando SectionIndexer (come AlphabetIndexer), potresti notare che dopo aver sostituito l'adattatore, lo scorrimento veloce non funziona. Un tipo di bug sfortunato, ma la sostituzione dell'adattatore, anche con un nuovo indicizzatore, non aggiorna l'elenco delle sezioni utilizzate da FastScroll. C'è una soluzione alternativa, vedere: descrizione del problema e soluzione alternativa
courtf

2

Ho avuto esattamente lo stesso problema e ho implementato un progetto github open source che copre schede sovrapposte, navigazione back-up e ben testato e documentato:

https://github.com/SebastianBaltesObjectCode/PersistentFragmentTabs

Questo è un framework semplice e piccolo per le schede di navigazione e la commutazione e gestione dei frammenti della navigazione su e indietro. Ogni scheda ha la sua pila di frammenti. Utilizza ActionBarSherlock ed è compatibile con il livello API 8.


2

Questo è un problema complesso poiché Android gestisce solo 1 back stack, ma questo è fattibile. Mi ci sono voluti giorni per creare una libreria chiamata Tab Stacker che fa esattamente quello che stai cercando: una cronologia dei frammenti per ogni scheda. È open source e completamente documentato e può essere incluso facilmente con Gradle. Puoi trovare la libreria su github: https://github.com/smart-fun/TabStacker

Puoi anche scaricare l'app di esempio per verificare che il comportamento corrisponda alle tue esigenze:

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

Se hai qualche domanda, non esitare a lasciare una mail.


2

Vorrei suggerire la mia soluzione nel caso qualcuno stia cercando e voglia provare a scegliere la migliore per le sue esigenze.

https://github.com/drusak/tabactivity

Lo scopo di creare la libreria è abbastanza banale: implementalo come iPhone.

I principali vantaggi:

  • usa la libreria android.support.design con TabLayout;
  • ogni scheda ha il suo stack usando FragmentManager (senza salvare i riferimenti dei frammenti);
  • supporto per deep linking (quando è necessario aprire una scheda specifica e il livello di frammento specifico in essa);
  • salvare / ripristinare gli stati delle schede;
  • metodi adattivi del ciclo di vita dei frammenti nelle schede;
  • abbastanza facile da implementare per le tue esigenze.

Grazie, questo è stato molto utile. Ho bisogno di usare ListFragments in aggiunta a Fragments, quindi ho duplicato BaseTabFragment.java in BaseTabListFragment.java e l'ho fatto estendere ListFragment. Quindi ho dovuto cambiare varie parti del codice in cui si presupponeva sempre un BaseTabFragment. C'è un modo migliore?
primehalo

Sfortunatamente, non ho pensato a ListFragment. Tecnicamente è la soluzione giusta, ma richiederà controlli addizionali per TabFragment e la sua istanza di BaseTabListFragment. Un altro approccio per usare Fragment con ListView all'interno (esattamente lo stesso di ListFragment implementato). Ci penserò su. Grazie per avermi indicato!
Kasurd,

1

Una soluzione semplice:

Ogni volta che cambi la chiamata alla vista tab / root:

fragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);

Cancella il BackStack. Ricorda di chiamarlo prima di cambiare il frammento di radice.

E aggiungi frammenti con questo:

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

Nota il .addToBackStack(null)e il transaction.addpotrebbe ad esempio essere cambiato con transaction.replace.


-1

Questa discussione è stata molto interessante e utile.
Grazie Krishnabhadra per la tua spiegazione e il tuo codice, uso il tuo codice e sono migliorato un po ', permettendomi di persistere in stack, currentTab, ecc ... dalla modifica della configurazione (ruotando principalmente).
Testato su dispositivi 4.0.4 e 2.3.6 reali, non testato sull'emulatore

Cambio questa parte del codice su "AppMainTabActivity.java", il resto rimane lo stesso. Forse Krishnabhadra lo aggiungerà al suo codice.

Ripristina dati su Crea:

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

Salvare le variabili e metterle in 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);
}

Se esiste un CurrentTab precedente, impostalo, altrimenti crea un nuovo 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);
    }
}

Spero che questo aiuti altre persone.


Questo è sbagliato. Quando onCreate viene chiamato con un pacchetto, quei frammenti non saranno gli stessi che verranno mostrati sullo schermo e perderai quelli vecchi, a meno che tu non stia usando setRetainInstance. E se ActivityManager "salva" la tua attività, poiché un frammento non è serializzabile né parcelabile, quando l'utente ritorna alla tua attività, si arresterà in modo anomalo.
sergio91pt,

-1

Consiglierei di non usare lo backstack basato su HashMap> ci sono molti bug nella modalità "non mantenere le attività". Non ripristinerà correttamente lo stato nel caso in cui tu sia profondamente nello stack del frammento. Inoltre, verrà inserito nel frammento di mappa nidificato (con l'eccezione: frammento non trovato per l'ID). Coz HashMap> dopo l'applicazione background \ foreground sarà nulla

Ottimizzo il codice sopra per il lavoro con lo zaino del frammento

È TabView in basso

Classe di attività 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>

inserisci qui la descrizione dell'immagine

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.