Pilha traseira separada para cada guia no Android usando fragmentos

158

Estou tentando implementar guias para navegação em um aplicativo Android. Como TabActivity e ActivityGroup estão obsoletos, gostaria de implementá-lo usando Fragments.

Eu sei como configurar um fragmento para cada guia e depois alternar fragmentos quando uma guia é clicada. Mas como posso ter uma pilha traseira separada para cada guia?

Por exemplo, o fragmento A e B estaria na guia 1 e o fragmento C e D na guia 2. Quando o aplicativo é iniciado, o fragmento A é mostrado e a guia 1 é selecionada. O fragmento A pode ser substituído pelo fragmento B. Quando a guia 2 é selecionada, o fragmento C deve ser exibido. Se a guia 1 for selecionada, o fragmento B deve ser exibido novamente. Nesse ponto, deve ser possível usar o botão Voltar para mostrar o fragmento A.

Além disso, é importante que o estado de cada guia seja mantido quando o dispositivo for girado.

BR Martin

mardah
fonte

Respostas:

23

No momento, a estrutura não fará isso automaticamente para você. Você precisará criar e gerenciar suas próprias pilhas traseiras para cada guia.

Para ser sincero, isso parece algo realmente questionável. Não consigo imaginá-lo resultando em uma interface de usuário decente - se a tecla Voltar fará coisas diferentes dependendo da guia que eu sou, especialmente se a tecla Voltar também tiver seu comportamento normal de fechar toda a atividade quando estiver no topo da tela. a pilha ... parece desagradável.

Se você estiver tentando criar algo como uma interface do usuário do navegador da Web, obter uma UX natural para o usuário envolverá muitos ajustes sutis de comportamento, dependendo do contexto, então você definitivamente precisará fazer sua própria pilha traseira gerenciamento em vez de confiar em alguma implementação padrão na estrutura. Por exemplo, tente prestar atenção em como a tecla Voltar interage com o navegador padrão das várias maneiras pelas quais você pode entrar e sair dela. (Cada "janela" do navegador é essencialmente uma guia.)

hackbod
fonte
7
Não faça isso. E o quadro é quase inútil. Ele não fornece suporte automático para esse tipo de coisa, que, como eu disse, não consigo imaginar resultando em uma experiência decente para o usuário, exceto em situações muito especializadas em que você esteja, precisará controlar cuidadosamente o comportamento das costas.
hackbod
9
Nesse tipo de navegação, você tem guias e hierarquia de páginas em cada guia é muito comum para aplicativos do iPhone, por exemplo (você pode verificar os aplicativos da App Store e do iPod). Acho a experiência do usuário bastante decente.
Dmitry Ryadnenko
13
Isso é uma loucura. O iPhone nem tem um botão Voltar. Existem demos de API mostrando código muito simples para implementar fragmentos em guias. A pergunta que foi feita era sobre ter pilhas inversas diferentes para cada guia, e minha resposta é que a estrutura não fornece isso automaticamente porque semanticamente, para o que o botão voltar faz, provavelmente seria uma experiência de usuário ruim. Você pode implementar facilmente a semântica de volta, se quiser.
hackbod 8/09/11
4
Mais uma vez, o iPhone não possui um botão Voltar, portanto, ele não possui um comportamento de pilha traseira como o Android. Também "melhor ficar com as atividades e me salvar um monte de tempo" não faz qualquer sentido aqui, porque as atividades não deixe que você colocar manter abas em uma interface de usuário com suas próprias pilhas de volta diferentes; de fato, o gerenciamento de atividades da pilha traseira é menos flexível do que o fornecido pela estrutura Fragment.
hackbod
22
@hackbod Estou tentando seguir seus pontos, mas estou tendo problemas para implementar o comportamento personalizado de back-stack. Sei que, estando envolvido no projeto disso, você teria uma visão sólida de como isso pode ser fácil. É possível que exista um aplicativo de demonstração para o caso de uso do OP, pois é realmente uma situação muito comum, especialmente para aqueles que precisam escrever e portar aplicativos iOS para clientes que fazem essas solicitações ... o fragmento é revezado em cada FragmentActivity.
Richard Le Mesurier
138

Estou muito atrasado para esta pergunta. Mas como esse tópico foi muito informativo e útil para mim, é melhor postar meus dois centavos aqui.

Eu precisava de um fluxo de tela como este (um design minimalista com 2 guias e 2 visualizações em cada guia),

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

