BottomNavigationView Android, Como e Quando Utilizar

Investir em Você é Barra de Ouro a R$ 2,00. Cadastre-se e receba grátis conteúdos Android sem precedentes! Você receberá um email de confirmação. Somente depois de confirma-lo é que eu poderei lhe enviar os conteúdos semanais exclusivos. Os artigos em PDF são entregues somente para os inscritos na lista.

Email inválido.
Blog /Android /BottomNavigationView Android, Como e Quando Utilizar

BottomNavigationView Android, Como e Quando Utilizar

Vinícius Thiengo
(17980) (2)
Go-ahead
"O método consciente de tentativa e erro é mais bem-sucedido que o planejamento de um gênio isolado."
Peter Skillman
Prototipagem Android
Capa do curso Prototipagem Profissional de Aplicativos
TítuloAndroid: Prototipagem Profissional de Aplicativos
CategoriasAndroid, Design, Protótipo
AutorVinícius Thiengo
Vídeo aulas186
Tempo15 horas
ExercíciosSim
CertificadoSim
Acessar Curso
Quer aprender a programar para Android? Acesse abaixo o curso gratuito no Blog.
Lendo
TítuloManual de DevOps: como obter agilidade, confiabilidade e segurança em organizações tecnológicas
CategoriaEngenharia de Software
Autor(es)Gene Kim, Jez Humble, John Willis, Patrick Debois
EditoraAlta Books
Edição
Ano2018
Páginas464
Conteúdo Exclusivo
Investir em Você é Barra de Ouro a R$ 2,00. Cadastre-se e receba gratuitamente conteúdos Android sem precedentes!
Email inválido

Tudo bem?

Neste artigo vamos, passo a passo, ao estudo do widget BottomNavigationView, um dos componentes visuais mais atuais do Android, porém já muito utilizado.

Depois da apresentação da API vamos a um projeto que simula um aplicativo real, aplicativo sobre jogos de futebol, e que tem como necessidade a melhora do menu de navegação entre as listas de jogos.

Projeto Android com BottomNavigationView

Antes de prosseguir, não deixe de se inscrever na 📩 lista de emails do Blog, logo acima, para receber os conteúdos Android exclusivos e em primeira mão.

Abaixo os tópicos que estaremos abordando:

O componente visual BottomNavigationView

O BottomNavigationView foi lançado no Android, como API nativa, somente em março de 2016, junto a versão 25 da Material Design Support library.

O foco deste componente visual é em devices mobile, sendo recomendado, quando em sistemas desktop / Web, o uso do menu gaveta.

Por fazer parte do pacote de suporte do Android, o BottomNavigation atende às principais versões deste SO.

Com o BottomNavigation é possível prover ao usuário acesso rápido a telas de contextos similares, porém há uma série de regras de negócio sobre quando e como devemos utilizar este widget.

A partir daqui vamos aos estudo geral deste componente e também às regras de uso dele.

Instalação

Para poder utilizar o BottomNavigationView é necessário, no Gradle App Level, a referência a biblioteca de suporte do Material Design:

...
dependencies {
implementation 'com.android.support:design:26.1.0'
}
...

 

Certifique-se de sempre estar utilizando a versão mais atual da library de suporte. Na época da construção deste artigo era a versão 26.1.0.

Definição em código

Primeiro vamos aos trechos que devem entrar no XML: tags de layout e de menu.

Para o layout, diferente do DrawerLayout que trabalha como uma espécie de invólucro a outros componentes necessários, o BottomNavigationView, apesar de herdar do FrameLayout, é utilizado de maneira isolada, digo, sem componentes filhos diretos, com uso de tags de abertura e de fechamento.

Abaixo uma possível definição do BottomNavigationView:

...
<android.support.design.widget.BottomNavigationView
android:id="@+id/bottom_nav_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:menu="@menu/activity_main_bottom_actions" />
...

 

Então os itens de menu em /res/menu/activity_main_bottom_actions (arquivo referenciado no atributo app:menu):

<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:showIn="navigation_view">

<group android:checkableBehavior="single">
<item
android:id="@+id/nav_uefa"
android:checked="true"
android:icon="@drawable/ic_uefa"
android:title="UEFA" />
<item
android:id="@+id/nav_libertadores"
android:icon="@drawable/ic_libertadores"
android:title="Libertadores" />
<item
android:id="@+id/nav_afc"
android:icon="@drawable/ic_afc"
android:title="AFC" />
</group>
</menu>

 

Assim a definição de cores em um arquivo XML padrão de estilo de um novo projeto Android, mais precisamente o trecho de definição de cores no arquivo /res/values/styles.xml:

...
<style
name="AppTheme"
parent="Theme.AppCompat.Light.DarkActionBar">

<item name="colorPrimary">@color/colorPrimary</item> <!-- Azul escuro -->
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
...

 

Com os códigos XML definidos anteriormente teríamos:

  • O transparente como "cor de background" do BottomNavigationView - aqui acabou ficando o branco como cor de background, pois essa é a cor padrão de background de projeto quando nenhuma outra cor é definida. Se tivéssemos definido o item <item name="android:windowBackground"> em styles.xml a cor de fundo seria a do drawable definido como valor neste item. Ou se tivéssemos utilizado o atributo android:background em <android.support.design.widget.BottomNavigationView>, o valor deste atributo seria então a cor de background;
  • Os itens não selecionados, ícones e rótulos, com um ton de cinza. Alias essa é a cor padrão da API BottomNavigationView para item não selecionado;
  • E o item selecionado com a cor primária do projeto. Neste exemplo: o azul escuro de colorPrimary.

Itens do BottomNavigationView

Para a parte de programação do projeto, o principal é o acesso aos listeners de acionamento e re-acionamento de BottomActions, alias, os itens do BottomNavigationView também são chamados de BottomActions:

class MainActivity : AppCompatActivity(),
BottomNavigationView.OnNavigationItemSelectedListener,
BottomNavigationView.OnNavigationItemReselectedListener {

override fun onCreate(savedInstanceState: Bundle?) {
...
initBottomNavigation()
}

private fun initBottomNavigation() {
bottom_nav_view.setOnNavigationItemSelectedListener( this )
bottom_nav_view.setOnNavigationItemReselectedListener( this )
}

override fun onNavigationItemSelected(item: MenuItem): Boolean {
/* TODO */
return true
}

override fun onNavigationItemReselected(item: MenuItem) {
/* TODO */
}
}

 

No código Kotlin anterior é assumido que o plugin kotlin-android-extensions está definido no projeto, por isso não há necessidade de uso do findViewById().

Note que todo o código XML definido no início desta seção também é passível de ser construído via programação, com qualquer uma das linguagens oficiais para dev Android.

Customização dos itens

Caso queira definir outras cores para os BottomActions e seus componentes, pode utilizar os atributos a seguir em destaque:

...
<android.support.design.widget.BottomNavigationView
android:id="@+id/bottom_nav_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
app:itemBackground="@color/colorPrimary"
app:itemIconTint="@drawable/bottom_item_background_color"
app:itemTextColor="@drawable/bottom_item_background_color"
app:menu="@menu/activity_main_bottom" />
...

 

O conteúdo de cada um destes atributos pode ser uma cor direta, como em: @color/colorPrimary. Ou um drawable que permite mudanças de estilo de acordo com o status do item, como o @drawable/bottom_item_background_color:

<?xml version="1.0" encoding="utf-8"?>
<selector
xmlns:android="http://schemas.android.com/apk/res/android">

<item
android:color="@android:color/white"
android:state_checked="true" />

<item
android:color="#99ffffff" />
</selector>

 

Note que no caso da definição de um <selector> para o atributo app:itemBackground, ao invés de android:color deverá ser utilizado o atributo android:drawable.

Com os XMLs anteriores, teríamos:

Itens do BottomNavigationView com cores personalizadas

Caso não queira trabalhar o background dos itens de acordo com os estados deles, algo correto de se fazer quando seguindo as regras do BottomNavigation no Material Design, utilize o atributo android:background:

...
<android.support.design.widget.BottomNavigationView
android:id="@+id/bottom_nav_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:background="@color/colorPrimary"
app:itemIconTint="@drawable/bottom_item_background_color"
app:itemTextColor="@drawable/bottom_item_background_color"
app:menu="@menu/activity_main_bottom" />
...

 

O resultado é o mesmo do que o apresentado anteriormente, pois em ambos estamos utilizando uma cor direta: @color/colorPrimary.

Ressaltando que toda a configuração via XML é passível de ser realizada também via linguagem de programação. Aqui estamos optando por mostrar ou somente em XML ou somente em código de programação, pois assim seguimos boas práticas de desenvolvimento, mantendo no lado estático, XML, aquilo que se mantém estático por todo o ciclo de vida do aplicativo.

Animação

Utilizando a API nativa do BottomNavigationView não precisamos nos preocupar com a codificação de animação que nos permitirá seguir as regras do Material Design para este widget.

A versão nativa já faz, com maestria, a parte dela: Ripple Effect, mudança de cor e redimensionamento de itens e rótulos, incluindo a não apresentação de rótulos de itens não acionados em uma BottomNavigation pequena:

Animação entre itens do BottomNavigationView

A parte de animação que devemos implementar sem auxílio da API do BottomNavigation é a de transição de fragmentos, algo também definido nas especificações deste widget como opcional.

