A implementação de navegação aninhada em aplicações Android frequentemente exige a combinação de múltiplos componentes de UI. Um cenário comum envolve uma barra de navegação inferior que alterna Fragments principais, onde um desses Fragments contém um ViewPager com navegação superior, e finalmente, uma das páginas do ViewPager exibe uma ListView. Este artigo detalha a arquitetura necessária para integrar esses componentes, resolvendo conflitos de toque e gerenciando o ciclo de vida dos Fragments.
Estrutura da Atividade Principal
A atividade principal (MainActivity) gerencia a navegação de nível superior. Utilizamos um FrameLayout como container para trocar os Fragments inferiores (Home, Explorar, Estatísticas, Configurações) e uma barra layoutada manualmente para os botões de navegação.
package com.exemplo.app.navegacao;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import android.os.Bundle;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import java.util.ArrayList;
import java.util.List;
public class MainActivity extends AppCompatActivity {
private List<Fragment> fragmentStack;
private FrameLayout containerLayout;
private LinearLayout[] navButtons;
private ImageButton[] navIcons;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.layout_main_activity);
configurarComponentes();
inicializarFragments();
definirOuvintes();
carregarFragmentoInicial(0);
}
private void configurarComponentes() {
containerLayout = findViewById(R.id.container_fragmento);
navButtons = new LinearLayout[4];
navIcons = new ImageButton[4];
int[] idsBotoes = {R.id.btn_home, R.id.btn_explorar, R.id.btn_stats, R.id.btn_config};
int[] idsLayouts = {R.id lay_home, R.id.lay_explorar, R.id.lay_stats, R.id.lay_config};
for (int i = 0; i < 4; i++) {
navButtons[i] = findViewById(idsBotoes[i]);
navButtons[i] = findViewById(idsBotoes[i]);
navButtons[i].setOnClickListener(navegacaoListener);
navButtons[i] = findViewById(idsBotoes[i]);
}
}
private void inicializarFragments() {
fragmentStack = new ArrayList<>();
fragmentStack.add(new HomeTabFragment());
fragmentStack.add(new ExplorarFragment());
fragmentStack.add(new EstatisticasFragment());
fragmentStack.add(new ConfigFragment());
}
private void definirOuvintes() {
// A lógica de clique é unificada no listener abaixo
}
View.OnClickListener navegacaoListener = v -> {
resetarIcones();
int index = -1;
if (v.getId() == R.id.lay_home) index = 0;
else if (v.getId() == R.id.lay_explorar) index = 1;
else if (v.getId() == R.id.lay_stats) index = 2;
else if (v.getId() == R.id.lay_config) index = 3;
if (index != -1) {
atualizarSelecao(index);
}
};
private void resetarIcones() {
// Restaurar ícones para o estado não selecionado
navIcons[0].setImageResource(R.drawable.ic_home_outline);
navIcons[1].setImageResource(R.drawable.ic_explore_outline);
navIcons[2].setImageResource(R.drawable.ic_stats_outline);
navIcons[3].setImageResource(R.drawable.ic_settings_outline);
}
private void atualizarSelecao(int indice) {
switch (indice) {
case 0: navIcons[0].setImageResource(R.drawable.ic_home_filled); break;
case 1: navIcons[1].setImageResource(R.drawable.ic_explore_filled); break;
case 2: navIcons[2].setImageResource(R.drawable.ic_stats_filled); break;
case 3: navIcons[3].setImageResource(R.drawable.ic_settings_filled); break;
}
trocarFragmento(indice);
}
private void trocarFragmento(int indice) {
FragmentManager fm = getSupportFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
ft.replace(R.id.container_fragmento, fragmentStack.get(indice));
ft.commit();
}
private void carregarFragmentoInicial(int indice) {
resetarIcones();
atualizarSelecao(indice);
}
}
Fragmento Home com ViewPager
O Fragmento "Home" é mais complexo, pois contém uma navegação secundária horizontal. Utilizamos um RadioGroup dentro de um HorizontalScrollView para as abas superiores e um ViewPager para o conteúdo. É crucial sincronizar o deslizamento do ViewPager com a seleção das abas.
package com.exemplo.app.navegacao;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.HorizontalScrollView;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.viewpager.widget.ViewPager;
public class HomeTabFragment extends Fragment implements ViewPager.OnPageChangeListener {
private View rootView;
private RadioGroup grupoAbas;
private ViewPager pagerConteudo;
private HorizontalScrollView scrollAbas;
private String[] categorias;
private CategoriaPagerAdapter adaptador;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
if (rootView == null) {
rootView = inflater.inflate(R.layout.layout_home_tab, container, false);
categorias = getResources().getStringArray(R.array.categorias_news);
pagerConteudo = rootView.findViewById(R.id.viewpager_news);
grupoAbas = rootView.findViewById(R.id.radio_grupo_abas);
scrollAbas = rootView.findViewById(R.id.scroll_abas);
inicializarViewPager();
criarAbasSuperiores(inflater);
grupoAbas.setOnCheckedChangeListener((group, checkedId) -> {
pagerConteudo.setCurrentItem(checkedId);
});
}
return rootView;
}
private void inicializarViewPager() {
FragmentManager fm = getActivity().getSupportFragmentManager();
adaptador = new CategoriaPagerAdapter(fm, categorias);
pagerConteudo.setAdapter(adaptador);
pagerConteudo.setOffscreenPageLimit(2);
pagerConteudo.addOnPageChangeListener(this);
}
private void criarAbasSuperiores(LayoutInflater inflater) {
for (int i = 0; i < categorias.length; i++) {
RadioButton rb = (RadioButton) inflater.inflate(R.layout.item_tab_radio, null);
rb.setId(i);
rb.setText(categorias[i]);
grupoAbas.addView(rb);
}
grupoAbas.check(0);
}
@Override
public void onPageSelected(int position) {
sincronizarAba(position);
}
private void sincronizarAba(int indice) {
RadioButton rb = (RadioButton) grupoAbas.getChildAt(indice);
if (rb != null) {
rb.setChecked(true);
// Lógica para scroll horizontal automático da aba selecionada
int scrollX = rb.getLeft() - (scrollAbas.getWidth() / 2) + (rb.getWidth() / 2);
scrollAbas.smoothScrollTo(scrollX, 0);
}
}
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {}
@Override
public void onPageScrollStateChanged(int state) {}
}
Implementação do Fragmento com Lista
Para uma das categorias do ViewPager (ex: "Headlines"), utilizamos uma ListView. Este Fragmento recebe dados via Bundle e configura um adaptador simples. Note o uso de um ID específico para a ListView para evitar conflitos com o sistema.
package com.exemplo.app.navegacao;
import android.content.DialogInterface;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
import java.util.ArrayList;
public class NewsListFragment extends Fragment {
private static final String ARG_DADOS = "arg_dados_noticias";
private String[] manchetes;
private ArrayList<String> itensLista = new ArrayList<>();
private String[] detalhesInfo = {"Detalhe A", "Detalhe B", "Detalhe C", "Detalhe D"};
public static NewsListFragment criarInstancia(String[] dados) {
NewsListFragment fragment = new NewsListFragment();
Bundle args = new Bundle();
args.putStringArray(ARG_DADOS, dados);
fragment.setArguments(args);
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getArguments() != null) {
manchetes = getArguments().getStringArray(ARG_DADOS);
}
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.layout_lista_noticias, container, false);
ListView lista = view.findViewById(R.id.lista_conteudo);
// Preencher dados simulados
for (int i = 0; i < 50; i++) {
itensLista.add(manchetes[i % manchetes.length]);
}
ArrayAdapter<String> adaptador = new ArrayAdapter<>(
getContext(),
android.R.layout.simple_list_item_1,
itensLista
);
lista.setAdapter(adaptador);
lista.setOnItemClickListener((parent, view1, position, id) -> {
new AlertDialog.Builder(getContext())
.setTitle("Detalhes")
.setMessage(detalhesInfo[position % detalhesInfo.length])
.setPositiveButton("Fechar", (dialog, which) -> dialog.dismiss())
.show();
});
return view;
}
}
Adaptador do ViewPager
O adaptador gerencia a criação dos Fragments para cada página do ViewPager. Ele decide dinamicamente se deve retornar o Fragment de lista ou um Fragment de texto simples baseado no índice.
package com.exemplo.app.navegacao;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentStatePagerAdapter;
public class CategoriaPagerAdapter extends FragmentStatePagerAdapter {
private String[] categorias;
private String[] dadosNoticias = {
"Notícia Urgente 1",
"Atualização Política",
"Mercado Financeiro",
"Tecnologia Hoje"
};
public CategoriaPagerAdapter(FragmentManager fm, String[] categorias) {
super(fm);
this.categorias = categorias;
}
@Override
public Fragment getItem(int position) {
// Exemplo: Apenas a segunda aba usa ListView, outras usam texto
if (position == 1) {
return NewsListFragment.criarInstancia(dadosNoticias);
}
return TextoSimplesFragment.novo(categorias[position]);
}
@Override
public int getCount() {
return categorias.length;
}
}
Resolução de Conflitos de Toque
Um problema comum nessa arquitetura é o conflito de gestos entre o ViewPager (deslize horizontal) e a ListView ou ScrollView (deslize vertical). Para garantir que o ViewPager não intercepte o scroll vertical da lista, podemos sobrescrever a interceptação de toque no Fragmento pai.
private float distanciaX, distanciaY, ultimoX, ultimoY;
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
distanciaX = distanciaY = 0f;
ultimoX = ev.getX();
ultimoY = ev.getY();
break;
case MotionEvent.ACTION_MOVE:
float atualX = ev.getX();
float atualY = ev.getY();
distanciaX += Math.abs(atualX - ultimoX);
distanciaY += Math.abs(atualY - ultimoY);
ultimoX = atualX;
ultimoY = atualY;
// Se o movimento horizontal for predominante, intercepta para o ViewPager
if (distanciaX > distanciaY && distanciaX > 10) {
return true;
}
break;
}
return super.onInterceptTouchEvent(ev);
}
Definições de Layout e Recursos
Os layouts XML devem seguir a hierarquia definida. O layout da lista requer um ID específico. O layout principal utiliza um peso (layout_weight) para garantir que a área de conteúdo ocupe o espaço restante acima da barra de navegação.
layout_lista_noticias.xml
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:id="@+id/lista_conteudo"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</FrameLayout>
layout_home_tab.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<HorizontalScrollView
android:id="@+id/scroll_abas"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollbars="none">
<RadioGroup
android:id="@+id/radio_grupo_abas"
android:orientation="horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</HorizontalScrollView>
<androidx.viewpager.widget.ViewPager
android:id="@+id/viewpager_news"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
</LinearLayout>
strings.xml
<resources>
<string-array name="categorias_news">
<item>Seguir</item>
<item>Manchetes</item>
<item>Vídeos</item>
<item>Local</item>
<item>Política</item>
<item>Entretenimento</item>
</string-array>
</resources>