Eu tinha os mesmos requisitos no passado e o fiz usando TabActivityGroup(que também estava obsoleto na época) e Atividades. Desta vez, eu queria usar fragmentos.

Então foi assim que eu fiz.

1. Crie uma classe de fragmento 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){
    }
}

Todos os fragmentos no seu aplicativo podem estender essa classe Base. Se você deseja usar fragmentos especiais, como ListFragmentvocê deve criar uma classe base para isso também. Você será claro sobre o uso onBackPressed()e, onActivityResult()se ler a publicação na íntegra.

2. Crie alguns identificadores de guia, acessíveis em qualquer lugar do projeto

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

nada a explicar aqui ..

3. Ok, Atividade da guia principal - consulte os comentários no código.

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 (no caso de alguém interessado).

<?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 (Primeiro fragmento na guia A, semelhante a todas as guias)

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

Esta pode não ser a maneira mais polida e correta. Mas funcionou lindamente no meu caso. Além disso, eu só tinha esse requisito no modo retrato. Eu nunca tive que usar esse código em um projeto que suporta ambas as orientações. Então não posso dizer que tipo de desafios eu enfrento lá ..

EDIT:

Se alguém quiser um projeto completo, enviei um projeto de amostra para o github .

Krishnabhadra
fonte
2
Armazenar dados para cada fragmento, recriar cada um deles, reconstruir as pilhas ... muito trabalho para uma simples mudança de orientação.
precisa
3
@omegatai concordo totalmente com você .. Todo o problema surge desde que o Android não gerencia a pilha para nós (o que o iOS faz e a mudança de orientação ou a guia com vários fragmentos é fácil ) e nos leva de volta à discussão original neste Q / Um fio. Não é bom voltar para isso agora ..
Krishnabhadra
1
@ Renjith Isso ocorre porque o fragmento é recriado toda vez, quando você alterna a guia. Não pense nem por uma vez que seu fragmento é reutilizado na guia. quando alterno da guia A para B, a guia A é liberada da memória. Portanto, salve seus dados em atividade e verifique sempre se a atividade possui dados antes de tentar obtê-los do servidor.
Krishnabhadra
2
@Krishnabhadra Ok, isso soa muito melhor. Deixe-me corrigir caso eu esteja errado. Conforme seu exemplo, há apenas uma atividade e, portanto, um pacote. Crie instâncias do adaptador no BaseFragment (referindo-se ao seu projeto) e salve os dados lá. Use-os sempre que a visualização for criada.
Renjith
1
Tenho que trabalhar. Muito obrigado. Carregar o projeto inteiro foi uma boa ideia! :-)
Vinay W
96

Tivemos que implementar exatamente o mesmo comportamento que você descreve para um aplicativo recentemente. As telas e o fluxo geral do aplicativo já estavam definidos, então tivemos que ficar com ele (é um clone do aplicativo iOS ...). Felizmente, conseguimos nos livrar dos botões de retorno na tela :)

Nós invadimos a solução usando uma mistura de TabActivity, FragmentActivities (estávamos usando a biblioteca de suporte para fragmentos) e Fragmentos. Em retrospectiva, tenho certeza de que não foi a melhor decisão de arquitetura, mas conseguimos fazer a coisa funcionar. Se eu tivesse que fazer isso de novo, provavelmente tentaria fazer uma solução mais baseada em atividades (sem fragmentos), ou tentaria ter apenas uma atividade para as guias e permitir que todo o resto fosse visualizações (que considero muito mais reutilizáveis ​​do que as atividades em geral).

Portanto, os requisitos eram ter algumas guias e telas aninhadas em cada guia:

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

etc ...

Digamos: o usuário começa na guia 1, navega da tela 1 para a tela 2 e depois para a tela 3, depois alterna para a guia 3 e navega da tela 4 para 6; se retornou à guia 1, ele deve ver a tela 3 novamente e, se pressionou Voltar, deve retornar à tela 2; De volta e ele está na tela 1; mude para a guia 3 e ele estará na tela 6 novamente.

A principal atividade no aplicativo é MainTabActivity, que estende o TabActivity. Cada guia está associada a uma atividade, digamos ActivityInTab1, 2 e 3. E então cada tela será um fragmento:

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

Cada ActivityInTab contém apenas um fragmento de cada vez e sabe como substituir um fragmento por outro (praticamente o mesmo que um ActvityGroup). O legal é que é muito fácil manter pilhas de costas separadas para cada guia dessa maneira.