A seguir um código simples com a animação de fade, nativa do Android, sendo aplicada a mudança de fragmentos de itens, BottomActions:

...
supportFragmentManager
.beginTransaction()
.setCustomAnimations( android.R.anim.fade_in, android.R.anim.fade_out )
.replace(R.id.rl_container, fragment, FragmentAbstract.KEY)
.commit()
...

 

Como resultado, temos:

Animação entre fragmentos Android

Nada de animação em swipe, algo não aceito pelas regras de negócio do BottomNavigationView.

Especificações de design

A seguir algumas características de design do BottomNavigationView, muitas delas já presentes na versão nativa da API:

Especificações de design BottomNavigationView

  • Altura de 56dp e largura variando entre 80dp-168dp:
    • A exceção é quando há animação e itens suficiente, na barra de bottom, para exigir que o item atualmente selecionado tenha um espaço bem maior do que os outros:
      • Item ativo, de 96dp a 168dp;
      • Item inativo de 56dp a 96dp.
    • Quando há um número de itens onde até mesmo o rótulo de todos pode ser apresentado, neste caso todas as abas devem ter a mesma largura.
  • Ícones devem ter medidas como as dos ícones do NavigationView, 24dp x 24dp;
  • Os rótulos dos BottomActions devem ser pequenos e significativos, isso caso haja a necessidade de também coloca-los junto aos ícones, e não devem ser truncados, ocupando apenas uma linha;
  • O design dos itens deve ser em duas categorias: item em foco; e itens fora de foco. Ou seja, não utilize cores distintas para itens diferentes em foco. Se a cor primária do app for a utilizada para itens em foco, sempre utilize ela independente do item e da cor principal da tela de conteúdo acionada por ele.

Regra de estilos de itens no BottomNavigationView

Mais sobre as especificações de design, no conteúdo a seguir: Components - Bottom Navigation - Specs.

Especificações de comportamento

A seguir algumas regras de negócio quanto ao comportamento do widget em estudo:

  • O acionamento de qualquer BottomAction não deve abrir outros menus ou dialogs;
  • Caso seja acionado um item já ativo, o comportamento deve ser de refresh de conteúdo, o que faríamos caso um SwipeRefreshLayout estivesse em uso;
  • A navegação entre os itens do BottomNavigationView deve cancelar a tarefa atual em curso do item anteriormente selecionado;
  • O apresentar / esconder do BottomNavigation, de acordo com o scroll da tela, é algo opcional;

Animação do BottomNavigationView com comportamento de scroll

  • O gesto de swipe na área de conteúdo não deve permitir a mudança do próprio conteúdo e nem mesmo a mudança de item no BottomNavigation:
    • O efeito de transição de conteúdo, se presente, deve ser com a animação de fade (in / out).
  • A navegação hierárquica dos conteúdos não deve tirar da tela o BottomNavigationView, como ocorre com o menu gaveta.

Mais sobre as especificações de comportamento, no conteúdo a seguir: Components - Bottom Navigation - Behavior.

Especificações de uso

A seguir algumas regras de negócio quanto ao uso do BottomNavigationView:

  • Deve ser tratado como opção de uso somente para contextos onde há de 3 a 5 telas de alto nível no aplicativo:
    • Para menos três telas, utilize Tabs;
    • Para mais de 5 telas, utilize o menu gaveta, isso, pois mais do 5 itens atrapalha na experiência do usuário, os itens ficam muito próximos uns aos outros.
  • Os conteúdos dos itens do BottomNavigation devem ter similar importância no aplicativo;
  • Evite o uso, em mesma tela, de Tabs e BottomNavigationView, isso confunde o usuário;
  • O BackButton do device não deve acionar navegação de volta entre os itens do BottomNavigationView, ou seja, não há o trabalho de empilhamento entre os fragmentos acionados a partir dos BottomActions.

Mais sobre as especificações de uso, no conteúdo a seguir: Components - Bottom Navigation - Usage.

Curiosidades

Pelo mais incrível que pareça, na documentação, tanto a oficial do Android como a oficial do Material Design, não há restrições de uso do BottomNavigationView junto a:

Todos são componentes, que pelo histórico de conhecimento, um desenvolvedor Android poderia imaginar não serem úteis quando o BottomNavigation estivesse sendo utilizado, principalmente o menu gaveta.

Uma observação importante é que não há necessidade e nem mesmo é inteligente querer utilizar um ViewPager, que facilita a transição e cache de fragmentos, junto ao BottomNavigation. Isso principalmente pelo ViewPager permitir, por padrão, a animação de swipe entre fragmentos.

Limitações

Apesar da API nativa do BottomNavigationView ser bem simples de utilizar, ela fica devendo em:

  • Não fornecer uma maneira trivial de vincular fragmentos e animações de transição aos itens, BottomActions. Algo que é possível quando utilizando TabLayout e ViewPager;
  • Não fornecer uma maneira trivial de ativar o scroll da barra de bottom de acordo com o scroll do conteúdo em tela.

Outras APIs

Apesar de eu frequentemente recomendar as versões nativas de qualquer API necessária em seu projeto Android, no caso do BottomNavigationView pode ser que o uso de alguma API de terceiro traga maior facilidade no desenvolvimento, incluindo o uso de funcionalidades ainda não oferecidas pela versão nativa.

A seguir alguns conteúdos com outras APIs de BottomNavigation (em inglês) que podem ser úteis em sua jornada como desenvolvedor Android:

Assim podemos partir para um exemplo de projeto que passará a utilizar o BottomNavigationView como uma melhor escolha de menu.

Projeto Android

Como projeto de exemplo, para simular a aplicação do BottomNavigationView em um contexto real, construiremos um aplicativo que apresentará os confrontos finais das três principais ligas continentais de futebol: UEFA Champions LeagueTaça Libertadores e Liga dos Campeões da AFC.

Primeiro vamos a apresentação do protótipo estático desta parte inicial, logo depois seguiremos com a codificação do projeto. Somente na segunda parte é que aplicaremos o BottomNavigationView, explicando também o porquê da mudança de design.

Protótipo estático

A seguir as imagens do protótipo estático que devemos construir antes de iniciar até mesmo um novo projeto no Android Studio:

Tela de entrada do projeto Android

Tela de entrada

Lista de jogos do projeto Android

Lista de jogos

Menu gaveta do projeto Android

Menu gaveta

Área de detalhes de clube do projeto Android

Área de detalhes de clube

Note que nessa primeira parte estaremos utilizando o menu gaveta ao invés do BottomNavigation. É visível o espaço extra considerável que temos neste componente devido ao uso de somente três itens e a não necessidade de um cabeçalho.

Iniciando o projeto

Com o Android Studio aberto, crie um novo projeto Android Kotlin (pode seguir com Java se preferir está linguagem), coloque como nome "Torneios Continentais". Selecione como API mínima a 16 e como design inicial um projeto utilizando o menu gaveta, Navigation Drawer Activity.

Na raiz do projeto, app/Java/seu.pacote.torneioscontinentais, coloque os seguintes novos pacotes: adaptador; atividadedados; dominio; fragment.

Estrutura projeto Android no Android Studio

Configurações Gradle

A seguir as configurações iniciais de ambos os arquivos gradle do projeto. Iniciando com o Gradle Project Level, ou build.gradle (Project: TorneiosContinentais), que por sinal não passou por nenhuma modificação:

buildscript {
ext.kotlin_version = '1.1.51'
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.0.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}

allprojects {
repositories {
google()
jcenter()
}
}

task clean(type: Delete) {
delete rootProject.buildDir
}

 

Então o Gradle App Level, ou build.gradle (Module: app):

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'

android {
compileSdkVersion 26
defaultConfig {
applicationId "thiengo.com.br.torneioscontinentais"
minSdkVersion 16
targetSdkVersion 26
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}

dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
implementation 'com.android.support:appcompat-v7:26.1.0'
implementation 'com.android.support:support-v4:26.1.0'
implementation 'com.google.android:flexbox:0.2.6'
implementation 'com.android.support:design:26.1.0'
}

 

Para esta versão do Gradle foi adicionada a referência ao FlexboxLayout, que faremos uso no trecho de código de apresentação de estrelas de campeonatos ganhos por cada time.

Ambas as configurações Gradle não mais terão atualizações neste projeto, pois o BottomNavigationView está presente em com.android.support:design, já referenciada no Gradle App Level.

Configurações AndroidManifest

A seguir as configurações do AndroidManifest.xml, que por sinal são as mesmas do projeto inicial, somente adicionamos a referência a atividade de detalhes:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="thiengo.com.br.torneioscontinentais">

<application
android:allowBackup="true"
android:hardwareAccelerated="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">

<activity
android:name=".atividade.MainActivity"
android:label="@string/app_name"
android:theme="@style/AppTheme.NoActionBar">

<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<activity
android:name=".atividade.DetalhesTimeActivity"
android:label="@string/title_activity_detalhes_time"
android:theme="@style/AppTheme.NoActionBar" />
</application>
</manifest>

Configurações de estilo

Para os arquivos de: definição de estilo; conteúdo em texto; e dimensões. Para estes teremos poucas modificações ao que é gerado para um projeto inicial Navigation Drawer Activity.

