Início de Projeto e Menu Gaveta Customizado - Android M-Commerce

Receba em primeira mão, e com prioridade, os conteúdos Android exclusivos do Blog. Você receberá um email de confirmação. Somente depois de confirma-lo é que poderei lhe enviar os conteúdos exclusivos.

Email inválido.
Blog /Android /Início de Projeto e Menu Gaveta Customizado - Android M-Commerce

Início de Projeto e Menu Gaveta Customizado - Android M-Commerce

Vinícius Thiengo
(2806) (25)
Go-ahead
"Você não deve se concentrar em por que você não pode fazer algo, que é o que a maioria das pessoas fazem. Você deve se concentrar em porquê, talvez, você pode, e ser uma das exceções."
Steve Case
Kotlin Android
Capa do livro Desenvolvedor Kotlin Android - Bibliotecas para o dia a dia
TítuloDesenvolvedor Kotlin Android - Bibliotecas para o dia a dia
CategoriasAndroid, Kotlin
AutorVinícius Thiengo
Edição
Capítulos19
Páginas1035
Acessar Livro
Treinamento Oficial
Android: Prototipagem Profissional de Aplicativos
CursoAndroid: Prototipagem Profissional de Aplicativos
CategoriaAndroid
InstrutorVinícius Thiengo
NívelTodos os níveis
Vídeo aulas186
PlataformaUdemy
Acessar Curso
Receitas Android
Capa do livro Receitas Para Desenvolvedores Android
TítuloReceitas Para Desenvolvedores Android
CategoriaDesenvolvimento Android
AutorVinícius Thiengo
Edição
Ano2017
Capítulos20
Páginas936
Acessar Livro
Código Limpo
Capa do livro Refatorando Para Programas Limpos
TítuloRefatorando Para Programas Limpos
CategoriaEngenharia de Software
AutorVinícius Thiengo
Edição
Capítulos46
Páginas599
Acessar Livro
Quer aprender a programar para Android? Acesse abaixo o curso gratuito no Blog.
Conteúdo Exclusivo
Receba em primeira mão, e com prioridade, os conteúdos Android exclusivos do Blog.
Email inválido

Tudo bem?

Neste artigo vamos dar continuidade ao desenvolvimento do projeto Android de mobile-commerce, mais precisamente, ao aplicativo BlueShoes apresentado em Android Mobile-Commerce, Apresentação e Protótipo do Projeto.

Aqui iniciaremos com a codificação, sabendo que os protótipos e regras de negócio discutidos no artigo do link acima já nos dão o suficiente para seguir com o desenvolvimento.

Animação menu gaveta m-commerce Android

Antes de prosseguir, não deixe de se inscrever 📩 na lista de emails do Blog para ter acesso aos conteúdos exclusivos sobre desenvolvimento Android e também aos conteúdos deste projeto de desenvolvimento.

A seguir os pontos abordados:

Por onde iniciaremos a codificação?

Como informado no primeiro artigo do projeto: primeiro desenvolveremos a parte de interface gráfica do aplicativo Android.

Porém é improvável que seja desenvolvida toda a parte gráfica em um único artigo. Sendo assim, vamos iniciar pelas partes essenciais e então vamos às partes simples para depois irmos aos trechos que exigirão maior codificação e empenho.

Neste artigo, vamos:

  • Iniciar um novo projeto Android;
  • Colocar os recursos necessários para início de projeto;
  • Desenvolver toda a codificação da interface dos dois possíveis menus gaveta:
    • Versão com usuário não conectado;
    • Versão com usuários conectado.
  • Remover o que não utilizaremos.

Por que o menu gaveta?

Porque este componente é a parte central do projeto, é a partir dele que poderemos navegar para as telas simples e telas complexas.

Para ficar mais claro o que será desenvolvido neste artigo, a seguir o protótipo das telas:

Abertura

Abertura

Menu - usuário não conectado

Menu - usuário não conectado

Menu - usuário conectado

Menu - usuário conectado

 

 

Note que nas telas de menu gaveta aberto somente trabalharemos o menu gaveta, a listagem de tênis ficará para outro artigo.

Iniciando um novo projeto Android

Em seu Android Studio inicie um novo projeto Kotlin:

  • Atividade inicial: Navigation Drawer Activity;

Android Studio - Escolha seu projeto

  • Nome da aplicação: BlueShoes;
  • Linguagem: Kotlin;
  • API mínima: 16 (Android Jelly Bean);
  • Para todos os outros campos, mantenha os valores já definidos por padrão.

Android Studio - Configuração de projeto

Com o fim das configurações de um novo projeto, teremos a seguinte arquitetura no Android Studio IDE:

Arquitetura física do projeto Android BlueShoes

Saiba que você pode acompanhar o projeto também pelo repositório dele no GitHub: https://github.com/viniciusthiengo/blueshoes-kotlin-android.

Arquivos de configuração (Gradle)

Para o Gradle Level de Projeto, build.gradle (Project: BlueShoes), temos:

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

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

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

 

Para o Gradle Level de Aplicativo, build.gradle (Module: app), temos:

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

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

dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"

implementation 'com.android.support:appcompat-v7:28.0.0'
implementation 'com.android.support:design:28.0.0'
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
}

 

Note que as referências às APIs de testes foram todas removidas, pois não serão necessárias neste projeto.

Configurações AndroidManifest

A seguir as configurações iniciais do AndroidManifest.xml:

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

<application
android:allowBackup="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=".view.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>
</application>
</manifest>

Configurações de estilo

A seguir o arquivo de definição de cores, /res/values/colors.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#F9F9F9</color>
<color name="colorPrimaryDark">#C6C6C6</color>
<color name="colorAccent">#F5F5F5</color>

<color name="colorText">#666666</color>
<color name="colorLink">#00A6FF</color>
<color name="colorNavButton">#77D353</color>
<color name="colorViewLine">#DDDDDD</color>

<color name="colorNavItemSelected">#F1F1F1</color>
</resources>

 

Note que algumas configurações estáticas de projeto, configurações em XML, nós já estaremos definindo aqui, isso, pois é possível obtê-las de acordo com o que foi definido em protótipo estático. Para a escolha de cores foi utilizado o site oficial do Material Design IO.

Agora o arquivo de dimensões, /res/values/dimens.xml:

<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>
</resources>

 

Assim o arquivo de Strings, /res/values/strings.xml:

<resources>
<string name="app_name">BlueShoes</string>

<string name="navigation_drawer_open">Menu gaveta aberto</string>
<string name="navigation_drawer_close">Menu gaveta fechado</string>

<string name="nav_header_title">Android Studio</string>
<string name="nav_header_subtitle">android.studio@android.com</string>
<string name="nav_header_desc">Navigation header</string>

<string name="action_settings">Settings</string>

<string name="tx_login">Login</string>
<string name="tx_or">ou</string>
<string name="tx_me_sign_up">me Cadastrar</string>
<string name="tx_conected_as">Conectado como:</string>

<string name="item_all_shoes">Todos os calçados</string>
<string name="item_flip_flops">Chinelos</string>
<string name="item_cleats">Chuteiras</string>
<string name="item_sandals">Sandálias</string>
<string name="item_ballet_shoes">Sapatilhas</string>
<string name="item_suit_shoes">Sapatênis</string>
<string name="item_shoes">Tênis</string>
<string name="item_performance_shoes">Tênis performance</string>
<string name="item_contact">Contato BlueShoes</string>
<string name="item_about">Sobre</string>
<string name="item_privacy_policy">Políticas de privacidade</string>
<string name="item_my_orders">Meus pedidos</string>
<string name="item_settings">Minhas configurações</string>
<string name="item_sign_out">Sair</string>
</resources>

 

Agora o arquivo com algumas configurações de tema para o aplicativo quando em aparelhos Android com a API 21 ou superior, segue /res/values-v21/styles.xml:

<resources>

<!--
Para que a barra de topo padrão não seja utilizada e
assim somente o AppBarLayout junto ao Toolbar possam
ser usados. Somando a isso a aplicação de transparência
na statusBar.
-->
<style name="AppTheme.NoActionBar">

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

 

E por fim o arquivo de definição geral de tema do aplicativo, /res/values/styles.xml:

<resources>

<!--
Estilo padrão, aplicado em todo o projeto.
-->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">

<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>

<!--
Para que a barra de topo padrão não seja utilizada e
assim somente o AppBarLayout junto ao Toolbar possam ser
usados.
-->
<style name="AppTheme.NoActionBar">

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

<!--
Para o correto enquadramento do AppBarLayout.
-->
<style
name="AppTheme.AppBarOverlay"
parent="ThemeOverlay.AppCompat.ActionBar"/>

<!--
Utilizado para a correta apresentação de menus de pop-up
em barra de topo.
-->
<style
name="AppTheme.PopupOverlay"
parent="ThemeOverlay.AppCompat.Light"/>
</resources>

 

Note que em AppTheme.AppBarOverlay atualizamos o tema parent para ThemeOverlay.AppCompat.ActionBar ao invés do padrão ThemeOverlay.AppCompat.Dark.ActionBar que deixava os itens da barra de topo sem constraste com a cor de fundo dela.

Atividade principal

É importante mostrarmos também a codificação da única atividade do novo projeto com um menu gaveta, isso, pois ainda neste artigo, tanto o código dinâmico da atividade, código Kotlin, quanto o código estático, XML, passarão por atualizações.

Vamos iniciar com os códigos estáticos, códigos de layout. Primeiro o layout de conteúdo, /res/layout/content_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:showIn="@layout/app_bar_main"
tools:context=".MainActivity">

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
</android.support.constraint.ConstraintLayout>

 

A seguir o diagrama do layout anterior:

Diagrama do layout content_main.xml

Agora o layout que contém o layout de conteúdo, /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"
tools:context=".MainActivity">

<android.support.design.widget.AppBarLayout
android:layout_height="wrap_content"
android:layout_width="match_parent"
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>

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

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

 

A seguir o diagrama do layout anterior:

Diagrama do layout app_bar_main.xml

Agora o layout de cabeçalho padrão do menu gaveta, /res/layout/nav_header.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
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="@dimen/nav_header_height"
android:background="@drawable/side_nav_bar"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:theme="@style/ThemeOverlay.AppCompat.Dark"
android:orientation="vertical"
android:gravity="bottom">

<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="@dimen/nav_header_vertical_spacing"
app:srcCompat="@mipmap/ic_launcher_round"
android:contentDescription="@string/nav_header_desc"
android:id="@+id/imageView"/>

<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/nav_header_vertical_spacing"
android:text="@string/nav_header_title"
android:textAppearance="@style/TextAppearance.AppCompat.Body1"/>

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/nav_header_subtitle"
android:id="@+id/textView"/>
</LinearLayout>

 