A funcionalidade de cada ActivityInTab era a mesma: saber como navegar de um fragmento para outro e manter uma pilha inversa; portanto, colocamos isso em uma classe base. Vamos chamá-lo simplesmente de 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();
    }

}

O activity_in_tab.xml é exatamente isso:

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

Como você pode ver, o layout da visualização para cada guia era o mesmo. Isso ocorre porque é apenas um FrameLayout chamado conteúdo que contém cada fragmento. Os fragmentos são os que têm a visualização de cada tela.

Apenas para os pontos de bônus, também adicionamos um pouco de código para mostrar uma caixa de diálogo de confirmação quando o usuário pressiona Voltar e não há mais fragmentos para retornar:

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

Essa é basicamente a configuração. Como você pode ver, cada FragmentActivity (ou simplesmente Activity in Android> 3) está cuidando de todo o empilhamento com seu próprio FragmentManager.

Uma atividade como ActivityInTab1 será realmente simples, apenas mostrará seu primeiro fragmento (por exemplo, tela):

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

Então, se um fragmento precisar navegar para outro fragmento, ele precisará fazer uma seleção um pouco desagradável ... mas não é tão ruim assim:

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

Então é isso mesmo. Tenho certeza de que esta não é uma solução muito canônica (e principalmente não muito boa), então gostaria de perguntar aos desenvolvedores experientes do Android qual seria a melhor abordagem para obter essa funcionalidade e, se não for "como é" done "no Android, eu apreciaria se você pudesse me indicar algum link ou material que explique qual é a maneira do Android de abordar isso (guias, telas aninhadas em guias etc.). Sinta-se livre para separar esta resposta nos comentários :)

Como sinal de que esta solução não é muito boa, recentemente tive que adicionar algumas funcionalidades de navegação ao aplicativo. Algum botão bizarro que deve levar o usuário de uma guia para outra e para uma tela aninhada. Fazer isso de forma programática foi uma chatice, por causa de problemas de quem sabe quem e de quando lidar com fragmentos e atividades realmente instanciados e inicializados. Acho que teria sido muito mais fácil se essas telas e guias fossem apenas apenas visualizações.


Por fim, se você precisar sobreviver às alterações de orientação, é importante que seus fragmentos sejam criados usando setArguments / getArguments. Se você definir variáveis ​​de instância nos construtores de seus fragmentos, ficará ferrado. Mas, felizmente, é realmente fácil de corrigir: salve tudo em setArguments no construtor e, em seguida, recupere essas coisas com getArguments no onCreate para usá-las.

epidemia
fonte
13
Ótima resposta, mas acho que muito poucos verão isso. Eu escolhi exatamente o mesmo caminho (como você pode ver na conversa na resposta anterior) e não estou feliz com ele como você. Acho que o Google realmente estragou esses fragmentos, já que essa API não cobre os principais casos de uso. Outro problema que você pode encontrar é a impossibilidade de incorporar fragmento em outro fragmento.
Dmitry Ryadnenko
Obrigado pelo comentário boulder. Sim, eu não poderia concordar mais sobre a API de fragmentos. Eu já me deparei com o problema de fragmentos aninhados (é por isso que optamos pela abordagem "substituir um fragmento por outro" hehe).
epidemian
1
Eu implementei isso através de TODAS as atividades. Não gostei do que recebi e vou experimentar o Fragments. Esse é o oposto da sua experiência! Há muita implementação com o Activities para lidar com o ciclo de vida das visualizações filho em cada guia e também para implementar seu próprio botão Voltar. Além disso, você não pode simplesmente manter uma referência a todas as exibições ou irá explodir a memória. Espero que os fragmentos: 1) ofereçam suporte ao ciclo de vida dos fragmentos com clara separação de memória e 2) ajudem a implementar a funcionalidade do botão voltar Além disso, se você usar fragmentos para esse processo, não será mais fácil rodar em tablets?
gregm
O que acontece quando o usuário alterna entre guias? O backstack do fragmento é excluído? Como garantir que o backstack permaneça?
gregm
1
@gregm Se você fizer uma atividade de 1 guia <-> 1 como eu, o backstack de cada guia permanecerá quando as guias forem trocadas, porque as atividades são realmente mantidas ativas; eles são pausados ​​e retomados. Não sei se existe uma maneira de fazer com que as atividades sejam destruídas e recriadas quando as guias são alternadas em um TabActivity. No entanto, se você fizer com que os fragmentos dentro das atividades sejam substituídos como sugeri, eles serão destruídos (e recriados quando o backstack for acionado). Portanto, você terá no máximo um fragmento ativo por guia a qualquer momento.
epidemian
6