Vamos iniciar com o arquivo de definição de cores, /res/values/colors.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#1a237e</color>
<color name="colorPrimaryDark">#000051</color>
<color name="colorAccent">#2e7d32</color>

<color name="colorNavigation">#F5F5F6</color>
<color name="colorNavigationItemDefault">#777777</color>
<color name="colorNavigationItemChecked">#FFFFFF</color>

<color name="colorItemTexto">#47525E</color>
<color name="colorItemTextoPequeno">#777</color>
</resources>

 

Assim o arquivo de definição de dimensões, /res/values/dimens.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="activity_horizontal_margin">16dp</dimen>
<dimen name="activity_vertical_margin">16dp</dimen>
<dimen name="nav_header_vertical_spacing">8dp</dimen>
<dimen name="nav_header_height">176dp</dimen>
<dimen name="fab_margin">16dp</dimen>
<dimen name="app_bar_height">220dp</dimen>
<dimen name="text_margin">16dp</dimen>
</resources>

 

Agora o arquivo de definição de conteúdos em texto, /res/values/strings.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Torneios Continentais</string>

<string name="navigation_drawer_open">Abrir navigation drawer</string>
<string name="navigation_drawer_close">Fechar navigation drawer</string>
<string name="title_activity_detalhes_time">DetalhesTimeActivity</string>

<string name="historia">História</string>
<string name="torcida">Torcida</string>
<string name="descricao">
Lorem Ipsum é simplesmente uma simulação de texto da indústria tipográfica e de impressos, e
vem sendo utilizado desde o século XVI, quando um impressor desconhecido pegou uma bandeja de
tipos e os embaralhou para fazer um livro de modelos de tipos. Lorem Ipsum sobreviveu não só
a cinco séculos, como também ao salto para a editoração eletrônica, permanecendo essencialmente
inalterado. Se popularizou na década de 60, quando a Letraset lançou decalques contendo passagens
de Lorem Ipsum, e mais recentemente quando passou a ser integrado a softwares de editoração
eletrônica como Aldus PageMaker.
\n\n

Ao contrário do que se acredita, Lorem Ipsum não é simplesmente um texto randômico. Com mais
de 2000 anos, suas raízes podem ser encontradas em uma obra de literatura latina clássica datada
de 45 AC. Richard McClintock, um professor de latim do Hampden-Sydney College na Virginia,
pesquisou uma das mais obscuras palavras em latim, consectetur, oriunda de uma passagem de
Lorem Ipsum, e, procurando por entre citações da palavra na literatura clássica, descobriu a sua
indubitável origem. Lorem Ipsum vem das seções 1.10.32 e 1.10.33 do "de Finibus Bonorum et Malorum"
(Os Extremos do Bem e do Mal), de Cícero, escrito em 45 AC. Este livro é um tratado de teoria da
ética muito popular na época da Renascença. A primeira linha de Lorem Ipsum, "Lorem Ipsum dolor
sit amet..." vem de uma linha na seção 1.10.32.
\n\n

O trecho padrão original de Lorem Ipsum, usado desde o século XVI, está reproduzido abaixo para os
interessados. Seções 1.10.32 e 1.10.33 de "de Finibus Bonorum et Malorum" de Cicero também foram
reproduzidas abaixo em sua forma exata original, acompanhada das versões para o inglês da tradução
feita por H. Rackham em 1914.
</string>
</resources>

 

Por fim os dois arquivos de definição de tema do projeto. Começando pelo mais genérico, /res/values/styles.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>

<style
name="AppTheme"
parent="Theme.AppCompat.Light.DarkActionBar">

<item name="android:windowBackground">@drawable/background</item>
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>

<style
name="AppTheme.NoActionBar">

<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
</style>

<style
name="AppTheme.AppBarOverlay"
parent="ThemeOverlay.AppCompat.Dark.ActionBar" />

<style
name="AppTheme.PopupOverlay"
parent="ThemeOverlay.AppCompat.Light" />
</resources>

 

E então o mais específico, somente para versões iguais ou superiores a API 21, /res/values-v21/styles.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
<style
name="AppTheme.NoActionBar">

<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="android:statusBarColor">@android:color/transparent</item>
</style>
</resources>

Classes de domínio

Teremos duas classes de domínio: Jogo e Time. Ambas no pacote /dominio. Note que no projeto não apresentaremos os resultados dos jogos, somente os confrontos e alguns dados de jogo e de cada time.

A seguir o código da classe Time:

class Time(
val nome: String,
val idEscudo: Int,
val idBandeiraPais: Int,
val qtdCampeonatos: Int) : Parcelable {

constructor(source: Parcel) : this(
source.readString(),
source.readInt(),
source.readInt(),
source.readInt()
)

override fun describeContents() = 0

override fun writeToParcel(dest: Parcel, flags: Int) = with(dest) {
writeString(nome)
writeInt(idEscudo)
writeInt(idBandeiraPais)
writeInt(qtdCampeonatos)
}

companion object {
@JvmField
val KEY = "key_time"

@JvmField
val CREATOR: Parcelable.Creator<Time> = object : Parcelable.Creator<Time> {
override fun createFromParcel(source: Parcel): Time = Time(source)
override fun newArray(size: Int): Array<Time?> = arrayOfNulls(size)
}
}
}

 

A implementação do Parcelable é necessária, pois o objeto Time poderá ser enviado, via Bundle de Intent, a atividade de detalhes.

idEscudoidBandeiraPais estão como inteiros, pois são imagens já presentes internamente ao projeto, estaremos utilizando uma base de dados local, sem carregamento remoto.

Agora o código da pequena classe Jogo:

class Jogo(
val timeCasa: Time,
val timeVisitante: Time,
val estadio: String,
val data: String,
val horario: String )

Banco de dados simulado

Dentro do pacote /dados crie a classe BancoDados. Essa será a base Mock, base de dados simulados do projeto, prática comum em ambientes de desenvolvimento para acelerar a construção de projetos de software.

Para essa classe não trabalharemos a criação de instância, logo todos os métodos serão companion objects:

class BancoDados {

companion object {
fun getUefaTimes() = listOf<Jogo>(
Jogo(
Time("Juventus", R.drawable.es_juventus, R.drawable.ic_italia, 2),
Time("Tottenham", R.drawable.es_tottenham, R.drawable.ic_inglaterra, 0),
"Juventus Stadium",
"13/02/2018",
"17:45"
),
Jogo(
Time("Basel", R.drawable.es_basel, R.drawable.ic_suica, 0),
Time("Manchester City", R.drawable.es_manchester_city, R.drawable.ic_inglaterra, 0),
"St. Jakob-Park",
"13/02/2018",
"17:45"
),
Jogo(
Time("Porto", R.drawable.es_porto, R.drawable.ic_portugal, 2),
Time("Liverpool", R.drawable.es_liverpool, R.drawable.ic_inglaterra, 5),
"Estádio do Dragão",
"14/02/2018",
"17:45"
),
Jogo(
Time("Real Madrid", R.drawable.es_real_madrid, R.drawable.ic_espanha, 12),
Time("Paris Saint Germain", R.drawable.es_psg, R.drawable.ic_franca, 0),
"Santiago Bernabéu",
"14/02/2018",
"17:45"
),
Jogo(
Time("Bayern de Munique", R.drawable.es_bayern_munchen, R.drawable.ic_alemanha, 5),
Time("Besiktas", R.drawable.es_besiktas, R.drawable.ic_turquia, 0),
"Allianz Arena",
"20/02/2018",
"16:45"
),
Jogo(
Time("Chelsea", R.drawable.es_chelsea, R.drawable.ic_inglaterra, 1),
Time("Barcelona", R.drawable.es_barcelona, R.drawable.ic_espanha, 5),
"Stamford Bridge",
"20/02/2018",
"16:45"
),
Jogo(
Time("Sevilla", R.drawable.es_sevilla, R.drawable.ic_espanha, 0),
Time("Manchester United", R.drawable.es_manchester_united, R.drawable.ic_inglaterra, 3),
"Ramón Sánchez Pizjuán",
"21/02/2018",
"16:45"
),
Jogo(
Time("Shakhtar Donetsk", R.drawable.es_shakhtar_donetsk, R.drawable.ic_ucrania, 0),
Time("Roma", R.drawable.es_roma, R.drawable.ic_italia, 0),
"Arena Lviv",
"21/02/2018",
"16:45"
)
)

fun getLibertadoresTimes() = listOf<Jogo>(
Jogo(
Time("Godoy Cruz", R.drawable.es_godoy_cruz, R.drawable.ic_argentina, 0),
Time("Grêmio", R.drawable.es_gremio, R.drawable.ic_brasil, 3),
"Malvinas Argentinas",
"04/07/2017",
"21:45"
),
Jogo(
Time("Guaraní", R.drawable.es_guarani, R.drawable.ic_paraguai, 0),
Time("River Plate", R.drawable.es_river_plate, R.drawable.ic_argentina, 3),
"Rogelio Livieres",
"04/07/2017",
"21:45"
),
Jogo(
Time("Atlético-PR", R.drawable.es_atletico_paranaense, R.drawable.ic_brasil, 0),
Time("Santos", R.drawable.es_santos, R.drawable.ic_brasil, 3),
"Arena da Baixada",
"05/07/2017",
"21:45"
),
Jogo(
Time("Jorge Wilstermann", R.drawable.es_jorge_wilstermann, R.drawable.ic_bolivia, 0),
Time("Atlético-MG", R.drawable.es_atletico_mineiro, R.drawable.ic_brasil, 1),
"Félix Capriles",
"05/07/2017",
"21:45"
),
Jogo(
Time("Barcelona SC", R.drawable.es_barcelona_sc, R.drawable.ic_equador, 0),
Time("Palmeiras", R.drawable.es_palmeiras, R.drawable.ic_brasil, 1),
"Monumental Isidro Romero Carbo",
"05/07/2017",
"21:45"
),
Jogo(
Time("The Strongest", R.drawable.es_the_strongest, R.drawable.ic_bolivia, 0),
Time("Lanús", R.drawable.es_lanus, R.drawable.ic_argentina, 0),
"Hernando Siles",
"06/07/2017",
"21:45"
),
Jogo(
Time("Emelec", R.drawable.es_emelec, R.drawable.ic_equador, 0),
Time("San Lorenzo", R.drawable.es_san_lorenzo, R.drawable.ic_argentina, 1),
"George Capwell",
"06/07/2017",
"21:45"
),
Jogo(
Time("Nacional", R.drawable.es_nacional, R.drawable.ic_uruguai, 3),
Time("Botafogo", R.drawable.es_botafogo, R.drawable.ic_brasil, 0),
"Gran Parque Central",
"07/07/2017",
"21:45"
)
)

fun getAfcTimes() = listOf<Jogo>(
Jogo(
Time("Al-Hilal", R.drawable.es_al_hilal, R.drawable.ic_arabia_saudita, 2),
Time("Esteghlal Khuzestan", R.drawable.es_esteghlal_khuzestan, R.drawable.ic_ira, 0),
"King Fahd Stadium",
"23/05/2017",
"20:15"
),
Jogo(
Time("Esteghlal F.C.", R.drawable.es_esteghlal, R.drawable.ic_ira, 1),
Time("Al-Ain", R.drawable.es_al_ain, R.drawable.ic_emirados_arabes_unidos, 1),
"Azadi",
"22/05/2017",
"20:15"
),
Jogo(
Time("Al Ahli", R.drawable.es_al_ahli, R.drawable.ic_emirados_arabes_unidos, 0),
Time("Al-Ahli Dubai", R.drawable.es_al_ahli_dubai, R.drawable.ic_emirados_arabes_unidos, 0),
"Al Rashid",
"22/05/2017",
"20:15"
),
Jogo(
Time("Al-Duhail", R.drawable.es_al_duhail, R.drawable.ic_qatar, 0),
Time("Persepolis", R.drawable.es_persepolis, R.drawable.ic_ira, 0),
"Abdullah bin Khalifa",
"23/05/2017",
"20:15"
),
Jogo(
Time("Shanghai East Asia", R.drawable.es_shanghai_east_asia, R.drawable.ic_china, 0),
Time("Jiangsu Suning", R.drawable.es_jiangsu_suning, R.drawable.ic_china, 0),
"Estádio de Shanghai",
"24/05/2017",
"20:15"
),
Jogo(
Time("Kashima Antlers", R.drawable.es_kashima_antlers, R.drawable.ic_japao, 0),
Time("Guangzhou Evergrande", R.drawable.es_guangzhou_evergrande, R.drawable.ic_china, 2),
"Kashima",
"23/05/2017",
"20:15"
),
Jogo(
Time("Kawasaki", R.drawable.es_kawasaki, R.drawable.ic_japao, 0),
Time("Muangthong", R.drawable.es_muangthong, R.drawable.ic_tailandia, 0),
"Todoroki Athletics Stadium",
"23/05/2017",
"20:15"
),
Jogo(
Time("Jeju", R.drawable.es_jeju, R.drawable.ic_corea_do_sul, 0),
Time("Urawa Reds", R.drawable.es_urawa_reds, R.drawable.ic_japao, 1),
"Jeju World Cup Stadium",
"24/05/2017",
"20:15"
)
)
}
}

 

Vale ressaltar que todas as imagens e fontes do projeto você encontra no /res do GtiHub dele.

Fragmento abstrato, classe pai

No pacote /fragment crie um novo fragmento em branco com o nome FragmentAbstrato, sem incluir métodos fábrica e interface de callback:

  • No pacote /fragment clique com o botão direito do mouse;
  • Vá até New;
  • Vá em Fragment;
  • Clique em Fragment (Blank);
  • Em Fragment Name coloque FragmentAbstrato;
  • Em Fragment Layout Name coloque lista_jogos;
  • Desmarque include fragment factory methods?;
  • Desmarque include interface callbacks?;
  • Clique em Finish.

A seguir o código XML do layout que será carregado a partir de FragmentAbstrato, /res/layout/lista_jogos.xml:

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.RecyclerView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/rv_jogos"
android:layout_width="match_parent"
android:layout_height="match_parent" />

 

Abaixo o código da classe FragmentAbstrato:

/*
* Classe responsável por evitar códigos repetidos em
* todas as subclasses caso ela não fosse criada.
* */
abstract class FragmentAbstract : Fragment() {
var rvState: Parcelable? = null

companion object {
@JvmField val KEY = "fragment"
@JvmField val TITULO = "titulo"
}

override fun onCreateView(
inflater: LayoutInflater?,
container: ViewGroup?,
savedInstanceState: Bundle? ): View? {

return inflater!!.inflate(R.layout.lista_jogos, container, false)
}

override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)

rv_jogos.setHasFixedSize(true)
val layoutManager = LinearLayoutManager(activity)
rv_jogos.layoutManager = layoutManager
val divider = DividerItemDecoration(activity, layoutManager.orientation)
rv_jogos.addItemDecoration(divider)

rvState()
}

override fun onPause() {
super.onPause()
rvState()
}

override fun onResume() {
super.onResume()
rv_jogos.getLayoutManager().onRestoreInstanceState(rvState);
}

/*
* Junto ao código em onActivityCreated(), onResume() e onPause()
* rvState permite salvar o status do RecyclerView e então voltar
* com ele na mesma posição.
* */
private fun rvState(){
rvState = rv_jogos.getLayoutManager().onSaveInstanceState()
}
}

 

Esta classe contém todo o código que será necessário nas subclasses fragments de apresentação de confrontos dos torneios, dessa forma prevenimos o problema de repetição de código.

Note que o operador !! faz com que um NullPointerException seja gerado caso não haja um valor não-null sendo utilizado.

Os códigos de inicialização de RecyclerView estão no onCreateActivity(), pois ainda no onCreateView() não é possível acessar o recycler por meio da sintaxe do kotlin-android-extensions.

A classe Fragment que está sendo utilizada como classe pai é do pacote de suporte: android.support.v4.app. Alias, neste projeto, todas as entidades relacionadas a fragment são do pacote de suporte.

Fragmento UEFA Champions League

Ainda no pacote /fragment crie um outro fragmento em branco, dessa vez com o nome UEFAFragment e também sem a necessidade de layout:

class UefaFragment : FragmentAbstrato() {

companion object {
@JvmField val TITULO = "UEFA Champions League"
}

override fun onStart() {
super.onStart()
rv_jogos.adapter = JogosAdapter(activity, BancoDados.getUefaTimes())
}
}

 

A inicialização de adapter do RecyclerView está no onStart() somente por didática, para mostrar que a partir da indexação de layout ao fragmento no método onCreateView(), todos os métodos posteriores permitem o acesso ao recycler por meio da sintaxe do kotlin-android-extensions.

Fragmento Taça Libertadores

No pacote /fragment crie um novo fragmento em branco, sem layout, com o rótulo LibertadoresFragment:

class LibertadoresFragment : FragmentAbstrato() {

companion object {
@JvmField val TITULO = "Taça Libertadores"
}

override fun onStart() {
super.onStart()
rv_jogos.adapter = JogosAdapter(activity, BancoDados.getLibertadoresTimes())
}
}

Fragmento Liga dos Campeões da AFC

E por fim o último fragmento desta primeira parte do projeto. Ainda no pacote /fragment crie um novo fragmento em branco, sem layout, com o rótulo AfcFragment:

class AfcFragment : FragmentAbstrato() {

companion object {
@JvmField val TITULO = "Liga dos Campeões da AFC"
}

override fun onStart() {
super.onStart()
rv_jogos.adapter = JogosAdapter(activity, BancoDados.getAfcTimes())
}
}

Classe adaptadora

Para a classe que apresentará os confrontos de clubes, vamos iniciar com a apresentação do layout de itens, /res/layout/jogo.xml:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/rv_jogos"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">

<LinearLayout
android:id="@+id/ll_tc"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_toLeftOf="@+id/iv_tc_bandeira_pais"
android:layout_toStartOf="@+id/iv_tc_bandeira_pais"
android:gravity="center"
android:orientation="vertical">

<ImageView
android:id="@+id/iv_tc_escudo"
android:layout_width="wrap_content"
android:layout_height="80dp"
android:scaleType="fitCenter" />

<TextView
android:id="@+id/tv_tc_nome"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:gravity="center"
android:textColor="@color/colorItemTexto"
android:textSize="16sp" />

<include layout="@layout/estrelas" />
</LinearLayout>