Abaixo o diagrama do layout anterior de cabeçalho:

Diagrama do layout nav_header.xml

Assim o XML de itens de menu, /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_camera"
android:icon="@drawable/ic_menu_camera"
android:title="Import"/>
<item
android:id="@+id/nav_gallery"
android:icon="@drawable/ic_menu_gallery"
android:title="Gallery"/>
<item
android:id="@+id/nav_slideshow"
android:icon="@drawable/ic_menu_slideshow"
android:title="Slideshow"/>
<item
android:id="@+id/nav_manage"
android:icon="@drawable/ic_menu_manage"
android:title="Tools"/>
</group>

<item android:title="Communicate">
<menu>
<item
android:id="@+id/nav_share"
android:icon="@drawable/ic_menu_share"
android:title="Share"/>
<item
android:id="@+id/nav_send"
android:icon="@drawable/ic_menu_send"
android:title="Send"/>
</menu>
</item>
</menu>

 

A seguir o diagrama do menu anterior:

Diagrama do menu activity_main_drawer.xml

Então o layout principal da atividade, /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:fitsSystemWindows="true"
app:headerLayout="@layout/nav_header_main"
app:menu="@menu/activity_main_drawer"/>
</android.support.v4.widget.DrawerLayout>

 

Abaixo o diagrama do principal layout da MainActivity:

Diagrama do layout activity_main.xml

Antes de partirmos para a codificação dinâmica da atividade principal, vamos ao simples XML de menu de topo, /res/menu/main.xml:

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

<item android:id="@+id/action_settings"
android:title="@string/action_settings"
android:orderInCategory="100"
app:showAsAction="never"/>
</menu>

 

Abaixo o diagrama do layout anterior:

Diagrama do menu main.xml

Assim o código dinâmico inicial da MainActivity:

class MainActivity :
AppCompatActivity(),
NavigationView.OnNavigationItemSelectedListener {

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

fab.setOnClickListener { view ->
Snackbar
.make(
view,
"Mude para a sua própria ação.",
Snackbar.LENGTH_LONG
)
.setAction(
"Ação",
null
)
.show()
}

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 onBackPressed() {
if( drawer_layout.isDrawerOpen( GravityCompat.START ) ){
drawer_layout.closeDrawer( GravityCompat.START )
}
else{
super.onBackPressed()
}
}

override fun onCreateOptionsMenu( menu: Menu ): Boolean {
/*
* Infla o menu. Adiciona itens a barra de topo, se
* ela estiver presente.
* */
menuInflater.inflate( R.menu.main, menu )
return true
}

override fun onOptionsItemSelected( item: MenuItem ): Boolean {
/*
* Lidar com cliques de itens da barra de ação aqui.
* A barra de ação manipulará automaticamente os
* cliques no botão Home / Up, desde que seja
* especificada uma atividade pai em AndroidManifest.xml.
* */
when( item.itemId ){
R.id.action_settings -> return true
else -> return super.onOptionsItemSelected( item )
}
}

override fun onNavigationItemSelected( item: MenuItem ): Boolean {
/*
* Lida com cliques nos itens do menu gaveta.
* */
when( item.itemId ){
R.id.nav_camera -> { }
R.id.nav_gallery -> { }
R.id.nav_slideshow -> { }
R.id.nav_manage -> { }
R.id.nav_share -> { }
R.id.nav_send -> { }
}

drawer_layout.closeDrawer( GravityCompat.START )
return true
}
}

 

Muitos dos códigos anteriores, incluindo os XMLs, serão removidos ou atualizados.

Aqui somente foram apresentados os recursos que serão trabalhados, os muito outros recursos em /res serão removidos do projeto.

Imagem de abertura de aplicativo

Para o primeiro recurso apresentado assim que o aplicativo é acionado pelo usuário, a imagem de background de abertura, vamos utilizar o recurso a seguir:

Imagem de background de abertura do app Android BlueShoes

A imagem acima foi retirada do protótipo estático do projeto, resumidamente:

  • Um printscreen foi realizado;
  • E depois foram removidas as bordas que não pertencem a imagem requerida.

Os tamanhos esperados para a imagem de abertura são:

  • drawable-mdpi: 320x480 pixels;
  • drawable-hdpi: 480x800 pixels;
  • drawable-xhdpi: 640x960 pixels;
  • drawable-xxhdpi: 960x1600 pixels.

Você notará que será necessário dois ajustes de dimensões de imagem:

  • 960x1600 que permite a fácil diminuição de dimensões para 480x800;
  • 640x960 que permite a fácil diminuição de dimensões para 320x480.

Qualquer software popular de edição imagens deixará você realizar esses ajustes com facilidade. Existem até mesmo ferramentas online e gratuitas que permitem isso.

Apesar de para este projeto ter sido utilizado os programas Adobe Photoshop e Adobe Fireworks é possível fazer tudo com o GIMP. No curso de prototipagem Android do Blog é apresentado como também utilizar o GIMP para edição de imagens.

Folders drawable

Você notará que no projeto há também os diretórios, folders, de recursos: /drawable; /drawable-v21; e /drawable-v24.

Folder /res do projeto Android BlueShoes

Porém não há os aguardados drawable dpi. Vamos adiciona-los como a seguir:

  • Com o botão direito do mouse clique em /res;
  • Logo depois acesse New;
  • Então clique em Directory;
  • Coloque como rótulo o nome drawable-mdpi e clique em Ok.

Criando um novo folder drawable

O procedimento anterior, repita ele para os folders: /drawable-hdpi; /drawable-xhdpi; e /drawable-xxhdpi.

E os folders mipmap, não podemos utiliza-los?

Na verdade podemos, mas esses folders são para o ícone de abertura do aplicativo, aquele que fica na área de aplicativos do sistema Android.

Voltando aos folders drawable... ao final da criação deles teremos em /res:

Novo folder /res do projeto Android BlueShoes

Você deve ter notado que não criamos o folder /drawable-ldpi e /drawable-xxxhdpi, certo? Isso, pois segundo informações em comunidade e na própria documentação oficial: não há mais suporte para ldpi em aparelhos smartphones e tablets; e o xxxhdpi é somente para o ícone de abertura de aplicativo, o que fica em folder mipmap.

Com isso podemos partir para o download das imagens de abertura, caso você já não as tenha feito.

Download das imagens

Para adiantar o processo, você pode realizar o download da imagem de abertura em suas diferentes versões, isso nos links a seguir:

Por fim, coloque as imagens em seus respectivos folders de projeto.

Definindo a imagem como background de abertura

Para definir a imagem escolhida como background de abertura de aplicativo ainda é necessária uma atualização no arquivo de tema. Em /res/values/styles.xml coloque o código em destaque:

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

<!--
Definição da imagem que será carregada como imagem de
background em abertura de aplicativo.
-->
<item name="android:windowBackground">@drawable/background</item>

...
</style>
...

Imagem de fundo das atividades

A imagem de background das atividades de projeto é muito similar a imagem de background de abertura de aplicativo, somente não tem o rótulo central "BlueShoes". Veja-a seguir:

Imagem de background de atividade no app Android BlueShoes

Para a criação dela o procedimento foi similar ao da imagem de background de abertura de app. Essa também foi retirada do protótipo estático do projeto:

  • Primeiro o printscreen na imagem sem o rótulo central;
  • Depois os cortes de bordas que não são parte da imagem.

Novamente, os tamanhos aguardados como imagem de background de atividades são:

  • drawable-mdpi: 320x480 pixels;
  • drawable-hdpi: 480x800 pixels;
  • drawable-xhdpi: 640x960 pixels;
  • drawable-xxhdpi: 960x1600 pixels.

Porém dessa vez também temos de fornecer as imagens de background em orientação landscape, horizontal. Para isso teremos como base a imagem a seguir:

Imagem de background (em landscape) de atividade no app Android BlueShoes

E os tamanhos esperados para ela são:

  • drawable-land-mdpi: 480x320 pixels;
  • drawable-land-hdpi: 800x480 pixels;
  • drawable-land-xhdpi: 960x640 pixels;
  • drawable-land-xxhdpi: 1600x960 pixels.

Para criar as imagens você pode utilizar qualquer software de edição de imagens que permita corte e redimensionamento. Na sessão Imagem de abertura de aplicativo foram indicados alguns softwares.

Folders drawable para landscape

Já temos os diretórios para as imagens de background em versão portrait. Agora precisamos dos folders para imagens em landscape.

A seguir o passo a passo para a criação destes folders:

  • Com o botão direito do mouse clique em /res;
  • Logo depois acesse New;
  • Então clique em Directory;
  • Coloque como rótulo o nome drawable-land-mdpi e clique em Ok.

Novo folder drawable no app Android BlueShoes

Repita o procedimento anterior para os folders: /drawable-land-hdpi/drawable-land-xhdpi; e /drawable-land-xxhdpi.

Ao final da criação teremos:

Novo folder /res do app Android BlueShoes

Com isso podemos partir para o download das imagens de background de atividade.

Download das imagens

Para adiantar o desenvolvimento, realize o download da imagem de background de atividades em suas diferentes versões, nos links a seguir:

Coloque as imagens em seus respectivos folders de projeto.

Definindo a imagem como background de atividade

No layout /res/layout/app_bar_main.xml, carregado na única atividade do projeto, coloque a seguinte nova configuração de background no ViewGroup raiz:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
...
android:background="@drawable/bg_activity">
...

Ícone de abertura de aplicativo

A imagem do ícone de abertura do aplicativo é derivada da imagem de background de abertura de app. Veja-a a seguir:

Ícone do app Android BlueShoes

A criação dela foi simples como no caso das outras imagens já discutidas até aqui:

  • Primeiro o printscreen na imagem de background com rótulo central;
  • Depois o corte dela para mante-la quadrada.

Para ícone de lançamento de aplicativo os tamanhos aguardados são:

  • mipmap-mdpi: 48x48 pixels;
  • mipmap-hdpi: 72x72 pixels;
  • mipmap-xhdpi: 96x96 pixels;
  • mipmap-xxhdpi: 144x144 pixels;
  • mipmap-xxxhdpi: 192x192 pixels.

Desta vez, para a criação do recurso, vamos utilizar uma ferramenta online gratuita que permite a rápida geração de ícones para projetos Android.

Android Asset Studio

Primeiro, certifique-se de que o ícone de abertura de aplicativo tem ao menos as dimensões 192x192. Logo depois siga o roteiro abaixo:

  • Acesse o site Android Asset Studio;
  • Clique em Launcher icon generator;
  • Em seguida clique em Image e carregue a imagem quadrada criada para ser o ícone de abertura do app;
  • Coloque o Padding como 0%;
  • Certifique-se de que em Name está ic_launcher (nome comum para ícones de abertura de app);
  • Por fim clique no botão azul, no topo a direita, Download ZIP.