Armazenar referências fortes a fragmentos não é a maneira correta.

O FragmentManager fornece putFragment(Bundle, String, Fragment)esaveFragmentInstanceState(Fragment) .

Qualquer um deles é suficiente para implementar um backstack.


Usando putFragment, em vez de substituir um Fragmento, você desanexa o antigo e adiciona o novo. É isso que a estrutura faz com uma transação de substituição que é adicionada ao backstack.putFragmentarmazena um índice na lista atual de fragmentos ativos e esses fragmentos são salvos pela estrutura durante as alterações de orientação.

A segunda maneira, usando saveFragmentInstanceState, salva todo o estado do fragmento em um Bundle, permitindo que você realmente o remova, em vez de desanexá-lo. O uso dessa abordagem facilita a manipulação da pilha de trás, pois você pode abrir um fragmento quando quiser.


Eu usei o segundo método para este caso de uso:

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

Não quero que o usuário retorne à tela de inscrição, a partir da terceira, pressionando o botão Voltar. Também faço animações invertidas entre eles (usando onCreateAnimation), para que soluções hacky não funcionem, pelo menos sem que o usuário perceba claramente que algo não está certo.

Este é um caso de uso válido para um backstack personalizado, fazendo o que o usuário espera ...

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
fonte
2

Aviso Legal:


Eu sinto que este é o melhor lugar para postar uma solução relacionada em que trabalhei para um tipo de problema semelhante que parece ser um material padrão do Android. Não vai resolver o problema para todos, mas pode ajudar alguns.


Se a principal diferença entre seus fragmentos for apenas os dados que os estão fazendo backup (ou seja, não há muitas diferenças de layout), talvez você não precise substituir o fragmento, mas apenas trocar os dados subjacentes e atualizar a exibição.

Aqui está uma descrição de um exemplo possível para essa abordagem:

Eu tenho um aplicativo que usa ListViews. Cada item da lista é um pai com algum número de filhos. Quando você toca no item, uma nova lista precisa ser aberta com esses filhos, na mesma guia ActionBar da lista original. Essas listas aninhadas têm um layout muito semelhante (talvez alguns ajustes condicionais aqui e ali, talvez), mas os dados são diferentes.

Este aplicativo tem várias camadas de filhos abaixo da lista de pais inicial e podemos ou não ter dados do servidor quando um usuário tenta acessar uma certa profundidade além da primeira. Como a lista é construída a partir de um cursor do banco de dados e os fragmentos usam um carregador e um adaptador para preencher a exibição da lista com itens da lista, tudo o que precisa acontecer quando um clique é registrado é:

1) Crie um novo adaptador com os campos 'para' e 'de' apropriados que corresponderão às novas visualizações de itens adicionadas à lista e às colunas retornadas pelo novo cursor.

2) Defina este adaptador como o novo adaptador para o ListView.

3) Crie um novo URI com base no item que foi clicado e reinicie o carregador de cursor com o novo URI (e projeção). Neste exemplo, o URI é mapeado para consultas específicas com os argumentos de seleção transmitidos da interface do usuário.

4) Quando os novos dados forem carregados a partir do URI, troque o cursor associado ao adaptador pelo novo cursor e a lista será atualizada.

Não há backstack associado a isso, uma vez que não estamos usando transações, portanto, você precisará criar suas próprias ou reproduzir as consultas ao contrário ao sair da hierarquia. Quando tentei isso, as consultas foram rápidas o suficiente para que eu as execute novamente em oNBackPressed () até chegar ao topo da hierarquia, quando a estrutura assume o botão Voltar novamente.

Se você se encontrar em uma situação semelhante, leia os documentos: http://developer.android.com/guide/topics/ui/layout/listview.html

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

Espero que isso ajude alguém!

courtf
fonte
Caso alguém esteja fazendo isso e também esteja usando um SectionIndexer (como AlphabetIndexer), você pode perceber que, após a substituição do adaptador, sua rolagem rápida não funciona. Um tipo de bug infeliz, mas a substituição do adaptador, mesmo por um novo indexador, não atualiza a lista de seções usadas pelo FastScroll. Há uma solução alternativa, consulte: descrição do problema e solução
courtf
2

Eu tive exatamente o mesmo problema e implementei um projeto de código-fonte aberto do github que abrange guias empilhadas, navegação de backup e backup e é bem testado e documentado:

https://github.com/SebastianBaltesObjectCode/PersistentFragmentTabs