<ImageView
android:id="@+id/iv_tc_bandeira_pais"
android:layout_width="17dp"
android:layout_height="12dp"
android:layout_alignTop="@+id/tv_vs"
android:layout_marginTop="6dp"
android:layout_toLeftOf="@+id/tv_vs"
android:layout_toStartOf="@+id/tv_vs"
android:background="@drawable/bandeira_borda"
android:scaleType="fitCenter" />

<TextView
android:id="@+id/tv_data"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"
android:textColor="@color/colorItemTextoPequeno"
android:textSize="10sp" />

<TextView
android:id="@+id/tv_horario"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/tv_data"
android:layout_centerHorizontal="true"
android:textColor="@color/colorItemTextoPequeno"
android:textSize="10sp" />

<TextView
android:id="@+id/tv_vs"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/tv_horario"
android:layout_centerHorizontal="true"
android:layout_marginBottom="12dp"
android:layout_marginEnd="6dp"
android:layout_marginLeft="6dp"
android:layout_marginRight="6dp"
android:layout_marginStart="6dp"
android:layout_marginTop="12dp"
android:text="- VS -"
android:textColor="@color/colorItemTexto"
android:textSize="16sp" />

<TextView
android:id="@+id/tv_estadio"
android:layout_width="56dp"
android:layout_height="wrap_content"
android:layout_below="@+id/tv_vs"
android:layout_centerHorizontal="true"
android:gravity="center"
android:textColor="@color/colorItemTextoPequeno"
android:textSize="10sp" />

<ImageView
android:id="@+id/iv_tv_bandeira_pais"
android:layout_width="17dp"
android:layout_height="12dp"
android:layout_alignTop="@+id/tv_vs"
android:layout_marginTop="6dp"
android:layout_toEndOf="@+id/tv_vs"
android:layout_toRightOf="@+id/tv_vs"
android:background="@drawable/bandeira_borda"
android:scaleType="fitCenter" />

<LinearLayout
android:id="@+id/ll_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_toEndOf="@+id/iv_tv_bandeira_pais"
android:layout_toRightOf="@+id/iv_tv_bandeira_pais"
android:gravity="center"
android:orientation="vertical">

<ImageView
android:id="@+id/iv_tv_escudo"
android:layout_width="wrap_content"
android:layout_height="80dp"
android:scaleType="fitCenter" />

<TextView
android:id="@+id/tv_tv_nome"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:gravity="center"
android:textColor="@color/colorItemTexto"
android:textSize="16sp" />

<include layout="@layout/estrelas" />
</LinearLayout>
</RelativeLayout>

 

Agora o drawable que é adicionado ao background dos ImageView de bandeira de país para colocar borda neles, /res/drawable/bandeira_borda.xml:

<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android">
<solid
android:color="#dddddd" />
<stroke
android:width="1dp"
android:color="#dddddd" />
<padding
android:bottom="1dp"
android:left="1dp"
android:right="1dp"
android:top="1dp" />
</shape>

 

A seguir a imagem de como ficará a apresentação das bandeiras de países, com uso da borda:

Bandeiras de países com borda cinza claro

Então o arquivo XML de estrelas de campeonatos continentais ganhos, /res/layout/estrelas.xml:

<com.google.android.flexbox.FlexboxLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:flexWrap="wrap"
app:justifyContent="center">

<ImageView
android:layout_width="9dp"
android:layout_height="11dp"
android:layout_marginLeft="1dp"
android:layout_marginRight="1dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_star" />

<ImageView
android:layout_width="9dp"
android:layout_height="11dp"
android:layout_marginLeft="1dp"
android:layout_marginRight="1dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_star" />

<ImageView
android:layout_width="9dp"
android:layout_height="11dp"
android:layout_marginLeft="1dp"
android:layout_marginRight="1dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_star" />

<ImageView
android:layout_width="9dp"
android:layout_height="11dp"
android:layout_marginLeft="1dp"
android:layout_marginRight="1dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_star" />

<ImageView
android:layout_width="9dp"
android:layout_height="11dp"
android:layout_marginLeft="1dp"
android:layout_marginRight="1dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_star" />

<ImageView
android:layout_width="9dp"
android:layout_height="11dp"
android:layout_marginLeft="1dp"
android:layout_marginRight="1dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_star" />

<ImageView
android:layout_width="9dp"
android:layout_height="11dp"
android:layout_marginLeft="1dp"
android:layout_marginRight="1dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_star" />

<ImageView
android:layout_width="9dp"
android:layout_height="11dp"
android:layout_marginLeft="1dp"
android:layout_marginRight="1dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_star" />

<ImageView
android:layout_width="9dp"
android:layout_height="11dp"
android:layout_marginLeft="1dp"
android:layout_marginRight="1dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_star" />

<ImageView
android:layout_width="9dp"
android:layout_height="11dp"
android:layout_marginLeft="1dp"
android:layout_marginRight="1dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_star" />

<ImageView
android:layout_width="9dp"
android:layout_height="11dp"
android:layout_marginLeft="1dp"
android:layout_marginRight="1dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_star" />

<ImageView
android:layout_width="9dp"
android:layout_height="11dp"
android:layout_marginLeft="1dp"
android:layout_marginRight="1dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_star" />
</com.google.android.flexbox.FlexboxLayout>

 

A estratégia para o arquivo estrelas.xml foi a seguinte: colocar dentro do FlexboxLayout o maior número de estrelas, ImageView, para representar a quantidade de títulos de qualquer time. O máximo foi 12, pois é o número de títulos do Real Madrid que é o time com mais títulos continentais dentre todos os clubes de todos os três campeonatos representados no aplicativo.

Essa abordagem de já manter as Views de estrela é mais eficiente ante construir via código cada ImageView de acordo com a demanda.

Abaixo um print de exemplo da apresentação das estrelas de torneios ganhos:

A seguir o diagrama do layout jogo.xml:

Diagrama do layout Jogo.xml

Assim o código Kotlin de JogoAdapter:

class JogosAdapter(
private val context: Context,
private val jogos: List<Jogo>) :
RecyclerView.Adapter<JogosAdapter.ViewHolder>() {

override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int) : JogosAdapter.ViewHolder {

val v = LayoutInflater
.from(context)
.inflate(R.layout.jogo, parent, false)

return ViewHolder(v)
}

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.setDados(jogos[position])
}

override fun getItemCount(): Int {
return jogos.size
}

inner class ViewHolder(itemView: View) :
RecyclerView.ViewHolder(itemView),
View.OnClickListener {

/* Time casa */
var llTc: LinearLayout
var ivTcBandeiraPais: ImageView
var ivTcEscudo: ImageView
var tvTcNome: TextView

/* Time visitante */
var llTv: LinearLayout
var ivTvBandeiraPais: ImageView
var ivTvEscudo: ImageView
var tvTvNome: TextView

var tvData: TextView
var tvHorario: TextView
var tvEstadio: TextView

init {
llTc = itemView.findViewById(R.id.ll_tc)
llTc.setOnClickListener(this)
ivTcBandeiraPais = itemView.findViewById(R.id.iv_tc_bandeira_pais)
ivTcEscudo = itemView.findViewById(R.id.iv_tc_escudo)
tvTcNome = itemView.findViewById(R.id.tv_tc_nome)

llTv = itemView.findViewById(R.id.ll_tv)
llTv.setOnClickListener(this)
ivTvBandeiraPais = itemView.findViewById(R.id.iv_tv_bandeira_pais)
ivTvEscudo = itemView.findViewById(R.id.iv_tv_escudo)
tvTvNome = itemView.findViewById(R.id.tv_tv_nome)

tvData = itemView.findViewById(R.id.tv_data)
tvHorario = itemView.findViewById(R.id.tv_horario)
tvEstadio = itemView.findViewById(R.id.tv_estadio)
}

fun setDados(jogo: Jogo) {
ivTcBandeiraPais.setImageResource( jogo.timeCasa.idBandeiraPais )
ivTcEscudo.setImageResource( jogo.timeCasa.idEscudo )
tvTcNome.text = jogo.timeCasa.nome
llTc.tag = jogo.timeCasa /* Hackcode para recuperar o objeto correto no listener de clique. */
estrelasCampeonatosGanhos(llTc, jogo.timeCasa.qtdCampeonatos)

ivTvBandeiraPais.setImageResource( jogo.timeVisitante.idBandeiraPais )
ivTvEscudo.setImageResource( jogo.timeVisitante.idEscudo )
tvTvNome.text = jogo.timeVisitante.nome
llTv.tag = jogo.timeVisitante /* Hackcode para recuperar o objeto correto no listener de clique. */
estrelasCampeonatosGanhos(llTv, jogo.timeVisitante.qtdCampeonatos)

tvData.text = jogo.data
tvHorario.text = jogo.horario
tvEstadio.text = jogo.estadio
}

/*
* O número de ImageViews, com o ícone de estrela, presentes no
* layout estrelas.xml é o mesmo número do time que mais ganhou
* títulos continentais, 12. Assim o algoritmo abaixo somente
* tem de trabalhar o apresentar / esconder dos ImageViews. Essa
* é uma abordagem mais eficiente do que ter de criar e destruir
* ImageViews em tempo de execução.
* */
private fun estrelasCampeonatosGanhos(ll: LinearLayout, qtdCampeonatos: Int){

for ( i in 0..ll.childCount ) {

if( ll.getChildAt(i) is FlexboxLayout ){
val view = ll.getChildAt(i) as FlexboxLayout

for ( j in 0..(qtdCampeonatos - 1) ) {
view.getChildAt(j).visibility = View.VISIBLE
}
for ( j in qtdCampeonatos..(view.childCount - 1) ) {
view.getChildAt(j).visibility = View.GONE
}
}
}
}

/* O parâmetro view é na verdade o componente LinearLayout que
* recebeu o listener de clique em init{}. Na propriedade tag
* há o objeto time correto.
* */
override fun onClick(view: View?) {
val intent = Intent(context, DetalhesTimeActivity::class.java)
intent.putExtra(Time.KEY, view!!.tag as Time)
context.startActivity(intent)
}
}
}

 