Gerando ícone em Android Asset Studio

Com o ZIP descarregado, descompacte o arquivo e então coloque no projeto os ícones presentes nos folders /mipmap. Basta copiar os folders /mipmap e então cola-los dentro do folder /res:

  • Clique em Ok para Copy;

Copiando imagens em folder do Android Studio

  • Então clique em Overwrite for all.

Sobrescrevendo todas as imagens antigas em cópia

Ainda é preciso criar a versão circular do ícone de abertura de app, para isso repita todo o processo de carregamento de imagem no Android Asset Studio, porém com as seguintes alterações:

  • Em Shape selecione Circle;
  • Em Name coloque ic_launcher_round.

Descarregue o ZIP da versão circular do ícone de abertura. Descompacte ele e então copie os folders /mipmap e os coloque em projeto, como feito anteriormente.

Ao final do carregamento das versões do ícone de abertura teremos o seguinte nos principais folders /mipmap:

Folders mipmap no app Android BlueShoes

Download das imagens

A seguir os links para download do ícone de abertura de aplicativo em suas diferentes versões. Primeiro os links do ícone em versão retangular:

Agora os links do ícone em versão circular:

Coloque as imagens em seus respectivos diretórios de projeto.

Definição do menu gaveta

Vamos agora as atualizações, principalmente, em código dinâmico. Atualizações para a criação dos dois possíveis menus gaveta.

Entendendo o funcionamento

Primeiro, o que temos até o momento como layout da atividade principal, /res/layout/activity_main.xml, é o seguinte:

<?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:fitsSystemWindows="true"
app:headerLayout="@layout/nav_header_main"
app:menu="@menu/activity_main_drawer"/>

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

 

No código acima é importante entender o funcionamento do DrawerLayout. Primeiro que este componente de layout trabalha de maneira similar ao FrameLayout, sendo assim, as primeiras Views ficam abaixo das Views posteriores no eixo Z.

Mas o importante é saber que o primeiro componente visual filho será tratado como o componente que armazenará o conteúdo principal em tela. No código anterior o layout incluído, app_bar_main.xml, é responsável pelo conteúdo principal da atividade.

O segundo componente visual filho é responsável por conter o conteúdo que entrará na tela no formato "menu gaveta". No código anterior a View responsável pelo conteúdo gaveta é a NavigationView.

Estrutura Android DrawerLayout

Muitos desenvolvedores que estão iniciando no Android acreditam que ou o NavigationView, ou o ListView, ou o RecyclerView, que estes têm de ser utilizados como gaveta, quando na verdade qualquer View pode ser utilizada.

O que é esperado ser respeitado é o uso de apenas dois filhos diretos, que no conteúdo anterior são: o layout app_bar_main.xml (CoordinatorLayout é o ViewGroup raiz) e o NavigationView (que adiciona outros dois componentes - cabeçalho e menu).

Resumo: em ambos os lados do DrawerLayout podemos colocar o que quisermos. Veja o menu gaveta do aplicativo Discord:

Menu gaveta app Android Discord

Entendendo isso, podemos partir para o desenvolvimento do menu personalizado do projeto BlueShoes.

Cabeçalho de menu para usuário não conectado

Vamos primeiro desenvolver a parte mais simples, o cabeçalho de menu gaveta para usuário não conectado.

Esse cabeçalho tem uma imagem de background, imagem utilizada no protótipo estático do projeto:

Background do cabeçalho de menu gaveta

A construção desta imagem foi similar ao das imagens de background apresentadas em tópicos anteriores:

  • Primeiro foi realizado o printscreen;
  • Depois o corte das bordas utilizando algum software de edição de imagem.

É esperado que o menu gaveta, segundo a documentação oficial, tenha no máximo uma largura de 320dp. Sendo assim nossa imagem de background tem de ter no máximo essa largura, digo essa largura de acordo com o folder /drawable, pois ela terá quatro versões: mdpi; hdpi; xhdpi; e xxhdpi.

Para saber quais seriam estes tamanhos, você poderia utilizar o site Pixplicity | DP/PX converter e então informar o valor em teste, em nosso caso é 320dp:

Medições DPI no site Pixplicity | DP/PX converter

Sendo assim nossa imagem de background de cabeçalho terá de ter as larguras:

  • drawable-mdpi: 320 pixels;
  • drawable-hdpi: 480 pixels;
  • drawable-xhdpi: 640 pixels;
  • drawable-xxhdpi: 960 pixels.

Todo esse trabalho já foi feito para ti. Também temos como objetivo aqui mostrar ferramentas que você poderá utilizar em seu dia a dia como desenvolvedor. A seguir os links para download das diferentes versões da imagem de cabeçalho:

Agora, em /res/layout, crie o arquivo nav_header_user_not_logged.xml como a seguir:

  • Clique com o botão direito do mouse em /res/layout;
  • Acesse New;
  • Clique em Layout resource file;
  • Coloque como nome do arquivo nav_header_user_not_logged.xml e clique em OK.

Criando um novo layout de cabeçalho

Como conteúdo deste novo layout coloque:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/rl_header_user_not_logged"
android:background="@drawable/nav_header"
android:paddingRight="40dp"
android:paddingLeft="40dp"
android:paddingTop="30dp"
android:paddingBottom="30dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">

<Button
android:id="@+id/bt_login"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingLeft="30dp"
android:paddingRight="30dp"
android:layout_alignParentTop="true"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:background="@drawable/bt_nav_header_login_bg"
android:textColor="@android:color/white"
android:textAllCaps="false"
android:text="@string/tx_login"/>

<TextView
android:id="@+id/tv_or"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="@+id/bt_login"
android:layout_toEndOf="@+id/bt_login"
android:layout_centerVertical="true"
android:layout_marginLeft="12dp"
android:layout_marginRight="12dp"
android:textColor="@color/colorText"
android:text="@string/tx_or"/>

<TextView
android:id="@+id/tv_sign_up"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="@+id/tv_or"
android:layout_toEndOf="@+id/tv_or"
android:layout_centerVertical="true"
android:textColor="@color/colorLink"
android:text="@string/tx_me_sign_up"/>
</RelativeLayout>

 

Note que em <Button> temos um background personalizado, /res/drawable/bt_nav_header_login_bg.xml. Esse é o background que além de definir a cor e bordas arredondadas defini também o Ripple Effect para aparelhos com o Android API 21, Lollipop, ou superior.

A API de suporte do Android aplica o efeito ripple a partir da API 21, então primeiro temos de criar um arquivo bt_nav_header_login_bg.xml geral, que funcione sem definição específica de versão de API Android.

Logo, no folder /res/drawable crie um novo XML como a seguir:

  • Clique com o botão direito do mouse em /res/drawable;
  • Acesse New;
  • Clique em Drawable resource file;
  • Em File name coloque bt_nav_header_login_bg.xml;
  • Em Root element coloque shape;
  • Clique em OK.

Criando um novo recurso drawable

Como conteúdo deste novo arquivo coloque:

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

<!--
Definição da cor de background do shape
retangular.
-->
<solid android:color="@color/colorNavButton" />

<!--
Definição da curvatura das pontas do shape
retangular.
-->
<corners android:radius="3dp"/>
</shape>

 

Agora a versão de background de botão com o efeito ripple ativado. Em /res crie o folder /drawable-v21. Este novo folder conterá recursos que serão utilizados pelo sistema Android somente quando em API 21 ou superior.

Agora, dentro do novo folder /res/drawable-v21, crie um arquivo XML como feito anteriormente, com o mesmo nome e configuração do último arquivo XML criado.

Dentro do novo arquivo /res/drawable-v21/bt_nav_header_login_bg.xml coloque o seguinte código XML:

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

<item>
<shape android:shape="rectangle">
<solid android:color="@color/colorNavButton" />
<corners android:radius="3dp"/>
</shape>
</item>
</ripple>

 

Note que as configurações de shape são as mesmas, porém estão "envelopadas" pela API de efeito ripple.

Abaixo o diagrama do layout nav_header_user_not_logged.xml:

Diagrama do layout nav_header_user_not_logged.xml

Com isso nosso cabeçalho de usuário não conectado está pronto.

Cabeçalho de menu para usuário conectado

Para o cabeçalho de menu gaveta quando o usuário estiver conectado teremos somente informações de qual usuário está conectado, não haverá ação (botão ou link) nesta parte do menu.

Essas informações são importantes para que o usuário saiba qual conta está conectada ao aplicativo.

Antes de prosseguir com o código do layout, primeiro vamos adicionar uma API que permitirá, em poucas linhas de código, colocarmos um ImageView redondo no layout.

No Gradle Level de Projeto, build.gradle (Project: BlueShoes), adicione a referência em destaque a seguir:

...
allprojects {
repositories {
google()
jcenter()
mavenCentral() /* Para acesso a API RoundedImageView */
}
}
...

 

Agora no Gradle Level de Aplicativo, build.gradle (Module: app), adicione a referência em destaque, referência a API RoundedImageView:

...
dependencies {
...

/* RoundedImageView API */
implementation 'com.makeramen:roundedimageview:2.3.0'
}

 

Por fim sincronize o projeto.

Ainda temos de adicionar uma nova fonte ao projeto, fonte que será utilizada no rótulo "Conectado como":

Cabeçalho de menu gaveta com usuário conectado

Aqui o fonte será ainda Roboto, porém a versão light desta fonte. Foi utilizado o Google Fonts para baixar a fonte, mas se você quiser saber passo a passo como prosseguir com o trabalho com fontes personalizadas no Android, então acesse os artigos a seguir:

Agora no folder /res crie um novo folder, /font. E então coloque a fonte roboto_light.ttf dentro deste folder. Você pode baixar esta fonte diretamente do GitHub do projeto, em:

Assim, como feito no tópico anterior, em /res/layout crie um novo layout XML, desta vez com o nome nav_header_user_logged.xml e com a seguinte codificação 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:id="@+id/rl_header_user_logged"
android:background="@drawable/nav_header"
android:paddingRight="40dp"
android:paddingLeft="40dp"
android:paddingTop="30dp"
android:paddingBottom="30dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">

<com.makeramen.roundedimageview.RoundedImageView
android:id="@+id/iv_user"
android:layout_width="54dp"
android:layout_height="54dp"
android:layout_alignParentTop="true"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_marginRight="12dp"
android:layout_marginEnd="12dp"
android:scaleType="centerCrop"
app:riv_border_color="@android:color/transparent"
app:riv_border_width="0dp"
app:riv_corner_radius="54dp"
app:riv_mutate_background="false"
app:riv_oval="true"
app:riv_tile_mode="clamp"/>