Essa é uma estrutura simples e pequena para guias de navegação e troca de fragmentos e manipulação da navegação para cima e para trás. Cada guia possui sua própria pilha de fragmentos. Ele usa o ActionBarSherlock e é compatível com o nível 8 da API.

Sebastian Baltes
fonte
2

Esse é um problema complexo, pois o Android lida apenas com 1 pilha traseira, mas isso é viável. Levei dias para criar uma biblioteca chamada Tab Stacker que faz exatamente o que você está procurando: um histórico de fragmentos para cada guia. É de código aberto e totalmente documentado e pode ser incluído facilmente no gradle. Você pode encontrar a biblioteca no github: https://github.com/smart-fun/TabStacker

Você também pode baixar o aplicativo de exemplo para ver se o comportamento corresponde às suas necessidades:

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

Se você tiver alguma dúvida, não hesite em enviar um e-mail.

Arnaud SmartFun
fonte
2

Eu gostaria de sugerir minha própria solução, caso alguém esteja procurando e queira tentar escolher a melhor para suas necessidades.

https://github.com/drusak/tabactivity

O objetivo de criar a biblioteca é bastante banal - implemente-o como o iPhone.

As principais vantagens:

  • use a biblioteca android.support.design com TabLayout;
  • cada guia possui sua própria pilha usando o FragmentManager (sem salvar as referências dos fragmentos);
  • suporte para links diretos (quando você precisar abrir uma guia específica e um nível de fragmento específico);
  • salvar / restaurar estados de guias;
  • métodos de ciclo de vida adaptativo de fragmentos em guias;
  • bastante fácil de implementar para suas necessidades.
Kasurd
fonte
Obrigado, isso tem sido bastante útil. Eu preciso usar ListFragments além de Fragments, então dupliquei BaseTabFragment.java para BaseTabListFragment.java e estendi ListFragment. Então eu tive que mudar várias partes do código onde sempre supunha esperar um BaseTabFragment. Existe uma maneira melhor?
primehalo
Infelizmente, não pensei em ListFragment. Tecnicamente, é a solução certa, mas exigirá verificações adicionais para TabFragment e sua instanceOf BaseTabListFragment. Outra abordagem para usar o Fragment with ListView dentro (exatamente o mesmo que o ListFragment implementado). Eu vou pensar sobre isso. Obrigado por me indicar isso!
kasurd
1

Uma solução simples:

Sempre que você altera a chamada de guia / visualização raiz:

fragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);

Isso limpará o BackStack. Lembre-se de chamar isso antes de alterar o fragmento raiz.

E adicione fragmentos com isso:

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

Observe que o .addToBackStack(null)e transaction.addpoderia, por exemplo, ser alterado com transaction.replace.

Morten Holmgaard
fonte
-1

Este tópico foi muito, muito interessante e útil.
Obrigado Krishnabhadra pela sua explicação e código, eu uso o seu código e melhorei um pouco, permitindo persistir as pilhas, o currentTab, etc ... da configuração da alteração (principalmente na rotação).
Testado em dispositivos 4.0.4 e 2.3.6 reais, não testado no emulador

Eu mudo essa parte do código em "AppMainTabActivity.java", o restante permanece o mesmo. Talvez Krishnabhadra adicione isso em seu código.

Recuperar dados emCriar:

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

Salve as variáveis ​​e coloque em 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 existir um CurrentTab anterior, configure-o, caso contrário, crie um novo 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);
    }
}

Espero que isso ajude outras pessoas.

Sulfkain
fonte
Isto está errado. Quando onCreate é chamado com um Bundle, esses fragmentos não serão os mesmos que serão mostrados na tela e você estará vazando os antigos, a menos que esteja usando setRetainInstance. E se o ActivityManager "salvar" sua Atividade, uma vez que um Fragmento não é Serializável nem Parcelável, quando o usuário retorna à sua Atividade, ele trava.
Sergio91pt
-1

Eu recomendaria não usar o backstack com base no HashMap> existem muitos erros no modo "não manter atividades". Não restaurará corretamente o estado caso você esteja profundamente na pilha de fragmentos. E também será agrupado no fragmento de mapa aninhado (com exceção: Fragmento nenhuma visualização encontrada para o ID). Coz HashMap> após o aplicativo background \ foreground será nulo

Otimizo o código acima para trabalhar com o backstack do fragmento

É TabView inferior

Atividade principal Classe

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>

insira a descrição da imagem aqui

Flinbor
fonte