Não deixe de ler os comentários presentes em código, pois eles explicam o porquê dos trechos extra código de classe adaptadora.

Atividade de detalhes de clube

Para a atividade de detalhes de clube, vamos iniciar com o layout que contém os componentes visuais de apresentação de texto, /res/layout/content_detalhes_time.xml:

<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.NestedScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:context="thiengo.com.br.torneioscontinentais.atividade.DetalhesTimeActivity"
tools:showIn="@layout/activity_detalhes_time">

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="@dimen/text_margin">

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:fontFamily="@font/open_sans_condensed_light"
android:text="@string/historia"
android:textSize="38sp" />

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/descricao" />

</LinearLayout>
</android.support.v4.widget.NestedScrollView>

 

Note que como estamos trabalhando um exemplo de aplicativo, foi escolhido sempre apresentar o mesmo texto de descrição para todos os times, isso para facilitar também a compreensão do código.

A seguir o diagrama do layout anterior:

Diagrama do layout content_detalhes_time.xml

No primeiro TextView do layout anterior estou utilizando uma fonte personalizada, mais sobre essa característica no conteúdo a seguir: Fontes em XML, Android O. Configuração e Uso.

Agora o layout XML principal de DetalhesTimeActivity, /res/layout/activity_detalhes_time.xml:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white"
android:fitsSystemWindows="true"
tools:context="thiengo.com.br.torneioscontinentais.atividade.DetalhesTimeActivity">

<android.support.design.widget.AppBarLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/app_bar"
android:layout_width="match_parent"
android:layout_height="@dimen/app_bar_height"
android:fitsSystemWindows="true"
android:theme="@style/AppTheme.AppBarOverlay">

<android.support.design.widget.CollapsingToolbarLayout
android:id="@+id/toolbar_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
app:contentScrim="?attr/colorPrimary"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
app:toolbarId="@+id/toolbar">

<ImageView
android:id="@+id/iv_time"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/torcida"
android:contentDescription="@string/torcida"
android:fitsSystemWindows="true"
android:paddingBottom="60dp"
android:paddingLeft="170dp"
android:paddingStart="170dp"
android:paddingTop="35dp"
android:scaleType="fitCenter"
android:src="@drawable/torcida"
app:layout_collapseMode="parallax" />

<View
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/gradiente" />

<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin"
app:popupTheme="@style/AppTheme.PopupOverlay" />

</android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>

<include layout="@layout/content_detalhes_time" />

<android.support.design.widget.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/fab_margin"
app:layout_anchor="@id/app_bar"
app:layout_anchorGravity="bottom|end"
app:srcCompat="@drawable/ic_share_white_24dp" />
</android.support.design.widget.CoordinatorLayout>

 

Em <View> temos um drawable de gradiente sendo aplicado, mais precisamente o /res/drawable/gradiente.xml:

<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:angle="90"
android:centerColor="#33000000"
android:endColor="@android:color/transparent"
android:startColor="#dd000000" />

<corners android:radius="0dp" />
</shape>

 

Com ele conseguimos o seguinte efeito de gradiente na barra de topo, expandida, da atividade de detalhes:

Barra de topo da atividade de detalhes

Note que um pouco diferente do que foi definido no protótipo estático no início desta primeira fase de projeto, optamos por colocar uma mesma imagem de background para todos os clubes, porém com o logo do clube em destaque.

No ImageView podemos seguramente trabalhar duas imagens, uma de background e outra de source. A de source, android:src, sempre vem em primeiro plano neste componente.

Assim o diagrama do layout activity_detalhes_time.xml:

Diagrama do layout activity_detalhes_time.xml

E por fim o código Kotlin da classe DetalhesTimeActivity:

class DetalhesTimeActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_detalhes_time)
setSupportActionBar(toolbar)

/*
* Para liberar o back button na barra de topo da
* atividade.
* */
if (supportActionBar != null) {
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
supportActionBar!!.setDisplayShowHomeEnabled(true)
}
}

/*
* Hack code para que a atualização de título da atividade
* seja realizada. Logo foi escolhido para ficar também
* neste trecho de código a atualização do source do
* ImageView de topo de tela.
* */
override fun onResume() {
super.onResume()

val time = intent.getParcelableExtra<Time>(Time.KEY)

toolbar.title = time!!.nome
iv_time.setImageResource( time.idEscudo )
}

/*
* Para permitir que o back button tenha a ação de volta para
* a atividade anterior.
* */
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.getItemId() == android.R.id.home) {
finish()
return true
}
return super.onOptionsItemSelected(item)
}
}

Atividade principal

Como para os códigos da atividade de detalhes, aqui vamos iniciar com a apresentação dos códigos XML. Primeiro o XML que conterá as listas de jogos junto a barra de topo que deverá ser atualizada de acordo com o torneio selecionado, o layout /res/layout/app_bar_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white"
tools:context="thiengo.com.br.torneioscontinentais.atividade.MainActivity">

<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay">

<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay" />

</android.support.design.widget.AppBarLayout>

<RelativeLayout
android:id="@+id/rl_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</android.support.design.widget.CoordinatorLayout>

 

Abaixo o diagrama do layout anterior:

Diagrama do layout app_bar_main.xml

Agora o XML de menu que é referenciado no NavigationView, /res/menu/activity_main_drawer.xml:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:showIn="navigation_view">

<group android:checkableBehavior="single">
<item
android:id="@+id/nav_uefa"
android:checked="true"
android:icon="@drawable/ic_uefa"
android:title="UEFA Champions League" />
<item
android:id="@+id/nav_libertadores"
android:icon="@drawable/ic_libertadores"
android:title="Taça Libertadores" />
<item
android:id="@+id/nav_afc"
android:icon="@drawable/ic_afc"
android:title="Liga dos Campeões da AFC" />
</group>
</menu>

 

Então o diagrama do XML de menu anterior:

Diagrama do menu activity_main_drawer.xml

Assim o XML do layout principal, que referencia também os anteriores, /res/layout/activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:openDrawer="start">

<include
layout="@layout/app_bar_main"
android:layout_width="match_parent"
android:layout_height="match_parent" />

<android.support.design.widget.NavigationView
android:id="@+id/nav_view"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
android:background="@color/colorNavigation"
android:fitsSystemWindows="true"
app:itemBackground="@drawable/nav_item_background_color"
app:itemIconTint="@drawable/nav_item_color"
app:itemTextColor="@drawable/nav_item_color"
app:menu="@menu/activity_main_drawer" />

</android.support.v4.widget.DrawerLayout>

 

Para que os itens e os background deles tivessem as cores corretas como definidas no protótipo estático, foi necessária a criação de dois outros drawable.

Para background, /res/drawable/nav_item_background_color.xml:

<?xml version="1.0" encoding="utf-8"?>
<selector
xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:drawable="@color/colorAccent"
android:state_checked="true" />

<item
android:drawable="@android:color/transparent" />
</selector>

 

Para ícones e rótulos, /res/drawable/nav_item_color.xml:

<?xml version="1.0" encoding="utf-8"?>
<selector
xmlns:android="http://schemas.android.com/apk/res/android">

<item
android:color="@color/colorNavigationItemChecked"
android:state_checked="true" />

<item
android:color="@color/colorNavigationItemDefault" />
</selector>

 

Note que em arquivos <selector> as definições mais específicas vem em primeiro no arquivo, a ordem importa.

Assim segue o diagrama de activity_main.xml:

Diagrama do menu activity_main.xml

Por fim o código Kotlin de MainActivity:

class MainActivity :
AppCompatActivity(),
NavigationView.OnNavigationItemSelectedListener {

var titulo: String = UefaFragment.TITULO

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(toolbar)

initDrawerNavigation()

titulo = savedInstanceState?.getString(FragmentAbstract.TITULO) ?: titulo

/*
* Caso seja a primeira vez que o onCreate() esteja sendo
* invocado.
* */
if( supportFragmentManager.findFragmentByTag(FragmentAbstract.KEY) == null ) {
trocaFragmento(UefaFragment())
}
}

private fun initDrawerNavigation(){
val toggle = ActionBarDrawerToggle(
this,
drawer_layout,
toolbar,
R.string.navigation_drawer_open,
R.string.navigation_drawer_close)
drawer_layout.addDrawerListener(toggle)
toggle.syncState()

nav_view.setNavigationItemSelectedListener(this)
}