<TextView
android:id="@+id/tv_connected_as"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@+id/iv_user"
android:layout_toRightOf="@+id/iv_user"
android:layout_toEndOf="@+id/iv_user"
android:layout_marginTop="8dp"
android:textSize="12sp"
android:textColor="@color/colorText"
android:fontFamily="@font/roboto_light"
android:text="@string/tx_conected_as"/>

<TextView
android:id="@+id/tv_user"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="@+id/iv_user"
android:layout_toEndOf="@+id/iv_user"
android:layout_below="@+id/tv_connected_as"
android:layout_marginTop="-4dp"
android:maxLines="1"
android:ellipsize="end"
android:textSize="20sp"
android:textColor="@color/colorText"
android:textStyle="bold"/>
</RelativeLayout>

 

Abaixo o diagrama do layout anterior:

Diagrama do layout nav_header_user_logged.xml

Assim podemos partir para as duas listas de itens de menu.

Listas de itens

Teremos duas listas de itens:

  • Uma para qualquer status do usuário:
    • Contendo as categorias de calçados e alguns links que devem ser passíveis de acesso independente do status do usuário.

Menu gaveta para usuário não conectado

  • Outra para somente quando o usuário estiver conectado:
    • Será a lista ao fundo do menu gaveta;
    • Conterá itens específicos para usuários conectados, como, por exemplo, lista de pedidos.

Menu gaveta para usuário conectado

Iremos trabalhar com dois RecyclerViews. De acordo com o status do usuário (conectado ou não) a segunda lista será ou não apresentada, juntamente com o cabeçalho correto.

Antes de prosseguirmos para a codificação da primeira lista, vamos criar um arquivo de layout que conterá todos os componentes de menu gaveta.

Em /res/layout crie o layout nav_menu.xml com o código a seguir:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/ll_menu_drawer"
android:orientation="vertical"
android:layout_gravity="start"
android:layout_width="320dp"
android:layout_height="match_parent"
android:background="@color/colorPrimary">

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

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

<!-- Código da primeira e principal lista de itens. -->

<!-- Código da segunda lista de itens. -->
</LinearLayout>

 

Note que os <include> dos cabeçalhos já estão ocorrendo. Em código dinâmico trabalharemos qual será apresentado e qual será escondido de acordo com o status do usuário.

Ainda temos de atualizar o layout /res/layout/activity_main.xml para já adicionar o nav_menu.xml:

<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
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" />

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

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

 

Note que o <include layout="@layout/nav_menu" /> entrou no lugar da View <NavigationView>, que não mais será utilizada neste projeto.

Também removemos os atributos android:layout_widthandroid:layout_height do primeiro <include>, pois este layout vinculado via <include>, app_bar_main.xml, será utilizado somente neste ponto do projeto, a principio. Então não é necessário o fornecimento de valores extras de atributos que não diferem dos já definidos no layout incluído.

Classe de domínio para itens de menu gaveta

Antes de prosseguirmos com a codificação das listas, vamos primeiro definir uma classe de domínio para conter as informações de itens.

Na raiz do projeto crie um novo pacote, /domain:

  • Clique com o botão direito do mouse na raiz do projeto;
  • Então acesse New;
  • Logo após clique em Package;
  • Como nome coloque domain e clique em OK.

Criando um novo pacote no Android Studio

Agora crie uma nova classe com o rótulo NavMenuItem:

  • Em /domain clique com o botão direito do mouse;
  • Acesse New;
  • Clique em Kotlin File / Class;
  • Em Name coloque NavMenuItem;
  • Em Kind selecione Class;
  • Clique em OK.

Criando uma nova classe no Android Studio

Coloque como código inicial de NavMenuItem o seguinte:

class NavMenuItem(
val label : String,
val iconId : Int = DEFAULT_ICON_ID ){

companion object {
const val DEFAULT_ICON_ID = -1
}
}

 

Como alguns itens não têm ícone, a constante DEFAULT_ICON_ID será utilizada em ao menos uma lógica de negócio do projeto, mais precisamente no adaptador de itens de lista, para que os componentes visuais corretos do layout de item se mantenham em tela.

Base de dados dos itens de menu

Ainda temos de ter uma classe de "persistência" dos itens de menu, isso para manter todo o código bem dividido.

Na raiz do projeto crie um novo pacote, /data. Logo depois, neste novo pacote, crie a classe NavMenuItemsDataBase com o código a seguir:

class NavMenuItemsDataBase( context: Context ) {

/*
* Itens de menu gaveta que sempre estarão presentes,
* independente do status do usuário (conectado ou
* não).
* */
val items = listOf(
NavMenuItem(
context.getString(R.string.item_all_shoes)
),
NavMenuItem(
context.getString(R.string.item_flip_flops)
),
NavMenuItem(
context.getString(R.string.item_cleats)
),
NavMenuItem(
context.getString(R.string.item_sandals)
),
NavMenuItem(
context.getString(R.string.item_ballet_shoes)
),
NavMenuItem(
context.getString(R.string.item_suit_shoes)
),
NavMenuItem(
context.getString(R.string.item_shoes)
),
NavMenuItem(
context.getString(R.string.item_performance_shoes)
),
NavMenuItem(
context.getString(R.string.item_contact),
R.drawable.ic_email_black_24dp
),
NavMenuItem(
context.getString(R.string.item_about),
R.drawable.ic_domain_black_24dp
),
NavMenuItem(
context.getString(R.string.item_privacy_policy),
R.drawable.ic_shield_lock_black_24dp
)
)

/*
* Itens de menu gaveta que estarão presentes somente
* quando o usuário estiver conectado.
* */
val itemsLogged = listOf(
NavMenuItem(
context.getString(R.string.item_my_orders),
R.drawable.ic_package_variant_closed_black_24dp
),
NavMenuItem(
context.getString(R.string.item_settings),
R.drawable.ic_settings_black_24dp
),
NavMenuItem(
context.getString(R.string.item_sign_out),
R.drawable.ic_exit_run_black_24dp
)
)
}

 

Essa classe não é uma classe de dados simulados, mock. Os dados nela são os que serão utilizados em produção.

Ícones dos itens

No tópico anterior você deve ter notado que alguns itens têm ícones os acompanhando, como também apresentado em protótipo estático.

Todos foram descarregados do site Material Design Icons. Mesmo este não sendo o repositório oficial de ícones do Android, ele é o mais completo e útil.

Dica: em Material Design Icons, quando você escolher um ícone, clique nele e logo depois clique em Icon Package e então selecione Android 5.x. Assim você terá várias versões do mesmo ícone e poderá utilizar a necessária em projeto sem auxílio de software de edição de imagem. Aqui ficaremos com as versões de 24dp, tamanho padrão para ícones de itens de menu gaveta.

Você pode estar baixando todos os ícones de itens direto dos links a seguir:

Adapter dos itens

Para a classe adaptadora de itens, a mesma que será utilizada em ambas as listas possíveis, vamos iniciar com o layout de item.

Em /res/layout crie o novo layout XML nav_menu_item.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:paddingRight="16dp"
android:paddingEnd="16dp"
android:paddingLeft="16dp"
android:paddingStart="16dp"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="48dp">

<ImageView
android:id="@+id/iv_icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginRight="32dp"
android:layout_marginEnd="32dp"
android:layout_gravity="center_vertical"
android:tint="@color/colorText"/>

<TextView
android:id="@+id/tv_label"
android:layout_gravity="center_vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/colorText"/>
</LinearLayout>

 

A seguir o diagrama do layout anterior:

Diagrama do layout nav_menu_item.xml

Note que os espaçamentos, alturas e larguras do layout de item estão respeitando as exigências em documentação oficial de menu gaveta no Material Design.

Agora, na raiz do projeto, onde se encontra a MainActivity, crie uma nova classe Kotlin, classe com o rótulo NavMenuItemAdapter. Coloque nesta nova entidade o seguinte código de classe adaptadora de itens do RecyclerView:

class NavMenuItemsAdapter( val items: List<NavMenuItem> ) :
RecyclerView.Adapter<NavMenuItemsAdapter.ViewHolder>() {

lateinit var selectionTracker: SelectionTracker<Long>

override fun onCreateViewHolder(
parent: ViewGroup,
type: Int ): ViewHolder {

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

return ViewHolder( layout )
}

override fun onBindViewHolder(
holder: ViewHolder,
position: Int ) {

holder.setData( items[ position ] )
}

override fun getItemCount() = items.size

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

private val ivIcon : ImageView
private val tvLabel : TextView

init{
ivIcon = itemView.findViewById( R.id.iv_icon )
tvLabel = itemView.findViewById( R.id.tv_label )
}

fun setData( item: NavMenuItem ){

tvLabel.text = item.label

if( item.iconId != NavMenuItem.DEFAULT_ICON_ID ){
ivIcon.setImageResource( item.iconId )
ivIcon.visibility = View.VISIBLE
}
else{
ivIcon.visibility = View.GONE
}
}
}
}

 

Note o bloco condicional em setData() para corretamente apresentar o layout de cada item em lista, com ícone ou sem ícone.

Lista principal, contendo categorias de calçados

No layout /res/layout/nav_menu.xml adicione o seguinte RecyclerView em destaque:

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

<android.support.v7.widget.RecyclerView
android:id="@+id/rv_menu_items"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:scrollbars="vertical"/>
...
</LinearLayout>

 

O atributo android:layout_weight="1" está sendo utilizado, pois o único componente, segundo protótipo estático, que pode ter barra de rolagem é o da lista de categorias e itens principais. O cabeçalho e a lista de fundo, quando ativa, não podem ter rolagem. Por isso também o uso do LinearLayout como ViewGroup principal.

Agora vamos a inicialização da lista de itens, inicialização dentro da atividade principal. Na MainActivity adicione o método a seguir:

...
/*
* Método que inicializa a lista de itens de menu gaveta
* que estará presente quando o usuário estiver ou não
* conectado ao aplicativo.
* */
private fun initNavMenuItems(){
rv_menu_items.setHasFixedSize( false )
rv_menu_items.layoutManager = LinearLayoutManager( this )
rv_menu_items.adapter = NavMenuItemsAdapter( NavMenuItemsDataBase( this ).items )
}
...

 

Em tópicos posteriores criaremos ainda mais métodos responsáveis pela inicialização de todo o menu gaveta.

Lista de itens para usuário conectado

Ainda no layout /res/layout/nav_menu.xml adicione os seguintes componentes visuais em destaque:

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

<View
android:id="@+id/v_nav_vertical_line"
android:layout_width="match_parent"
android:layout_height="0.5dp"
android:background="@color/colorViewLine"/>