override fun onNavigationItemSelected(item: MenuItem): Boolean {
val fragmento: Fragment

when(item.itemId) {
R.id.nav_libertadores -> {
fragmento = LibertadoresFragment()
titulo = LibertadoresFragment.TITULO
}
R.id.nav_afc -> {
fragmento = AfcFragment()
titulo = AfcFragment.TITULO
}
else -> {
fragmento = UefaFragment()
titulo = UefaFragment.TITULO
}
}

toolbar.title = titulo
trocaFragmento( fragmento )

drawer_layout.closeDrawer(GravityCompat.START)
return true
}

private fun trocaFragmento( fragment: Fragment ){
supportFragmentManager
.beginTransaction()
.replace(R.id.rl_container, fragment, FragmentAbstract.KEY)
.commit()
}

/*
* Hackcode para apresentar o título correto do fragmento
* atual.
* */
override fun onResume() {
super.onResume()
toolbar.title = titulo
}

/*
* Para que seja possível sempre apresentar o título correto
* em tela.
* */
override fun onSaveInstanceState(outState: Bundle?) {
outState?.putString( FragmentAbstract.TITULO, titulo )
super.onSaveInstanceState( outState )
}

override fun onBackPressed() {
if (drawer_layout.isDrawerOpen(GravityCompat.START)) {
drawer_layout.closeDrawer(GravityCompat.START)
}
else {
super.onBackPressed()
}
}
}

Atualização para menu BottomNavigationView

Apesar de o uso do menu gaveta estar sendo aceitável na versão atual do projeto, o espaço que sobra depois dos três itens de menu não permiti um design enxuto como ocorreria se houvesse mais itens e cabeçalho de menu sendo utilizados: 

Menu gaveta aberto

Devido a isso iremos utilizar o BottomNavigationView para deixar o design do aplicativo mais robusto.

Protótipo estático da nova versão de projeto

A seguir o protótipo estático somente das telas que modificaremos:

Lista de jogos do projeto Android com BottomNavigation

Lista de jogos

Área de detalhes de time do projeto Android com BottomNavigation

Área de detalhes de clube


Atualizando o layout principal

Nosso primeiro passo é atualizar o layout /res/layout/activity_main.xml. Remover as Views de menu gaveta e colocar o BottomNavigationView.

Vamos iniciar por um novo XML drawable para mudança correta de cor dos ícones e rótulos, pois em nosso projeto a cor de fundo do BottomNavigation será a cor primária do aplicativo, diferente da aplicação padrão deste widget.

Segue /res/drawable/bottom_item_color.xml:

<?xml version="1.0" encoding="utf-8"?>
<selector
xmlns:android="http://schemas.android.com/apk/res/android">

<item
android:color="@android:color/white"
android:state_checked="true" />

<item
android:color="#99ffffff" /> <!-- Branco com opacidade abaixo de 100% -->
</selector>

 

Assim podemos partir para a atualização de activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"

xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white"
android:fitsSystemWindows="true">

<include
layout="@layout/app_bar_main"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_above="@+id/bottom_nav_view"
android:layout_alignParentTop="true" />

<android.support.design.widget.BottomNavigationView
android:id="@+id/bottom_nav_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:background="@color/colorPrimary"
app:itemIconTint="@drawable/bottom_item_color"
app:itemTextColor="@drawable/bottom_item_color"
app:menu="@menu/activity_main_bottom" />
</RelativeLayout>

 

O app_bar_main.xml referenciado no <include> permanecerá sem atualizações.

Com isso temos um novo diagrama para activity_main.xml:

Diagrama do menu activity_main.xml

Atualizando a atividade principal

Para a MainActivity vamos remover todo o código do DrawerLayout e do NavigationView e então trabalhar somente com o código do BottomNavigationView:

class MainActivity : AppCompatActivity(),
BottomNavigationView.OnNavigationItemSelectedListener {

var titulo: String = UefaFragment.TITULO

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main_bottom_navigation)
setSupportActionBar(toolbar)

bottom_nav_view.setOnNavigationItemSelectedListener( this )

titulo = savedInstanceState?.getString(FragmentAbstrato.TITULO) ?: titulo

/*
* Caso seja a primeira vez que o onCreate() esteja sendo
* invocado.
* */
if( supportFragmentManager.findFragmentByTag(FragmentAbstrato.KEY) == null ) {
trocaFragmento(UefaFragment())
}
}

override fun onNavigationItemSelected(item: MenuItem): Boolean {
val fragmento: Fragment

when(item.itemId) {
R.id.nav_libertadores -> {
fragmento = LibertadoresFragment()
titulo = LibertadoresFragment.TITULO
}
R.id.nav_afc -> {
fragmento = AfcFragment()
titulo = AfcFragment.TITULO
}
else -> {
fragmento = UefaFragment()
titulo = UefaFragment.TITULO
}
}

toolbar.title = titulo
trocaFragmento( fragmento )

return true
}

private fun trocaFragmento( fragment: Fragment ){
supportFragmentManager
.beginTransaction()
.setCustomAnimations(android.R.anim.fade_in, android.R.anim.fade_out)
.replace(R.id.rl_container, fragment, FragmentAbstrato.KEY)
.commit()
}

/*
* Hackcode para apresentar o título correto do fragmento
* atual.
* */
override fun onResume() {
super.onResume()
toolbar.title = titulo
}

/*
* Para que seja possível sempre apresentar o título correto
* em tela.
* */
override fun onSaveInstanceState(outState: Bundle?) {
outState?.putString( FragmentAbstrato.TITULO, titulo )
super.onSaveInstanceState( outState )
}
}

 

Note que em trocaFragmento() adicionamos uma animação nativa de fade in / fade out com setCustomAnimations( android.R.anim.fade_in, android.R.anim.fade_out ).

Vale ressaltar que a animação na troca de fragmentos é opcional. Se quiser saber mais sobre como construir um <set> de animação, entre no artigo a seguir: ObjectAnimator no Android, Aplicando Efeitos nos Componentes Visuais.

Note que caso em seu projeto fosse necessário manter o NavigationView, o método sobrescrito onNavigationItemSelected() seria ainda único na atividade, pois ele representaria a implementação necessária para ambas as Interfaces: BottomNavigationView.OnNavigationItemSelectedListenerNavigationView.OnNavigationItemSelectedListener.

Foi escolhido colocar todo o código da MainActivity, pois alguns métodos foram removidos, assim não fica a dúvida se eles estão ou não ainda presentes na atividade.

Fragmento de detalhes de time

Para respeitar a regra de negócio de navegação hierárquica do BottomNavigationView, vamos atualizar também o fluxo do projeto, ao invés de continuarmos com uma atividade de detalhes, vamos passar a utilizar um fragmento de detalhes onde o BottomNavigation permanecerá em tela.

No pacote /fragment crie um novo fragmento em branco, sem layout, com o rótulo DetalhesTimeFragment:

class DetalhesTimeFragment :
Fragment(),
View.OnClickListener {

override fun onCreateView(
inflater: LayoutInflater?,
container: ViewGroup?,
savedInstanceState: Bundle?): View? {

val view = inflater!!.inflate(R.layout.activity_detalhes_time, container, false)
return view
}

override fun onResume() {
super.onResume()

/*
* Código necessário para que seja possível a inclusão da
* seta de BackButton na barra de topo.
* */
toolbar.setNavigationIcon(R.drawable.ic_arrow_back_white_24dp);
toolbar.setNavigationOnClickListener( this )

val time = arguments.getParcelable<Time>( Time.KEY )

toolbar.title = time!!.nome
iv_time.setImageResource( time.idEscudo )
}

override fun onClick(view: View?) {
activity.onBackPressed()
}
}

 

No novo fragmento continuaremos com o mesmo layout de DetalhesTimeActivity, /res/layout/activity_detalhes_time.xml.

Atualizando o listener de clique da classe adaptadora

Agora para que o fragmento de detalhes seja invocado ao invés da atividade, devemos somente atualizar o código de onClick() em JogosAdapter.ViewHolder:

...
override fun onClick(view: View?) {
val bundle = Bundle()
bundle.putParcelable( Time.KEY, view!!.tag as Time )

val fragment = DetalhesTimeFragment()
fragment.arguments = bundle

(context as MainActivity)
.supportFragmentManager
.beginTransaction()
.add(R.id.rl_container, fragment, FragmentAbstrato.KEY)
.addToBackStack(null)
.commit()
}
...

 

O método addToBackStack(), quando o BottomNavigationView estiver em uso, deve ser utilizado somente quando a mudança de fragmento é em níveis diferentes de hierarquia, fora do contexto de acionamento direto de BottomActions.

Corrigindo problemas de design e de fluxo: Atividade vs Fragmento

Executando o projeto e acessando a área de detalhes de qualquer time, veja o que temos:

Telas de detalhes de time, com problema de design

Os problemas encontrados:

  1. StatusBar totalmente branca;
  2. Duas barras de topo: o Toolbar da MainActivity e o Toolbar do fragmento de detalhes;
  3. Conteúdo abaixo do BottomNavigation.

Para o primeiro problema apontado, a correção é simplesmente remover ou colocar como false o atributo android:fitsSystemWindows="true" do CoordinatorLayout do layout activity_detalhes_time.xml, isso, pois o layout da MainActivity é o layout host da nova arquitetura do visual de detalhes de clube do aplicativo e assim não há necessidade de um outro layout interno ter a mesma configuração de expansão em tela.