<android.support.v7.widget.RecyclerView
android:id="@+id/rv_menu_items_logged"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="8dp"
android:paddingBottom="8dp"/>
</LinearLayout>

 

A View foi adicionada para representar a linha superior da segunda lista de itens, como definido em protótipo estático:

Linhas vertical de separação de listas

Com isso podemos ir a inicialização da lista de itens para usuário conectado, inicialização também dentro da atividade principal. Na MainActivity adicione o método a seguir:

...
/*
* Método que inicializa a parte de lista de itens de menu
* gaveta que estará presente somente quando o usuário
* estiver conectado ao aplicativo.
* */
private fun initNavMenuItemsLogged(){
rv_menu_items_logged.setHasFixedSize( true )
rv_menu_items_logged.layoutManager = LinearLayoutManager( this )
rv_menu_items_logged.adapter = NavMenuItemsAdapter( NavMenuItemsDataBase( this ).itemsLogged )
}
...

E a seleção dos itens? Como será possível?

Se você conhece o RecyclerView deve estar se perguntando como se dará a seleção dos itens, qual lógica de negócio será utilizada, até porque o item de menu gaveta selecionado deverá estar em destaque e acionar um novo conteúdo na tela.

Temos dois RecyclerViews, logo sabemos que qualquer algoritmo de seleção de item adicionado terá de ter a parte de sincronia entre as duas listas, isso, pois a seleção em uma lista deve remover a seleção do item anterior, contido nela ou na outra lista.

Como resposta ao problema da seleção de itens, vamos utilizar a SelectionTracker, API nativa do Android e criada somente para isso: permitir a seleção de itens no RecyclerView.

Para saber mais sobre a API de seleção no RecyclerView entre no artigo a seguir: SelectionTracker Para Seleção de Itens no RecyclerView Android.

Instalação da API de seleção e atualizações de apoio

Nosso primeiro passo com a SelectionTracker é adiciona-la ao projeto. No Gradle Level de Aplicativo, build.gradle (Module: app), adicione a referência em destaque e logo depois sincronize o projeto:

...
dependencies {
...

/* SelectionTracker API */
implementation 'com.android.support:recyclerview-selection:28.0.0'
}

 

Se você estudou a SelectionTracker deve saber que teremos de ter um identificador único para os objetos que serão utilizados em seleção.

Em nosso domínio de problema esses objetos serão todos do tipo NavMenuItem, porém nesta classe não temos nenhuma propriedade para guardar um identificador único. Até poderíamos utilizar o rótulo de cada item, mas isso certamente será um problema em atualizações futuras do projeto, pois a função do rótulo não é ser único.

Sendo assim, vamos atualizar a classe NavMenuItem para suportar também uma propriedade id. Segue atualização:

class NavMenuItem(
val id: Long,
val label : String,
val iconId : Int = DEFAULT_ICON_ID ){

companion object {
const val DEFAULT_ICON_ID = -1
}
}

 

Antes de atualizarmos a classe de dados que contém os itens de menu gaveta, temos de ter em mente que os IDs que criaremos para os itens certamente serão únicos independente das atualizações que o projeto BlueShoes venha a sofrer. Devido a isso é possível que em lógicas de negócio futuras seja necessário o uso de alguns desses IDs.

Para que o uso de qualquer um dos IDs de itens em lógicas do projeto não venha a ser um problema de "valor mágico", por exemplo, algo que seguramente atrasaria qualquer atualização no menu gaveta. Para evitar isso, vamos definir os IDs em um arquivo XML específico para identificadores únicos.

Em /res/values crie um novo arquivo XML chamado nav_menu_items_ids.xml e com o seguinte código estático:

<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--
Segundo testes, foi observado que os valores inteiros
dos IDs são adicionados na ordem crescente de
declaração de <itens>.
-->
<item name="item_all_shoes" type="id" format="integer" />
<item name="item_flip_flops" type="id" format="integer" />
<item name="item_cleats" type="id" format="integer" />
<item name="item_sandals" type="id" format="integer" />
<item name="item_ballet_shoes" type="id" format="integer" />
<item name="item_suit_shoes" type="id" format="integer" />
<item name="item_shoes" type="id" format="integer" />
<item name="item_performance_shoes" type="id" format="integer" />
<item name="item_contact" type="id" format="integer" />
<item name="item_about" type="id" format="integer" />
<item name="item_privacy_policy" type="id" format="integer" />
<item name="item_my_orders" type="id" format="integer" />
<item name="item_settings" type="id" format="integer" />
<item name="item_sign_out" type="id" format="integer" />
</resources>

 

Com isso podemos atualizar a classe NavMenuItemDataBase. Adicione os códigos em destaque:

class NavMenuItemsDataBase( context: Context ) {

/*
* Itens de menu gaveta que sempre estarão presentes,
* independente do status do usuário (conectado ou
* não).
* */
val items = listOf(
NavMenuItem(
R.id.item_all_shoes.toLong(),
context.getString(R.string.item_all_shoes)
),
NavMenuItem(
R.id.item_flip_flops.toLong(),
context.getString(R.string.item_flip_flops)
),
NavMenuItem(
R.id.item_cleats.toLong(),
context.getString(R.string.item_cleats)
),
NavMenuItem(
R.id.item_sandals.toLong(),
context.getString(R.string.item_sandals)
),
NavMenuItem(
R.id.item_ballet_shoes.toLong(),
context.getString(R.string.item_ballet_shoes)
),
NavMenuItem(
R.id.item_suit_shoes.toLong(),
context.getString(R.string.item_suit_shoes)
),
NavMenuItem(
R.id.item_shoes.toLong(),
context.getString(R.string.item_shoes)
),
NavMenuItem(
R.id.item_performance_shoes.toLong(),
context.getString(R.string.item_performance_shoes)
),
NavMenuItem(
R.id.item_contact.toLong(),
context.getString(R.string.item_contact),
R.drawable.ic_email_black_24dp
),
NavMenuItem(
R.id.item_about.toLong(),
context.getString(R.string.item_about),
R.drawable.ic_domain_black_24dp
),
NavMenuItem(
R.id.item_privacy_policy.toLong(),
context.getString(R.string.item_privacy_policy),
R.drawable.ic_shield_lock_black_24dp
)
)

/*
* Itens de menu gaveta que estarão presentes somente
* quando o usuário estiver conectado.
* */
val itemsLogged = listOf(
NavMenuItem(
R.id.item_my_orders.toLong(),
context.getString(R.string.item_my_orders),
R.drawable.ic_package_variant_closed_black_24dp
),
NavMenuItem(
R.id.item_settings.toLong(),
context.getString(R.string.item_settings),
R.drawable.ic_settings_black_24dp
),
NavMenuItem(
R.id.item_sign_out.toLong(),
context.getString(R.string.item_sign_out),
R.drawable.ic_exit_run_black_24dp
)
)

fun getLastItemId() = items.last().id

fun getFirstItemLoggedId() = itemsLogged.first().id
}

 

Os métodos getLastItemId() e getFirstItemLoggedId() serão úteis em parte da lógica de seleção de itens, melhor explicada em classes posteriores, ligadas a API SelectionTracker.

Classes de permissão a seleção de item

A SelectionTracker API é acompanhada de um código boilerplate que não deixa nenhuma desenvolvedor satisfeito. Mas é algo obrigatório, então vamos a adição de algumas classes responsáveis pelo correto funcionamento da característica de seleção de itens.

Primeiro, na raiz do projeto crie um novo pacote com o rótulo util. Neste novo pacote crie uma nova classe Kotlin com o nome NavMenuItemKeyProvider e o código como a seguir:

/*
* Subclasse de ItemKeyProvider que fornece acesso a chaves de seleção
* estáveis, podendo ser de três tipos: Parcelable (e suas
* subclasses); String e Long.
* */
class NavMenuItemKeyProvider( val items: List<NavMenuItem> ) :
ItemKeyProvider<Long>( ItemKeyProvider.SCOPE_MAPPED ) {

/*
* Retorna a chave de seleção na posição fornecida do adaptador ou
* então retorna null.
* */
override fun getKey( position: Int ) = items[ position ].id

/*
* Retorna a posição correspondente à chave de seleção, ou
* RecyclerView.NO_POSITION em caso de null em getKey().
* */
override fun getPosition( key: Long )
= items.indexOf(
items.filter{
item -> item.id == key
}.single()
)
}

 

Importante: não deixe de ler os comentários de todos os códigos do projeto, eles complementam as explicações em artigo.

Ainda no pacote /util crie a classe Kotlin NavMenuItemDetails com o código abaixo:

/*
* Uma implementação de ItemDetails fornece à biblioteca de seleção
* acesso a informações sobre um específico item do RecyclerView. Esta
* classe é um componente chave no controle dos comportamentos da
* biblioteca de seleção no contexto de uma atividade específica.
* */
class NavMenuItemDetails(
var item: NavMenuItem? = null,
var adapterPosition: Int = -1
) : ItemDetailsLookup.ItemDetails<Long>() {

/*
* Retorna a posição do adaptador do item
* (ViewHolder.adapterPosition).
* */
override fun getPosition() = adapterPosition

/*
* Retorna a entidade que é a chave de seleção do item.
* */
override fun getSelectionKey() = item!!.id

/*
* Retorne "true" se o item tiver uma chave de seleção. Se true
* não for retornado o item em foco (acionado pelo usuário) não
* será selecionado.
* */
override fun inSelectionHotspot( e: MotionEvent ) = true
}

 

Agora, no mesmo pacote, crie a classe NavMenuItemDetailsLookup com o código a seguir:

/*
* ItemDetailsLookup permite que a biblioteca de seleção acesse
* informações sobre os itens do RecyclerView que receberam um
* MotionEvent. Ele é efetivamente uma factory para instâncias
* de ItemDetails que são submetidas a backup (ou extraídas)
* de uma ocorrência de RecyclerView.ViewHolder.
* */
class NavMenuItemDetailsLookup( val rvMenuItems: RecyclerView ) :
ItemDetailsLookup<Long>() {

/*
* Retorna o ItemDetails para o item sob o evento
* (MotionEvent) ou nulo caso não haja um.
* */
override fun getItemDetails( event: MotionEvent ): ItemDetails<Long>? {

val view = rvMenuItems.findChildViewUnder( event.x, event.y )

if( view != null ){
val holder = rvMenuItems.getChildViewHolder( view )
return (holder as NavMenuItemsAdapter.ViewHolder).itemDetails
}

return null
}
}

 

Note a referência ao adapter de listas de itens de menu: (holder as NavMenuItemsAdapter.ViewHolder).itemDetails. No próximo tópico adicionaremos as atualizações necessárias a essa classe adaptadora.