Para o segundo problema, uma possível solução é esconder a Toolbar da atividade principal quando o fragmento de detalhes for acionado. Para isso vamos iniciar criando um novo método em MainActivity que permita esconder ou apresentar o Toolbar dela:

class MainActivity : ... {
...

/*
* Com esse método será possível apresentar a Toolbar da
* MainActivity que o usuário estiver na lista de jogos de
* qualquer campeonato e esconder quando o usuário estiver na
* tela de detalhes, tela que tem o próprio Toolbar.
* */
fun apresentarToolbar(status: Boolean){
toolbar.visibility =
if( status )
View.VISIBLE
else
View.GONE
}
}

 

Agora temos de ajustar os locais onde este novo método será invocado. Para esconder o Toolbar da MainActivity vamos aproveitar o método onCreateView() de DetalhesTimeFragment.

Para apresentar novamente a barra de topo, vamos utilizar o método onDetach() também de DetalhesTimeFragment:

class DetalhesTimeFragment : ... {

override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?,
savedInstanceState: Bundle?): View? {

(activity as MainActivity).apresentarToolbar( false ) /* Escondendo */
return inflater!!.inflate(R.layout.activity_detalhes_time, container, false)
}
...

/*
* Melhor método, do ciclo de vida do fragmento de detalhes,
* para apresentar novamente a Toolbar da atividade principal
* isso, pois neste método todos os cenários são cobertos.
* */
override fun onDetach() {
(activity as MainActivity).apresentarToolbar( true ) /* Apresentando */
super.onDetach()
}
}

 

Ainda há um cenário onde a barra de topo da atividade principal não volta a ser apresentada: assim que o usuário sai do fragmento de detalhes de time direto para a lista de confrontos de algum outro torneio.

Isso ocorre, pois estamos trabalhando com pilha de fragmentos, addToBackStack(), e o fragmento de detalhe permanece em memória, não invocando o método onDetach().

Alias o trabalho com o método addToBackStack() também influencia negativamente na navegação do aplicativo. Partindo do cenário discutido nos parágrafos anteriores, haverá navegação entre os BottomActions quando houver novamente o acionamento do BackButton, algo que não deve de forma alguma acontecer com o BottomNavigationView em uso.

A solução para ambos os problemas é simplesmente remover a pilha de fragmentos quando acessando qualquer lista de jogos dos três torneios possíveis.

Na MainActivity, vamos atualizar o método trocaFragmento():

class MainActivity : ... {
...

private fun trocaFragmento( fragment: Fragment ){
/*
* Remove as pilhas de fragmentos para que não haja a
* possibilidade de navegação entre os itens do
* BottomNavigation, respeitando assim uma das regras
* de negócio deste componente.
* */
supportFragmentManager
.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)

supportFragmentManager
.beginTransaction()
.setCustomAnimations(android.R.anim.fade_in, android.R.anim.fade_out)
.replace(R.id.rl_container, fragment, FragmentAbstrato.KEY)
.commit()
}
...
}

 

Para o terceiro problema vamos apenas adicionar um paddingBottom ao layout root de conteúdo de DetalhesTimeFragment, um padding de 56dp que é a altura padrão do BottomNavigationView. Em /res/layout/content_detalhes_time.xml coloque o que está em destaque:

<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.NestedScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="56dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:showIn="@layout/activity_detalhes_time">
...

 

Executando novamente o projeto, temos:

Telas de detalhes de time, com problemas corrigidos

Testes e resultados

Executando o projeto e navegando pelas telas, temos:

Navegação pelo projeto Android Torneios Continentais

Com isso terminamos a apresentação do componente BottomNavigationView.

Como tarefa, implemente a Interface BottomNavigationView.OnNavigationItemReselectedListener e como código de onNavigationItemReselected() coloque um algoritmo que permita o re-carregamento de conteúdo de BottomAction na tela atual. Dica, este algoritmo já está presente na MainActivity.

Não deixe de se inscrever na 📩 lista de emails do Blog, logo acima ou ao lado, para receber em primeira mão os conteúdos exclusivos sobre o dev Android.

Se inscreva também no canal do Blog em: YouTube Vinícius Thiengo.

Slides

Abaixo os slides com a apresentação completa do widget BottomNavigationView:

Vídeo

Abaixo o vídeo com a implementação passo a passo do BottomNavigationView no aplicativo de torneios continentais:

Para acesso ao projeto completo, entre no GitHub a seguir: https://github.com/viniciusthiengo/torneios-continentais.

Conclusão

Com o BottomNavigationView, mesmo sabendo da não restrição do trabalho junto ao menu gaveta, podemos assumir que ele veio também para substituir, em alguns casos (de três a cinco itens), tanto o DrawerNavigation como também o uso de tabs de topo.

Respeitando ao menos as regras de cores, ícones e rótulos, já é o suficiente para termos um bom uso do widget discutido aqui.

Caso precise de alguma funcionalidade ainda não atendida pela implementação nativa do componente, não deixe de pesquisar por APIs de terceiros que permitam o uso mais customizado do BottomNavigation, que até o momento da construção deste artigo, a versão nativa, não permitia de forma trivial o scroll de acordo com o conteúdo em tela.

Fique atento quanto ao uso do BottomNavigationView, pois em alguns conteúdos de estudo para a construção do artigo e vídeo foram encontrados casos onde um ou outro item deste componente ficava desativado devido a algum passo que o usuário ainda não havia percorrido no aplicativo.

Uma das regras de negócio do BottomNavigationView é o acesso imediato ao conteúdo do BottomAction acionado, sem pré-requisitos.

Não esqueça de se inscrever na 📩 lista de emails e de deixar o seu comentário sobre o que achou.

Abraço.

Fontes

Material Design Guideline - BottomNavigation

Documentação oficial do Android: BottomNavigationView

Exploring the Android Design Support Library: Bottom Navigation View

Stack Overflow: Hide/Show bottomNavigationView on Scroll - Resposta de Abhishek Singh

Stack Overflow: Selected tab's color in Bottom Navigation View - Resposta de Kamran Ahmed

Stack Overflow: How to apply a fade-in/fade-out animation when replacing a fragment - Resposta de Rafique Mohammed

Investir em Você é Barra de Ouro a R$ 2,00. Cadastre-se e receba grátis conteúdos Android sem precedentes!
Email inválido

Relacionado

Material DesignMaterial DesignAndroid
Android Studio: Instalação, Configuração e OtimizaçãoAndroid Studio: Instalação, Configuração e OtimizaçãoAndroid
ViewModel Android, Como Utilizar Este Componente de ArquiteturaViewModel Android, Como Utilizar Este Componente de ArquiteturaAndroid
Android: Avaliação do Pré-projeto Agenda CulturalAndroid: Avaliação do Pré-projeto Agenda CulturalAndroid

Compartilhar

Comentários Facebook

Comentários Blog (2)

Para código / script, coloque entre [code] e [/code] para receber marcação especifica.
Forneça seu nome válido.
Forneça seu email válido.
Forneça o comentário.
Enviando, aguarde...
Robson Lira (0) (0)
18/11/2019
Olá Boa noite, Thiengo estou com uma duvida, eu iniciei um projeto apos atualizar a IDE AE 3.5.2 e criando um projeto com Drawer note que algo diferente, com componente de Navegação que deve ser semelhante ao deste artigo só que eu estou usando como disse o Drawer, corri para saber mais a respeito porém sem sucesso, não saio do lugar. Por Exemplo eu inseri um item para fazer o logout onde simplesmente eu não vou navegar apenas chamar um método na atividade principal mais nem sei o que devo fazer antes era simplesmente implementar um OnNavigationItemSelectedListener() hoje o projeto criado contém um
DrawerLayout drawer = findViewById(R.id.drawer_layout);
        NavigationView navigationView = findViewById(R.id.nav_view);
        // Passing each menu ID as a set of Ids because each
        // menu should be considered as top level destinations.
        mAppBarConfiguration = new AppBarConfiguration.Builder(
                R.id.nav_home, R.id.nav_gallery, R.id.nav_slideshow,
                R.id.nav_tools, R.id.nav_share, R.id.nav_send, R.id.nav_logout)
                .setDrawerLayout(drawer)
                .build();

        NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment);
        NavigationUI.setupActionBarWithNavController(this, navController, mAppBarConfiguration);
        NavigationUI.setupWithNavController(navigationView, navController);
Será que podes me ajudar? eu li este conteúdo aquihttps://codelabs.developers.google.com/codelabs/android-navigation/#8 mais não surtiu muito efeito
Responder
Vinícius Thiengo (0) (0)
30/12/2019
Robson, tudo bem?

Ainda não utilizo o NavHost, o NavController e nem o NavigationUI para componentes de menu gaveta ou barra de fundo de navegação.

Estou ainda estudando estes "camaradas".

Sendo assim, minha melhor resposta agora para ti é que destrinche o conteúdo do link a seguir: https://developer.android.com/guide/navigation/navigation-ui

Ele está em português e vai lhe guiar sobre como utilizar essas classes no Android.

É isso.

Surgindo mais dúvidas, pode perguntar.

Bons estudos.

Abraço.
Responder