Por fim, ainda no pacote /util, adicione a classe responsável por conter a lógica de negócio de itens que podem ganhar ou perder seleção, incluindo o uso dos métodos getLastItemId()getFirstItemLoggedId().

Segue código da classe NavMenuItemPredicate:

/*
* SelectionTracker.SelectionPredicate é utilizada para definir
* quais itens poderão ser selecionados e quantos deles.
*
* A parametrização deve ser do tipo da chave estável definida
* em ItemKeyProvider.
* */
class NavMenuItemPredicate( val activity: MainActivity ) :
SelectionTracker.SelectionPredicate<Long>() {

/*
* Retorne true se puder ter múltipla seleção e false para
* somente uma seleção.
* */
override fun canSelectMultiple() = false

/*
* Retorne true se o item referente a key puder ser definido
* como nextState.
* */
override fun canSetStateForKey(
key: Long,
nextState: Boolean ) : Boolean{

/*
* A lógica de negócio abaixo foi adotada para que não
* seja possível deixar o menu gaveta com nenhum item
* selecionado. Assim, se o status do item acionado for
* false (nextState = false) e se ele for o item já
* selecionado, então retornamos false para que o status
* dele não mude, continue como "selecionado".
* */
if( !nextState ){
val lastItemId = NavMenuItemsDataBase( activity ).getLastItemId()
val firstItemLoggedId = NavMenuItemsDataBase( activity ).getFirstItemLoggedId()

val sizeNavMenuItems = activity.selectNavMenuItems.selection.size()
val sizeNavMenuItemsLogged = activity.selectNavMenuItemsLogged.selection.size()

/*
* Há somente duas situações onde um item pode
* acionar canSetStateForKey() com nextState sendo
* false:
*
* 1ª - Quando o item está selecionado e então
* ele é acionado novamente, para que perca a
* seleção;
*
* 2ª - Quando é removida a seleção do item
* via deselect(), como estamos fazendo na
* atividade principal de projeto.
*
* No caso da 2ª situação, isso acontece porque
* temos dois objetos de seleção sendo utilizados,
* sendo assim, é preciso saber o intervalo do ID
* do item alvo, pois ele somente não perde a seleção
* se ele mesmo receber um novo acionamento. Em caso
* de item de outra lista, ele deve sim perder a
* seleção.
* */
if( (key <= lastItemId && sizeNavMenuItemsLogged == 0)
|| (key >= firstItemLoggedId && sizeNavMenuItems == 0) ){
return false
}
}

return true
}

/*
* Retorne true se o item referente a position puder ser definido
* como nextState. NÃO FUNCIONA COMO ESPERADO.
* */
override fun canSetStateAtPosition(
position: Int,
nextState: Boolean ) = true
}

 

Vamos às outras atualizações para que a SelectionTracker funcione como esperado.

Atualização no adapter de itens

Na classe adaptadora de itens de menu, NavMenuItemsAdapter, adicione os códigos em destaque:

class NavMenuItemsAdapter( ... ) : ... {

lateinit var selectionTracker: SelectionTracker<Long>

...

inner class ViewHolder( ... ) : ... {
...

val itemDetails: NavMenuItemDetails

init{
...

itemDetails = NavMenuItemDetails()
}

fun setData( item: NavMenuItem ){
...

/*
* São nos blocos condicionais a seguir que devem vir os
* algoritmos de atualização de UI, isso para indicar o
* item selecionado e os itens não selecionados.
* */
itemDetails.item = item
itemDetails.adapterPosition = adapterPosition

if( selectionTracker.isSelected( itemDetails.getSelectionKey() ) ){
itemView.setBackgroundColor(
ContextCompat.getColor(
itemView.context,
R.color.colorNavItemSelected
)
)
}
else{
itemView.setBackgroundColor( Color.TRANSPARENT )
}
}
}
}

Classe de domínio, User

Vamos adicionar uma nova classe de domínio que permitirá, neste primeiro momento, o teste de mudança de menu gaveta.

Em /domain adicione a classe User com o código a seguir:

class User(
val name: String,
val image: Int,
val status: Boolean = false
)

 

Na atividade principal adicione o código abaixo em destaque:

class MainActivity : ... {

val user = User(
"Thiengo Vinícius",
R.drawable.user,
false
)
...
}

 

A imagem de teste, que está sendo utilizada como avatar do usuário fictício, pode ser baixada nos links a seguir:

Inicialização dos SelectionTracker

Na atividade principal, para o SelectionTracker de itens de lista para usuários conectados ou não, adicione os códigos em destaque:

class MainActivity : ... {
...

lateinit var navMenuItems : List<NavMenuItem>
lateinit var selectNavMenuItems: SelectionTracker<Long>

...
private fun initNavMenuItems(){
rv_menu_items.setHasFixedSize( false )
rv_menu_items.layoutManager = LinearLayoutManager( this )
rv_menu_items.adapter = NavMenuItemsAdapter( navMenuItems )

initNavMenuItemsSelection()
}

/*
* Método responsável por inicializar o objeto de seleção
* de itens de menu gaveta, seleção dos itens que aparecem
* para usuário conectado ou não.
* */
private fun initNavMenuItemsSelection(){

selectNavMenuItems = SelectionTracker.Builder<Long>(
"id-selected-item",
rv_menu_items,
NavMenuItemKeyProvider( navMenuItems ),
NavMenuItemDetailsLookup( rv_menu_items ),
StorageStrategy.createLongStorage()
)
.withSelectionPredicate( NavMenuItemPredicate( this ) )
.build()

(rv_menu_items.adapter as NavMenuItemsAdapter).selectionTracker = selectNavMenuItems
}
...
}

 

Note que em initNavMenuItems(), além de termos adicionado a invocação ao método de configuração de selectNavMenuItemsinitNavMenuItemsSelection(), também atualizamos o modo de inicialização do adapter de lista: rv_menu_items.adapter = NavMenuItemsAdapter( navMenuItems ).

A propriedade navMenuItems será iniciada em um método de inicialização do menu gaveta, que será discutido em tópico posterior.

Agora vamos ao código do SelectionTracker para itens de menu específicos para usuário conectado. Ainda na MainActivity, adicione os códigos em destaque:

class MainActivity : ... {
...

lateinit var navMenuItemsLogged : List<NavMenuItem>
lateinit var selectNavMenuItemsLogged: SelectionTracker<Long>

...
/*
* Método que inicializa a parte de lista de itens de menu
* gaveta que estará presente somente quando o usuário
* estiver conectado ao aplicativo.
* */
private fun initNavMenuItemsLogged(){
rv_menu_items_logged.setHasFixedSize( true )
rv_menu_items_logged.layoutManager = LinearLayoutManager( this )
rv_menu_items_logged.adapter = NavMenuItemsAdapter( navMenuItemsLogged )

initNavMenuItemsLoggedSelection()
}

/*
* Método responsável por inicializar o objeto de seleção
* de itens de menu gaveta, seleção dos itens que aparecem
* somente quando o usuário está conectado ao app.
* */
private fun initNavMenuItemsLoggedSelection(){

selectNavMenuItemsLogged = SelectionTracker.Builder<Long>(
"id-selected-item-logged",
rv_menu_items_logged,
NavMenuItemKeyProvider( navMenuItemsLogged ),
NavMenuItemDetailsLookup( rv_menu_items_logged ),
StorageStrategy.createLongStorage()
)
.withSelectionPredicate( NavMenuItemPredicate( this ) )
.build()

(rv_menu_items_logged.adapter as NavMenuItemsAdapter).selectionTracker = selectNavMenuItemsLogged
}
...
}

 

Para que ambos os SelectionTracker possam trabalhar o item selecionado em circunstâncias de reconstrução de atividade é necessário, como parte do código, o uso do onSaveInstanceState() como a seguir na MainActivity (adicione a está atividade):

...
override fun onSaveInstanceState( outState: Bundle? ) {
super.onSaveInstanceState( outState )

/*
* Para manter o item de menu gaveta selecionado caso
* haja reconstrução de atividade.
* */
selectNavMenuItems.onSaveInstanceState( outState!! )
selectNavMenuItemsLogged.onSaveInstanceState( outState )
}
...

E como será o acionamento de fragmentos?

Com o uso de RecyclerView não poderemos aproveitar o listener de item selecionado adicionado ao projeto na criação dele, o listener NavigationView.OnNavigationItemSelectedListener.

Tendo em mente que estamos trabalhando com objetos de seleção específicos do RecyclerView, temos então de utilizar alguma API de clique / seleção nestes objetos, porém com algum algoritmo de sincronia para que somente um item fique selecionado em menu gaveta, independente da lista de itens em foco.

Sendo assim podemos utilizar o método onItemStateChanged() da classe SelectionTracker.SelectionObserver. Na MainActivity adicione a seguinte classe interna:

...
inner class SelectObserverNavMenuItems(
val callbackRemoveSelection: ()->Unit
) : SelectionTracker.SelectionObserver<Long>(){

/*
* Método responsável por permitir que seja possível
* disparar alguma ação de acordo com a mudança de
* status de algum item em algum dos objetos de seleção
* de itens de menu gaveta. Aqui vamos proceder com
* alguma ação somente em caso de item obtendo seleção,
* para item perdendo seleção não haverá processamento,
* pois este status não importa na lógica de negócio
* deste método.
* */
override fun onItemStateChanged(
key: Long,
selected: Boolean ) {
super.onItemStateChanged( key, selected )

/*
* Padrão Cláusula de Guarda para não seguirmos
* com o processamento em caso de item perdendo
* seleção. O processamento posterior ao condicional
* abaixo é somente para itens obtendo a seleção,
* selected = true.
* */
if( !selected ){
return
}

/*
* Para garantir que somente um item de lista se
* manterá selecionado, é preciso acessar o objeto
* de seleção da lista de itens de usuário conectado
* para então remover qualquer possível seleção
* ainda presente nela. Sempre haverá somente um
* item selecionado, mas infelizmente o método
* clearSelection() não estava respondendo como
* esperado, por isso a estratégia a seguir.
* */
callbackRemoveSelection()

/*
* TODO: Mudança de Fragment
* */

/*
* Fechando o menu gaveta.
* */
drawer_layout.closeDrawer( GravityCompat.START )
}
}
...

 

Estamos trabalhando com um callback, callbackRemoveSelection, para que ambos os SelectionTracker possam utilizar a mesma classe observadora.

Sobre o padrão Cláusula de Guarda que está sendo utilizado, você consegue mais informações sobre ele no artigo a seguir: Padrão de Projeto: Cláusula de Guarda. É um padrão excelente e de fácil uso.

Ainda é preciso atualizar os métodos de inicialização de SelectionTracker. Adicione os códigos em destaque, primeiro para a propriedade selectNavMenuItems:

...
private fun initNavMenuItemsSelection(){
...

selectNavMenuItems.addObserver(
SelectObserverNavMenuItems {
selectNavMenuItemsLogged.selection.filter {
selectNavMenuItemsLogged.deselect( it )
}
}
)

(rv_menu_items.adapter as NavMenuItemsAdapter).selectionTracker = selectNavMenuItems
}
...

 

Agora para a propriedade selectNavMenuItemsLogged:

...
private fun initNavMenuItemsLoggedSelection(){
...

selectNavMenuItemsLogged.addObserver(
SelectObserverNavMenuItems {
selectNavMenuItems.selection.filter {
selectNavMenuItems.deselect( it )
}
}
)

(rv_menu_items_logged.adapter as NavMenuItemsAdapter).selectionTracker = selectNavMenuItemsLogged
}
...

Inicialização do menu gaveta

Agora podemos ir aos métodos e lógicas de negócio para a correta inicialização do menu gaveta.

Primeiro o método responsável pelo preenchimento de dados no cabeçalho de menu quando com usuário conectado. Na MainActivity adicione o método a seguir:

...
private fun fillUserHeaderNavMenu(){
if( user.status ) { /* Conectado */
iv_user.setImageResource(user.image)
tv_user.text = user.name
}
}
...

 

Agora o método responsável por esconder as Views corretas de acordo com o status do usuário (conectado ou não). Ainda na atividade principal adicione o método a seguir:

...
/*
* Método responsável por esconder itens do menu gaveta de
* acordo com o status do usuário (conectado ou não).
* */
private fun showHideNavMenuViews(){
if( user.status ){ /* Conectado */
rl_header_user_not_logged.visibility = View.GONE
fillUserHeaderNavMenu()
}
else{ /* Não conectado */
rl_header_user_logged.visibility = View.GONE
v_nav_vertical_line.visibility = View.GONE
rv_menu_items_logged.visibility = View.GONE
}
}
...

 

Assim o método principal com a lógica de inicialização do menu gaveta, initNavMenu():

...
/*
* Método de inicialização do menu gaveta, responsável por
* apresentar o cabeçalho e itens de menu de acordo com o
* status do usuário (logado ou não).
* */
private fun initNavMenu( savedInstanceState: Bundle? ){

val navMenu = NavMenuItemsDataBase( this )
navMenuItems = navMenu.items
navMenuItemsLogged = navMenu.itemsLogged

showHideNavMenuViews()

initNavMenuItems()
initNavMenuItemsLogged()

if( savedInstanceState != null ){
selectNavMenuItems.onRestoreInstanceState( savedInstanceState )
selectNavMenuItemsLogged.onRestoreInstanceState( savedInstanceState )
}
else{
/*
* O primeiro item do menu gaveta deve estar selecionado
* caso não seja uma reinicialização de tela / atividade.
* O primeiro item aqui é o de ID 1.
* */
selectNavMenuItems.select( R.id.item_all_shoes.toLong() )
}
}
...

 

O método anterior é invocado no onCreate() da atividade:

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

initNavMenu( savedInstanceState )
}
...

Ajustes extras

Vamos criar um novo pacote na raiz do projeto, desta vez com o rótulo view. Logo em seguida arraste as classes MainActivityNavMenuItemsAdapter para dentro deste pacote. Assim teremos a seguinte nova configuração física de projeto:

Nova configuração física de projeto no Android Studio

Removendo o que não é necessário

Neste ponto do projeto, podemos remover alguns arquivos e códigos que, a princípio, não serão mais úteis ao aplicativo.

Em /res remova:

  • Arquivos:
    • /drawabe/ic_launcher_background.xml;
    • /drawabe/side_nav_bar.xml;
    • /drawable-v21/ic_menu_camera.xml;
    • /drawable-v21/ic_menu_gallery.xml;
    • /drawable-v21/ic_menu_manage.xml;
    • /drawable-v21/ic_menu_send.xml;
    • /drawable-v21/ic_menu_share.xml;
    • /drawable-v21/ic_menu_slideshow.xml;
    • /layout/nav_header_main.xml;
    • /menu/activity_main_drawer.xml;
    • /values/drawable.xml.
  • Diretórios (incluindo os arquivos dentro deles):
    • /drawable-v24;
    • /mipmap-anydpi-v26.

Agora na MainActivity remova os códigos a seguir em destaque:

class MainActivity :
AppCompatActivity(),
NavigationView.OnNavigationItemSelectedListener {

override fun onCreate( ... ) {
...

fab.setOnClickListener { view ->
Snackbar
.make(
view,
"Mude para a sua própria ação.",
Snackbar.LENGTH_LONG
)
.setAction(
"Ação",
null
)
.show()
}

...

nav_view.setNavigationItemSelectedListener( this )
}
...

override fun onNavigationItemSelected( item: MenuItem ): Boolean {
/*
* Lida com cliques nos itens do menu gaveta.
* */
when( item.itemId ){
R.id.nav_camera -> { }
R.id.nav_gallery -> { }
R.id.nav_slideshow -> { }
R.id.nav_manage -> { }
R.id.nav_share -> { }
R.id.nav_send -> { }
}

drawer_layout.closeDrawer( GravityCompat.START )
return true
}
}

 

Ainda na MainActivity, mas agora no layout /res/layout/app_bar_main.xml, remova o FloatingActionButton:

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

 

Em /res/values/dimens.xml remova as marcações em destaque:

<resources>
...

<dimen name="nav_header_vertical_spacing">8dp</dimen>
<dimen name="nav_header_height">176dp</dimen>

<dimen name="fab_margin">16dp</dimen>
</resources>

 

Por fim, em /res/values/strings.xml remova os trechos em destaque:

<resources>
...

<string name="nav_header_title">Android Studio</string>
<string name="nav_header_subtitle">android.studio@android.com</string>
<string name="nav_header_desc">Navigation header</string>

...
</resources>

 

Assim podemos testar essa primeira parte desenvolvida do projeto.

Testes e resultados

Abra o Android Studio, vá em "Build", então em "Rebuid project". Ao final do rebuild execute o aplicativo em seu aparelho ou emulador Android de testes.

Assumindo, em primeiro teste, o usuário com o status "não conectado":

...
val user = User(
"Thiengo Vinícius",
R.drawable.user,
false
)
...

 

Temos:

App Android BlueShoes com usuário não conectado

Agora mudando o status do usuário para "conectado":

...
val user = User(
"Thiengo Vinícius",
R.drawable.user,
true
)
...

 

E executando novamente o projeto, temos:

App Android BlueShoes com usuário conectado

Com isso finalizamos essa primeira parte de desenvolvimento do projeto Android de mobile-commerce. Não deixe de se inscrever na 📩 lista de emails do Blog para continuar acompanhando a série.

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

Vídeos

A seguir os vídeos mostrando o passo a passo de desenvolvimento desta primeira parte do projeto Android de m-commerce:

O projeto também pode ser seguido pelo GitHub dele em: https://github.com/viniciusthiengo/blueshoes-kotlin-android.

Conclusão

Neste artigo, segundo da série sobre o mobile-commerce Android BlueShoes, desenvolvemos não a parte mais simples, mas a essencial para que as partes simples e complexas sejam posteriormente desenvolvidas.

Como desenvolvedor Android, a mensagem importante deste artigo é que o menu gaveta pode ter qualquer configuração, somos livres para utilizar a interface que quisermos como layout de gaveta ou layout principal.

Caso você tenha dúvidas ou dicas para este projeto, deixe logo abaixo nos comentários.

Curtiu o conteúdo? Não esqueça de compartilha-lo. E, por fim, não deixe de se inscrever na 📩 lista de emails.

Abraço.

Fontes 

Criação de uma gaveta de navegação

More resource types - ID

Colocando Telas de Introdução em Seu Aplicativo Android - Configurações de estilo

Unleash functional power on Android (I): Kotlin lambdas

Mipmaps vs. drawable folders [duplicate] - Resposta de Joel e Rajesh

android providing different drawable resources for orientations - Resposta de blejzz

Is there an easy way to add a border to the top and bottom of an Android View? - Resposta de Levon Petrosyan

How to achieve ripple animation using support library? - Resposta de Amintabar

Storing R.id.elements in XML resources

Receba em primeira mão, e com prioridade, os conteúdos Android exclusivos do Blog.
Email inválido

Relacionado

Como Impulsionar o App Android - Compartilhamento NativoComo Impulsionar o App Android - Compartilhamento NativoAndroid
Android About Page API Para Construir a Tela SobreAndroid About Page API Para Construir a Tela SobreAndroid
5 livros que não são de TI, mas que um desenvolvedor deveria ler5 livros que não são de TI, mas que um desenvolvedor deveria lerEmpreendedorismo
Android Mobile-Commerce, Apresentação e Protótipo do ProjetoAndroid Mobile-Commerce, Apresentação e Protótipo do ProjetoAndroid

Compartilhar

Comentários Facebook

Comentários Blog (25)

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...
Rafael Queiroz (1) (0)
25/07/2019
Boa tarde Vinícius, estou com um problema no seguinte trecho de código. Já pesquisei e não achei o erro.
private fun initNavMenuItems(){
        rv_menu_items.setHasFixedSize( false )
        rv_menu_items.layoutManager = LinearLayoutManager( this )
        rv_menu_items.adapter = NavMenuItemsAdapter( NavMenuItemsDataBase( this ).items )
    }
Acontece que os métodos setHasFixedSize, layoutManager e adapter não são encontrados, já verifiquei a class RecicleView importada android.support.v7.widget.RecyclerView, e mesmo assim os métodos não são encontrados. Poderia me dar uma luz? Att.: Rafael Queiroz
Responder
Vinícius Thiengo (0) (0)
12/08/2019
Rafael, tudo bem?

Se possível, confirme se a sua versão de compileSdkVersion é a 28. Você consegue verificar isso no Gradle Nível de Aplicativo, build.gradle (Module: app), de sua versão de projeto.

Estou pedindo isso, pois aparentemente este é o problema.

Não precisa migrar o projeto para o Android X agora, pode seguir como ele está, pois essa migração você fará na aula 14 do projeto.

De qualquer forma, vou deixar aqui o link da versão atual do Gradle Nível de Aplicativo do projeto BlueShoes. Novamente, ignore, por agora, a configuração do Android X:

https://github.com/viniciusthiengo/blueshoes-kotlin-android/blob/master/app/build.gradle

Abraço.
Responder
Rafael Queiroz (1) (0)
13/08/2019
Bom dia Vinícius, na paz?

Depois de um tempo eu vi que já estava com a versão atualizada e no layout estava fazendo referência a android.support.v7.widget.RecyclerView, justamente o que você mencionou acima, foi só atualizar para
androidx.recyclerview.widget.RecyclerView e tudo rodando de boa. Vlw pelo retorno, sigo acompanhando as aulas.

Forte Abraço.
Responder
Fabricio (2) (0)
10/05/2019
Tenho uma duvida quanto ao tamanho das imagens, por exemplo resolvi criar uma imagem para acompanhar o projeto e ao reduzir o tamanho de 960x1600 para 640x960 o valor da altura esta variando um pouco e nao fica com 960 no ponto vai para 1067, se eu forçar da uma distorção e nao fica legal, então eu queria saber se eu posso deixar assim? e se deixar posso ter problemas na resolucao depois? por causa da variação acabando numero impar
Responder
Vinícius Thiengo (2) (0)
14/05/2019
Fabricio, tudo bem?

Na verdade você pode utilizar qualquer tamanho para a imagem de background, pode até mesmo fornecer somente uma imagem (o que se espera é uma imagem para todas as quatro principais versões do folder drawable).

O que vai acontecer neste caso é a falta de qualidade, distorção de imagem, em muitos aparelhos Android. Certamente os usuários e cliente notarão o problema, algo que vai soar pouco profissional para você.

Minha recomendação é que você tenha a imagem de background de app (ou de atividade ou de fragmento) nos seguintes folders com os seguintes tamanhos:

-> /res/drawable-mdpi: 320x480 pixels;

-> /res/drawable-hdpi: 480x800 pixels;

-> /res/drawable-xhdpi: 640x960 pixels;

-> /res/drawable-xxhdpi: 960x1600 pixels.

Resumo: a preocupação é com a qualidade do aplicativo entregue ao usuário final.

Abraço.
Responder
Jonathan (2) (0)
26/04/2019
Professor estou com esse erro e não consigo achar oque é

Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'void android.support.v7.widget.RecyclerView.setHasFixedSize(boolean)' on a null object refer
Responder
Vinícius Thiengo (0) (0)
27/04/2019
Jonathan, tudo bem?

Verifique se o seu layout /res/layout/nav_menu.xml está como no link a seguir:

https://github.com/viniciusthiengo/blueshoes-kotlin-android/blob/master/app/src/main/res/layout/nav_menu.xml

Ele deve estar como no link acima. Caso já esteja, volte aqui que assim vou lhe passar outra solução.

Abraço.
Responder
Alan Lucena (1) (0)
24/02/2019
Boa noite professor, qual é a sequência do curso, terminei a primeira parte, onde acho a segunda parte do curso? Muito obrigado pela atenção!
Responder
Vinícius Thiengo (0) (1)
24/02/2019
Alan, tudo bem?

Seguem os links das outras duas aulas já liberadas depois desta:

-> Fragmento da Tela Sobre e Links Sociais - Android M-Commerce: https://www.thiengo.com.br/fragmento-da-tela-sobre-e-links-sociais-android-m-commerce

-> Localização com Rota GPS, E-mail e Telefones - Android M-Commerce: https://www.thiengo.com.br/localizacao-com-rota-gps-e-mail-e-telefones-android-m-commerce

Abraço.
Responder
20/02/2019
Ola Thiengo,
Não consegui compreender o porque de se usar o SelectionTracker. Todo menu precisa necessariamente implementá-lo? Ou apenas neste projeto, para atender uma regra de negócio específico deste app. Num app normal, com menu, necessariamente precisarei implementar o SelectionTracker? Pelo que entendi, é para que a opção selecionada esteja destacada ao navegar pelo app, para dizer ao usuário que você está naquela sessão (exemplo, chinelos). Poderia esclarecer melhor o uso desta API?
Responder
Vinícius Thiengo (1) (0)
21/02/2019
Janes, tudo bem?

Primeiro é importante saber que o menu gaveta do projeto tem uma configuração que não é atendida por um NavigationView ou de maneira simples por um ListView ou qualquer outro framework de lista no Android.

Foi escolhido o RecyclerView como framework de lista para o menu gaveta, pois este é o framework Android mais eficiente, mesmo que a codificação dele seja maior do que a dos demais.

Porém o RecyclerView não teve, até o momento da criação da SelectioTracker API, uma interface de seleção de itens, nós desenvolvedores tínhamos de criar todo um algoritmo para conseguir isso.

Infelizmente a SelectionTracker API não é tão simples devido a quantidade de código boilerplate necessário. Porém é a melhor maneira, em nosso domínio de problema, para manter a seleção e sincronia de itens selecionados entre os dois RecyclerViews que estão sendo utilizados no menu gaveta.

Resumo: a SelectionTracker API é apenas uma API exclusiva para permitir a seleção de um ou mais itens em RecyclerView. Mas não é sempre necessário o uso dela, principalmente quando não estamos utilizando o RecyclerView.

Janes, essa é uma API importante para programadores Android. Tenho um conteúdo completo sobre ela, incluindo vídeos, no link a seguir:

-> SelectionTracker Para Seleção de Itens no RecyclerView Android: https://www.thiengo.com.br/selectiontracker-para-selecao-de-itens-no-recyclerview-android

Não deixe de estuda-lo.

Abraço.
Responder
Alan Lucena (2) (0)
19/02/2019
Outra observação professor, na activity_main tu chama esse cara app:menu="@menu/activity_main_drawer" mas dentro dele esta as configurações de criação do app como e onde foi trocada esse xml, pois não encontrei até agora, ou eu que estou errando em algum passo que o senhor explica, desde ja agradeço por sua atenção!
Responder
Vinícius Thiengo (1) (0)
20/02/2019
Alan, tudo bem?

O arquivo activity_main_drawer.xml é um dos arquivos excluídos ao final do artigo, mas provavelmente, em vídeo, eu não realizei esse passo.

A exclusão deste arquivo, em "Removendo o que não é necessário" no artigo acima, é porque ele não mais é útil ao projeto.

Certifique-se de que o layout activity_main.xml está como no link a seguir:

https://github.com/viniciusthiengo/blueshoes-kotlin-android/blob/master/app/src/main/res/layout/activity_main.xml

Essa parte eu vou adicionar ao artigo, percebi agora que eu não coloquei ela, por isso os erros de execução.

Abraço.
Responder
Alan Lucena (2) (0)
21/02/2019
Obrigado professor por tudo que tem feito por nós, para podermos aprender algo contigo!
Responder
Alan Lucena (2) (0)
19/02/2019
Professor, tenho um erro no mainActivity.
Quando eu tiro essa declaração "initNavMenu(savedInstanceState)" do Oncreate  o aplicativo executa porém não carrega os itens corretamente no menu Gaveta os itens corretamente. E se deixo essa declaração deste método da erro. O que poderia ser? Se puder me ajudar agradeço pois ja visualizei as 21 aulas 2x e ainda não consegui encontrar o erro, ai diz que esse cara rl_header_user_not_logged não pode ser nulo em qual momento isso poderia acontecer?

java.lang.RuntimeException: Unable to start activity ComponentInfo{lucenajj.com.br.blueshoes/lucenajj.com.br.blueshoes.view.MainActivity}: java.lang.IllegalStateException: rl_header_user_not_logged must not be null
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3003)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3064)
        at android.app.ActivityThread.-wrap14(ActivityThread.java)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1659)
        at android.os.Handler.dispatchMessage(Handler.java:102)
        at android.os.Looper.loop(Looper.java:154)
        at android.app.ActivityThread.main(ActivityThread.java:6816)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1565)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1453)
     Caused by: java.lang.IllegalStateException: rl_header_user_not_logged must not be null
        at lucenajj.com.br.blueshoes.view.MainActivity.showHideNavMenuViews(MainActivity.kt:88)
        at lucenajj.com.br.blueshoes.view.MainActivity.initNavMenu(MainActivity.kt:71)
        at lucenajj.com.br.blueshoes.view.MainActivity.onCreate(MainActivity.kt:61)
        at android.app.Activity.performCreate(Activity.java:6977)
        at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1126)
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2946)
Responder
Vinícius Thiengo (1) (0)
20/02/2019
Alan, tudo bem?

Neste caso provavelmente é o layout activity_main.xml que está errado.

Certifique-se de que ele está como no link a seguir:

https://github.com/viniciusthiengo/blueshoes-kotlin-android/blob/master/app/src/main/res/layout/activity_main.xml

Abraço.
Responder
Alan Lucena (2) (0)
21/02/2019
Obrigado professor, realmente o xml main_activity estava diferente
Responder
Alan Lucena (2) (0)
21/02/2019
Muitíssimo obrigado por sua atenção professor!
Responder
Alan Lucena (1) (0)
12/02/2019
Só uma observação professor, o nome dos links estão iguais, porém o nome do arquivo no GitHub está correto, desde ja agradeço o senhor disponibilizar seu precioso tempo para nos ensinar, muito obrigado mesmo e que seu DEUS continue lhe abençoando grandiosamente!

Ícone do item Minhas configurações:
/res/drawable-mdpi/ic_settings_black_24dp.png;
/res/drawable-hdpi/ic_settings_black_24dp.png;
/res/drawable-xhdpi/ic_settings_black_24dp.png;
/res/drawable-xxhdpi/ic_settings_black_24dp.png.

Ícone do item Sair:
/res/drawable-mdpi/ic_settings_black_24dp.png;
/res/drawable-hdpi/ic_settings_black_24dp.png;
/res/drawable-xhdpi/ic_settings_black_24dp.png;
/res/drawable-xxhdpi/ic_settings_black_24dp.png.
Responder
Vinícius Thiengo (1) (0)
12/02/2019
Alan, tudo bem?

Muito obrigado, erro corrigido.

Forte abraço.
Responder
Alan Lucena (2) (0)
12/02/2019
Outro pra ti professor!
Estudando e pensando quando terei tudo isso que tu tens na mente, fico impressionado com tanto conhecimento, meus parabéns pelos dias, horas e meses de estudos até chegar onde chegou, sem dizer as noites de sono perdido, mais uma vez meus parabéns!
Responder
10/02/2019
Não haverá vídeo, somente os artigos confere?
Responder
Vinícius Thiengo (3) (0)
10/02/2019
Paulo, tudo bem?

Haverá vídeos sim. Estou editando eles.

Provavelmente amanhã estarei liberando a Play List deste segundo episódio sobre o mobile-commerce.

Abraço.
Responder
09/02/2019
Muito top, esperando o próximo artigo.
Responder
Alan Lucena (3) (0)
09/02/2019
Muito bom este artigo, de cara ja da para aprender muita coisa nele, obrigado por transmitir seus conhecimentos para nós!
Responder