Como Desenvolver a Tela de Listagem de Calçados - Android M-Commerce

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 /Como Desenvolver a Tela de Listagem de Calçados - Android M-Commerce

Como Desenvolver a Tela de Listagem de Calçados - Android M-Commerce

Vinícius Thiengo
(4508)
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ção1ª
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 continuaremos com o projeto Android mobile-commerce, desta vez desenvolvendo a tela inicial do aplicativo, tela responsável por apresentar todos os calçados do mobile-commerce.

Animação da apresentação de calçados sem filtro de categoria

Para está tela teremos como principal entidade um fragmento host, este que também servirá de base, ancestral, para os fragmentos de outras categorias de calçados dentro do aplicativo.

Antes de prosseguir, não esqueça de se inscrever ðŸ“« na lista de e-mails do Blog para receber todas as atualizações do projeto BlueShoes e de outros conteúdos de desenvolvimento Android.

A seguir os tópicos abordados em artigo:

Iniciando no projeto Android BlueShoes

Caso você esteja conhecendo o projeto BlueShoes, nosso mobile-commerce, somente agora, saiba que está é na verdade a 19ª aula.

Todas as aulas são acompanhadas de artigos e vídeos, estes que são liberados ao longo da semana em que a aula foi liberada.

É importante que os conteúdos sejam consumidos na ordem correta para que a sua versão de projeto seja construída com consistência.

Sendo assim, seguem os links das aulas anteriores a está:

É importante que você também se inscreva na ðŸ“« lista de e-mails do Blog, pois por lá, além de receber as atualizações do projeto, eu também tiro suas principais dúvidas.

Estratégia para a listagem de todos os calçados

Como estratégia, nada de novidade, vamos primeiro ao desenvolvimento do que é fácil e tem menos dependentes para depois seguirmos aos trechos mais complexos.

Logo:

  • Primeiro vamos às partes estáticas, principalmente as que já podem ser obtidas diretamente do protótipo estático;
  • Depois vamos aos trechos de códigos dinâmicos, códigos Kotlin.

Um lembrete, que certamente você não está já cansado de ver:

Não deixe de acompanhar a aula por completo, mesmo se você tiver baixado o projeto para segui-lo em sua própria instalação do Android Studio, pois é na aula que eu vou explicando passo a passo o porquê das decisões e APIs em desenvolvimento.

Protótipo estático

A seguir o protótipo estático desta parte do projeto BlueShoes, parte de lista de calçados:

 

Lista de todos os calçados

Lista de todos os calçados

 

Note que aqui vamos desenvolver a tela responsável por apresentar os calçados sem filtro de categoria, ou seja, responsável por mostrar todos os calçados do aplicativo.

Atualizando arquivos de estilo

O que puder ser atualizado somente com as informações em protótipo estático...

Calçado em listagem de calçados

... será atualizado primeiro.

Atualizando as cores

Em /res/values/colors.xml adicione os trechos de cores em destaque:

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

<color name="colorMediumYellow">#FFFFD840</color>
<color name="colorMediumRed">#FF0000</color>
</resources>

Atualizando as Strings

Em /res/values/strings.xml, adicione os trechos em destaque:

<resources>
...

<!-- AllShoesListFragment -->
<string name="all_shoes_list_frag_title">BlueShoes Calçados</string>

<string name="offer">Oferta</string>
<string name="money_sign">R$</string>
<string name="in_until">em até</string>
<string name="of">de</string>

<string name="content_desc_rate_star_1">
Primeira estrela da medição de avaliação dos compradores do tênis.
</string>
<string name="content_desc_rate_star_2">
Segunda estrela da medição de avaliação dos compradores do tênis.
</string>
<string name="content_desc_rate_star_3">
Terceira estrela da medição de avaliação dos compradores do tênis.
</string>
<string name="content_desc_rate_star_4">
Quarta estrela da medição de avaliação dos compradores do tênis.
</string>
<string name="content_desc_rate_star_5">
Quinta estrela da medição de avaliação dos compradores do tênis.
</string>
</resources>

 

Thiengo, mas até mesmo o símbolo do real, R$, precisa entrar em strings.xml?

Sim. Os arquivos de Strings serão principalmente úteis quando houver a internacionalização do aplicativo e o símbolo monetário também entra nessa "tradução".

Domínio

Finalmente chegamos às principais classes de domínio de um projeto que visa a venda de calçados por meio de um aplicativo Android.

Aqui desenvolveremos parte das classes responsáveis pelos conteúdos de calçados.

Em outras telas, que serão desenvolvidas em outras aulas, essas classes deverão ser ampliadas para atender às necessidades dessas. Até mesmo novas classes de domínio poderão ser adicionadas.

Quais classes?

Aqui vamos desenvolver o óbvio, aquilo que já consegue "guardar" o suficiente apresentado em item de lista de calçados:

Itens da lista de calçados em app

Com a imagem acima podemos ter:

  • Uma classe responsável pelos dados de marca de calçado;
  • Uma classe responsável pelos dados de preço e desconto;
  • Uma classe responsável pelos dados de avaliação do calçado (rate e comentários);
  • E uma classe responsável por conter todos os dados, incluindo objetos das possíveis classes citadas acima.

Com isso, vamos ao desenvolvimento das classes de domínio.

Marca

No pacote /domain adicione a classe Brand, responsável pelos dados de marca de calçado, com o código a seguir:

class Brand(
val label: String,
val logo: String )

 

A propriedade logo é uma String?

Sim, pois tanto para logo de marca como para imagens de galeria de calçado nós utilizaremos URLs. Todas elas estarão em servidores remotos.

Preço

Ainda no pacote /domain adicione a classe Price, responsável pelos dados de preço e de desconto. Adicione com o código abaixo:

class Price(
private val normal: Float, /* Preço normal. */
private val parcels: Int,
val hasDiscount: Boolean,
private val withDiscount: Float /* Preço com o desconto já aplicado. */
) {

/*
* Locale.GERMAN está sendo utilizado para que na
* separação das casas decimais seja utilizada a
* vírgula ao invés de ponto.
* */

fun getNormalLabel( context: Context )
= String.format(
Locale.GERMAN,
"%s %.2f",
context.getString( R.string.money_sign ),
normal
)

fun getWithDiscountLabel( context: Context )
= String.format(
Locale.GERMAN,
"%s %.2f",
context.getString( R.string.money_sign ),
withDiscount
)

fun getPercentDiscountLabel() : String {
val percent = ((normal - withDiscount) / normal) * 100

/*
* Para apresentar o caractere de percentagem, %,
* como parte do texto é preciso utilizar dois dele
* como no String.format() abaixo.
* */
return String.format(
"-%d%%",
percent.toInt()
)
}

fun getParcelsLabel( context: Context ) : String {
val priceParcel = if( hasDiscount )
withDiscount / parcels
else
normal / parcels

return String.format(
Locale.GERMAN,
"%s %dx %s %s %.2f",
context.getString( R.string.in_until ),
parcels,
context.getString( R.string.of ),
context.getString( R.string.money_sign ),
priceParcel
)
}
}

 

Note que os métodos para a correta apresentação dos dados em tela, esses métodos já foram adicionados. Nada de mais, certo?

Note também que por causa do uso de Strings estáticas em strings.xml é preciso, com frequência, utilizar a String.format() "no detalhe", incluindo um parâmetro do tipo Context para obter as Strings.

Veja que na assinatura de Price algumas propriedades foram definidas como private:

class Price(
private val normal: Float,
private val parcels: Int,
val hasDiscount: Boolean,
private val withDiscount: Float )
...

 

Isso, pois essas propriedades não serão acessadas diretamente quando fora do contexto de Price. Sendo assim nós estamos diminuindo o escopo delas e consequentemente facilitando nossas futuras manutenções em código.

Ok, Thiengo. Mas me explique: por que você utilizou Float ao invés de Double? Este último que já é o tipo padrão para dados com casas decimais.

Simples: porque Float ocupa menos espaço em memória do que o tipo Double. Essa é uma daquelas "técnicas" que não precisam de uma refatoração para serem aplicadas.

Avaliação do comprador

Agora a classe responsável por conter os dados de avaliações do calçado. Ainda em /domain adicione a classe Rate como a seguir:

class Rate(
private val stars: Float,
private val numComments: Int ) {

fun getNumCommentsLabel()
= String.format(
"(%d)",
numComments
)
}

Tênis

Por fim a classe container de todos os dados presentes em um item de lista de calçados. Em /domain adicione a classe Shoes com o código abaixo:

class Shoes(
val model: String,
val mainImg: String,
val brand: Brand,
val price: Price,
val rate: Rate )

 

Thiengo, você não acha que estão faltando inúmeras propriedades nesta classe? Os tamanhos disponíveis do calçado, por exemplo.

Sim. Não somente acho como tenho certeza. Mas, como informado anteriormente, aqui adicionaremos somente o necessário para a apresentação de cada item.

E, acredite, mesmo que o projeto estivesse em estágio avançado, muito provavelmente nós iríamos carregar do back-end Web somente o necessário em lista.

O restante dos dados seriam carregados somente quando o usuário entrasse na área de detalhes do calçado, isso, pois o RecyclerView na verdade somente reaproveita os componentes visuais, a lista de objetos vinculada ao adapter do RecyclerView continua "cheia" e ocupando espaço de memória, sem reaproveitamento de nada.

Banco de dados auxiliar

Como já fizemos para outras telas do projeto, telas com uso de listas, aqui também trabalharemos com uma base de dados mock, dados simulados. Somente para facilitar os testes de interface gráfica.

No pacote /data adicione a classe AllShoesDataBase com o código a seguir:

class AllShoesDataBase {

companion object{

fun getItems()
= listOf(
Shoes(
"Tênis VR Caminhada Confortável Detalhes Couro Masculino - Preto",
"https://static.netshoes.com.br/produtos/tenis-vr-caminhada-confortavel-detalhes-couro-masculino/06/E74-0413-006/E74-0413-006_zoom1.jpg",
Brand(
"Adidas",
"https://cdn.awsli.com.br/400x300/1062/1062636/logo/1a09cccb3a.png"
),
Price(
119.90F,
10,
false,
0F
),
Rate(
3.5F,
193
)
),
Shoes(
"Chinelo Oakley Malibu Slide Masculino - Vermelho",
"https://static.netshoes.com.br/produtos/chinelo-oakley-malibu-slide-masculino/16/D63-5200-016/D63-5200-016_zoom1.jpg",
Brand(
"Oakley",
"https://upload.wikimedia.org/wikipedia/commons/thumb/8/88/Oakley_logo.svg/641px-Oakley_logo.svg.png"
),
Price(
149.99F,
2,
true,
84.99F
),
Rate(
4.5F,
37
)
),
Shoes(
"Chuteira Campo Nike Mercurial Superfly 6 Club CR7 MG - Branco e Preto",
"https://static.netshoes.com.br/produtos/chuteira-campo-nike-mercurial-superfly-6-club-cr7-mg/28/D12-9247-028/D12-9247-028_zoom1.jpg",
Brand(
"Nike",
"https://upload.wikimedia.org/wikipedia/commons/thumb/a/a6/Logo_NIKE.svg/1200px-Logo_NIKE.svg.png"
),
Price(
349.99F,
5,
true,
229.99F
),
Rate(
4.5F,
6
)
),
Shoes(
"Tênis Olympikus Flower 415 Feminino - Rosa",
"https://static.netshoes.com.br/produtos/tenis-olympikus-flower-415-feminino/18/D22-1131-018/D22-1131-018_zoom1.jpg",
Brand(
"Olympikus",
"https://logodownload.org/wp-content/uploads/2017/06/olympikus-logo.png"
),
Price(
159.99F,
3,
true,
119.99F
),
Rate(
4.5F,
339
)
),
Shoes(
"Tênis Nike Shox Nz Eu Masculino - Preto",
"https://static.netshoes.com.br/produtos/tenis-nike-shox-nz-eu-masculino/14/D12-9970-014/D12-9970-014_zoom1.jpg",
Brand(
"Adidas",
"https://9d41bboy87-flywheel.netdna-ssl.com/outlets-at-legends/wp-content/uploads/sites/11/2019/07/Adidas_Logo.png"
),
Price(
699.99F,
10,
true,
439.99F
),
Rate(
4.5F,
820
)
),
Shoes(
"Tênis Nike Revolution 4 Feminino - Preto e Branco",
"https://static.netshoes.com.br/produtos/tenis-nike-revolution-4-feminino/26/D12-9120-026/D12-9120-026_zoom1.jpg",
Brand(
"Nike",
"https://upload.wikimedia.org/wikipedia/commons/thumb/a/a6/Logo_NIKE.svg/1200px-Logo_NIKE.svg.png"
),
Price(
229.99F,
4,
true,
169.99F
),
Rate(
5F,
889
)
),
Shoes(
"Tênis Reebok Crossfit Nano 9 Masculino - Azul e Preto",
"https://static.netshoes.com.br/produtos/tenis-reebok-crossfit-nano-9-masculino/08/D19-3259-108/D19-3259-108_zoom1.jpg",
Brand(
"Reebok",
"https://seeklogo.com/images/R/reebok-logo-B8CC638372-seeklogo.com.png"
),
Price(
599.99F,
10,
false,
0F
),
Rate(
4.5F,
13
)
),
Shoes(
"Tênis Nike Metcon Sport Masculino - Azul e amarelo",
"https://static.netshoes.com.br/produtos/tenis-nike-metcon-sport-masculino/76/HZM-1277-076/HZM-1277-076_zoom1.jpg",
Brand(
"Nike",
"https://upload.wikimedia.org/wikipedia/commons/thumb/a/a6/Logo_NIKE.svg/1200px-Logo_NIKE.svg.png"
),
Price(
449.99F,
10,
false,
0F
),
Rate(
5F,
6
)
)
)
}
}

 

A classe acima tem na verdade um spoiler sobre como os dados de listagem de calçados serão entregues a partir do back-end Web.

Note que como estamos trabalhando com Float ao invés de Double é necessário deixar explicito em código que o valor é um Float, colocando um F ao lado do número.

Carregamento remoto de imagens

Thiengo, e esse carregamento remoto de imagens, ainda estou "grilado" com isso. Será mesmo necessário o carregamento remoto até para a logo da marca do calçado?

Sim, para a logo de marca também.

É importante lembrar que os dados binários de imagem, digo, imagens de calçados, de logo de marca e de foto de perfil de usuário serão todos hospedados em servidor remoto para que os usuários tenham acesso a eles independente do aparelho Android em uso.

No caso das imagens de logo de marca, a principio elas também ficarão em servidor remoto, mesmo sabendo que este número de imagens é bem limitado.

Com essas imagens de logo também presentes em servidor remoto o administrador do mobile-commerce poderá, via dashboard Web, adicionar ou modificar marcas de calçados a qualquer momento sem necessidade de enviar uma nova versão de aplicativo à Google Play Store.

E sim. Se você me acompanha já a algum tempo deve saber que já faz alguns anos que venho utilizando somente a Picasso API quando o assunto é: carregamento remoto de imagens. Isso, pois para mim é a API mais simples e completa para esta tarefa.

Preparando a Picasso API

Falando em Picasso API e já sabendo que as imagens de calçados e de logo de marcas serão carregadas de servidores remotos (por enquanto estamos utilizando imagens remotas aleatórias na Internet), já podemos então trabalhar a configuração desta API em projeto.

No Gradle Nível de Aplicativo, build.gradle (Module: app), adicione a referência em destaque:

...
dependencies {
...

/* Picasso API - Carregamento e cache de imagens */
implementation 'com.squareup.picasso:picasso:2.71828'
}
...

 

E sincronize o projeto.

Não esqueça de colocar, caso ainda não tenha em sua versão de projeto, a permissão de Internet no AndroidManifest.xml:

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

...
<uses-permission android:name="android.permission.INTERNET"/>

...
</manifest>

Listagem de calçados

Agora podemos prosseguir para a codificação do fragmento de listagem de calçados. Fragmento e componentes complementares.

Vamos iniciar com as entidades de adapter e então vamos ao fragmento em si.

Layout de item

O layout de item de lista será simples, mas provavelmente utilizando um ViewGroup diferente do aguardado por você.

Em /res/layout adicione o layout shoes_item.xml com a estrutura XML a seguir:

<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:card_view="http://schemas.android.com/apk/res-auto"
android:layout_marginTop="6dp"
android:layout_marginBottom="6dp"
android:layout_marginLeft="6dp"
android:layout_marginRight="6dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
card_view:cardCornerRadius="2dp">

<RelativeLayout
android:padding="8dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">

<ImageView
android:id="@+id/iv_model"
android:layout_width="match_parent"
android:layout_height="108dp"
android:layout_alignParentTop="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:scaleType="fitCenter"/>

<LinearLayout
android:id="@+id/ll_discount"
android:layout_width="44dp"
android:layout_height="44dp"
android:layout_alignParentTop="true"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:orientation="vertical"
android:gravity="center"
android:background="@drawable/bg_discount">

<TextView
android:id="@+id/tv_discount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@android:color/white"
android:textSize="14sp"
android:textStyle="bold"/>

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@android:color/white"
android:layout_marginTop="-4dp"
android:textSize="8sp"
android:text="@string/offer"/>
</LinearLayout>

<LinearLayout
android:id="@+id/ll_first_line_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/iv_model"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:orientation="horizontal">

<TextView
android:id="@+id/tv_model"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="13sp"
android:maxLines="2"
android:ellipsize="end"/>

<View
android:layout_width="1dp"
android:layout_height="match_parent"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:background="@color/colorNavItemSelected"/>

<ImageView
android:id="@+id/iv_brand"
android:layout_width="28dp"
android:layout_height="match_parent"
android:tint="@android:color/black"
android:scaleType="fitCenter"/>
</LinearLayout>

<View
android:id="@+id/v_first_horizontal_line"
android:layout_width="match_parent"
android:layout_height="0.8dp"
android:layout_below="@+id/ll_first_line_container"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_marginTop="2dp"
android:layout_marginBottom="2dp"
android:background="@color/colorNavItemSelected"/>

<TextView
android:id="@+id/tv_price_current"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/v_first_horizontal_line"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:textStyle="italic"
android:textSize="14sp"
android:textColor="@color/colorLightBlue"/>

<TextView
android:id="@+id/tv_price_without_discount"
android:visibility="gone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/v_first_horizontal_line"
android:layout_toLeftOf="@+id/tv_price_current"
android:layout_toStartOf="@+id/tv_price_current"
android:layout_marginTop="1dp"
android:layout_marginRight="6dp"
android:layout_marginEnd="6dp"
android:background="@drawable/tv_strike_through"
android:textSize="12sp"
android:textColor="@color/colorMediumRed"/>

<TextView
android:id="@+id/tv_price_parcels"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/tv_price_current"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:textSize="7sp"/>

<ImageView
android:id="@+id/iv_rate_star_1"
style="@style/ImageViewRateStarItemList"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:contentDescription="@string/content_desc_rate_star_1"/>

<ImageView
android:id="@+id/iv_rate_star_2"
style="@style/ImageViewRateStarItemList"
android:layout_toRightOf="@+id/iv_rate_star_1"
android:layout_toEndOf="@+id/iv_rate_star_1"
android:contentDescription="@string/content_desc_rate_star_2"/>

<ImageView
android:id="@+id/iv_rate_star_3"
style="@style/ImageViewRateStarItemList"
android:layout_toRightOf="@+id/iv_rate_star_2"
android:layout_toEndOf="@+id/iv_rate_star_2"
android:contentDescription="@string/content_desc_rate_star_3"/>

<ImageView
android:id="@+id/iv_rate_star_4"
style="@style/ImageViewRateStarItemList"
android:layout_toRightOf="@+id/iv_rate_star_3"
android:layout_toEndOf="@+id/iv_rate_star_3"
android:contentDescription="@string/content_desc_rate_star_4"/>

<ImageView
android:id="@+id/iv_rate_star_5"
style="@style/ImageViewRateStarItemList"
android:layout_toRightOf="@+id/iv_rate_star_4"
android:layout_toEndOf="@+id/iv_rate_star_4"
android:layout_marginRight="2dp"
android:layout_marginEnd="2dp"
android:contentDescription="@string/content_desc_rate_star_5"/>

<TextView
android:id="@+id/tv_num_rates"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_toRightOf="@+id/iv_rate_star_5"
android:layout_toEndOf="@+id/iv_rate_star_5"
android:layout_marginTop="6dp"
android:textSize="8sp"/>
</RelativeLayout>
</androidx.cardview.widget.CardView>

 

Fique tranquilo, o estilo ImageViewRateStarItemList e o drawable tv_strike_through.xml serão trabalhados em seções a seguir.

Abaixo o diagrama do layout anterior:

Diagrama do layout shoes_item.xml

Background da área de porcentagem de desconto

Você deve ter notado que há uma imagem de background no primeiro LinearLayout do layout, certo?

...
<LinearLayout
android:id="@+id/ll_discount"
...
android:background="@drawable/bg_discount">
...

 

Essa imagem representa o background vermelho a seguir:

Área de porcentagem de desconto no tênis

Ainda temos de adiciona-la em projeto. Realize o download dela diretamente do repositório oficial:

Background da área de porcentagem de desconto

Depois coloque cada imagem em seu respectivo folder drawable.

Estilo das estrelas de avaliação

Como as estrelas de avaliação dos tênis têm um design em comum:

Estrelas de avaliação de tênis

Então é prudente colocar esse design em um estilo específico em /res/values/styles.xml:

<resources>
...

<style name="ImageViewRateStarItemList">
<item name="android:layout_width">10dp</item>
<item name="android:layout_height">10dp</item>
<item name="android:layout_alignParentBottom">true</item>
<item name="android:scaleType">fitCenter</item>
<item name="android:tint">@color/colorMediumYellow</item>
</style>
</resources>

 

Adicione o estilo acima em sua versão de projeto, adicione a parte em destaque.

Drawable de linha atravessada

Para que tenhamos o design de Strike Through, linha atravessada em texto, no antigo preço de tênis:

Preço do tênis com linha atravessada (preço antigo)

Vamos utilizar um drawable.

Em /res/drawable adicione tv_strike_through.xml com o código estático a seguir:

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

<item>
<shape android:shape="rectangle">
<solid android:color="@android:color/transparent"/>
</shape>
</item>

<item android:top="3dp">
<shape android:shape="line">
<stroke
android:width="0.8dp"
android:color="@color/colorMediumRed"/>
</shape>
</item>
</layer-list>

 

Neste ponto do projeto vou seguramente assumir que você entende todos os componentes XML acima, certo? Caso não, deixe nos comentários as suas dúvidas ou, melhor ainda, envie diretamente ao e-mail oficial do Blog e canal.

Por que um drawable e não uma SpannableString?

É, eu sei que você está com essa dúvida: Por que um drawable e não uma SpannableString?

Bom, com um drawable definido em layout, temos a certeza de que ele será reaproveitado pelos algoritmos ViewHolder do RecyclerView.

Caso nós estivéssemos definindo a linha atravessada via SpannableString, em código teríamos de criar, a todo momento e sem reaproveitamento de objetos, um novo Spannable, como a seguir:

...
val spannable = SpannableString(
String.format(
Locale.GERMAN,
"%s %.2f",
context.getString( R.string.money_sign ),
withDiscount
)
)

spannable.setSpan(
StrikethroughSpan(),
0,
spannable.length,
Spanned.SPAN_INCLUSIVE_INCLUSIVE
)
...

 

Ou seja, manter o uso de tv_strike_through.xml, em nosso caso de item de lista, consome menos memória disponível ao aplicativo do que a opção com SpannableString.

Detalhe: neste caso a "melhoria" foi óbvia a mim que já conheço as APIs, mas é provável, caso você esteja em seu primeiro projeto Android, que a opção com SpannableString fosse a melhor, não teria problemas. Ao longo do desenvolvimento você certamente melhoraria o seu projeto.

Cadê o ConstraintLayout?

Sim, nosso layout de item não faz uso de um ConstraintLayout para evitar o "aninhamento", como o a seguir:

...
<LinearLayout
android:id="@+id/ll_first_line_container"
...>

<TextView
android:id="@+id/tv_model"
.../>

<View
.../>

<ImageView
android:id="@+id/iv_brand"
.../>
</LinearLayout>
...

 

Incluindo o uso de android:layout_weight, algo que a própria documentação pede para evitar quando em layout de item de framework de lista.

Vamos às minhas razões (temporárias):

  • A documentação realmente recomenda isso, não utilizar o atributo android:layout_weight em layout de item de lista. Porém esse texto da documentação é da época em que o RecyclerView ainda não existia como uma das opções de framework de lista, somente ListView e GridView;
  • Eu cheguei a configurar o layout de item com o ConstraintLayout no lugar do RelativeLayout, porém tive sérios problemas de posicionamento e preenchimento de componentes visuais, algo que vai exigir de mim mais tempo dedicado a documentação do ConstraintLayout para em algum momento, principalmente se houver problemas de OutOfMemoryException em lista, utiliza-lo com eficácia.

É isso. Vamos, até onde for possível, apostar nossas fichas no reaproveitamento de componentes visuais aplicado internamente pelo RecyclerView.

Classe adaptadora

Antes de partirmos para os algoritmos da classe adaptadora de itens, vamos primeiro criar um novo pacote, pacote que será responsável por conter as classes da camada de visualização (atividades, fragmentos e adapters, ao menos) responsáveis somente por calçados.

Em /view crie o pacote /shoes. Ao final teremos a seguinte estrutura física de pacotes filhos dentro de /view:

Estrutura física do pacote /view

No novo pacote crie o adapter AllShoesListAdapter com o código a seguir:

class AllShoesListAdapter(
private val shoesList: List<Shoes>
) :
RecyclerView.Adapter<AllShoesListAdapter.ViewHolder>() {

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

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

return ViewHolder( layout )
}

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

holder.setData( shoesList[ position ] )
}

override fun getItemCount() = shoesList.size

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

private val context : Context

private val ivModel : ImageView
private val tvModel : TextView

private val ivBrand : ImageView

private val llDiscount : LinearLayout
private val tvDiscount : TextView

private val tvPriceCurrent : TextView
private val tvPriceWithoutDiscount : TextView
private val tvPriceParcels : TextView

private val ivRateStar1 : ImageView
private val ivRateStar2 : ImageView
private val ivRateStar3 : ImageView
private val ivRateStar4 : ImageView
private val ivRateStar5 : ImageView
private val tvNumRates : TextView

init{
context = itemView.context

ivModel = itemView.findViewById( R.id.iv_model )
tvModel = itemView.findViewById( R.id.tv_model )

ivBrand = itemView.findViewById( R.id.iv_brand )

llDiscount = itemView.findViewById( R.id.ll_discount )
tvDiscount = itemView.findViewById( R.id.tv_discount )
tvPriceCurrent = itemView.findViewById( R.id.tv_price_current )
tvPriceWithoutDiscount = itemView.findViewById( R.id.tv_price_without_discount )
tvPriceParcels = itemView.findViewById( R.id.tv_price_parcels )

ivRateStar1 = itemView.findViewById( R.id.iv_rate_star_1 )
ivRateStar2 = itemView.findViewById( R.id.iv_rate_star_2 )
ivRateStar3 = itemView.findViewById( R.id.iv_rate_star_3 )
ivRateStar4 = itemView.findViewById( R.id.iv_rate_star_4 )
ivRateStar5 = itemView.findViewById( R.id.iv_rate_star_5 )

tvNumRates = itemView.findViewById( R.id.tv_num_rates )
}

fun setData( shoes: Shoes ){

Picasso.get()
.load( shoes.mainImg )
.into( ivModel )
ivModel.contentDescription = shoes.model
tvModel.text = shoes.model

Picasso.get()
.load( shoes.brand.logo )
.into( ivBrand )
ivBrand.contentDescription = shoes.brand.label
}
}
}

 

Calma! Os algoritmos de preço e de avaliação, rate, de tênis vão entrar no adapter, logo nas seções a seguir.

Antes, um destaque para a simplicidade da Picasso API, já em uso no método setData().

Algoritmo de preço

Para a listagem de preço e de desconto, quando presente, dentro do ViewHolder de AllShoesListAdapter adicione o método setPrice() como a seguir:

...
private fun setPrice( shoes: Shoes ){

if( shoes.price.hasDiscount ){
llDiscount.visibility = View.VISIBLE
tvPriceWithoutDiscount.visibility = View.VISIBLE

tvDiscount.text = shoes.price.getPercentDiscountLabel()
tvPriceCurrent.text = shoes.price.getWithDiscountLabel( context )
tvPriceWithoutDiscount.text = shoes.price.getNormalLabel( context )
}
else{
llDiscount.visibility = View.GONE
tvPriceWithoutDiscount.visibility = View.GONE

tvPriceCurrent.text = shoes.price.getNormalLabel( context )
}

tvPriceParcels.text = shoes.price.getParcelsLabel( context )
}
...

 

Agora, em setData(), adicione o trecho em destaque:

...
fun setData( shoes: Shoes ){
...

setPrice( shoes )
}
...

 

Desta forma o preço e o desconto já serão apresentados como esperado, sem erro de valor e posicionamento.

Algoritmo de avaliação

Antes de partirmos para o algoritmo de avaliação de tênis, vamos primeiro adicionar os ícones de estrela necessários a esse algoritmo.

Na verdade eu já os tenho adicionado em projeto. Você pode baixa-los direto do repositório oficial do Android BlueShoes. Links a seguir.

Primeiro a estrela completa:

Ícone de estrela completa

Agora a estrela pela metade:

Ícone de estrela pela metade

Por fim a estrela vazia:

Ícone de estrela vazia

Coloque cada versão de imagem em seu respectivo folder drawable.

Agora em Rate vamos adicionar o condicional que coloca em ImageView a imagem correta de estrela:

...
fun getStarResource( starPosition : Int )
= if( starPosition <= floor( stars ).toInt() )
R.drawable.ic_star_filled
else if( starPosition == ceil( stars ).toInt() )
R.drawable.ic_star_half_empty
else
R.drawable.ic_star_empty
...

 

floor() e ceil() são métodos nativos da biblioteca de matemática do Kotlin.

Assim, no ViewHolder de AllShoesListAdapter, adicione o método setRate() com o código a seguir:

...
private fun setRate( rate: Rate ){

tvNumRates.text = rate.getNumCommentsLabel()

ivRateStar1.setImageResource( rate.getStarResource( 1 ) )
ivRateStar2.setImageResource( rate.getStarResource( 2 ) )
ivRateStar3.setImageResource( rate.getStarResource( 3 ) )
ivRateStar4.setImageResource( rate.getStarResource( 4 ) )
ivRateStar5.setImageResource( rate.getStarResource( 5 ) )
}
...

 

Agora, em setData() do mesmo ViewHolder, adicione o trecho em destaque:

...
fun setData( shoes: Shoes ){
...

setRate( shoes.rate )
}
...

 

Adapter finalizado!

Fragmento de todos os calçados

Esse fragmento é simples, tem muito código similar ao que já desenvolvemos em projeto, digo, desenvolvemos em fragmentos com listas.

O ponto de destaque aqui é que utilizaremos como LayoutManager um GridLayoutManager ao invés de um LinearLayoutManager.

Layout

Primeiro vamos ao simples layout do fragmento de listagem de todos os calçados, layout que por sinal dispensa o uso de diagrama.

Em /res/layout adicione o layout fragment_all_shoes_list.xml com o código a seguir:

<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/rv_shoes"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="6dp"
android:scrollbars="vertical"/>

Classe

Assim o fragmento AllShoesListFragment. Adicione este fragmento no novo pacote /shoes, com o código a seguir:

class AllShoesListFragment : Fragment() {

companion object{
const val GRID_COLUMNS = 2
}

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

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

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

initItems()
}

private fun initItems(){
rv_shoes.setHasFixedSize( false )

val layoutManager = GridLayoutManager(
activity,
GRID_COLUMNS,
RecyclerView.VERTICAL,
false
)
rv_shoes.layoutManager = layoutManager

val adapter = AllShoesListAdapter( AllShoesDataBase.getItems() )
rv_shoes.adapter = adapter
}

override fun onResume() {
super.onResume()

(activity as MainActivity)
.updateToolbarTitleInFragment( R.string.all_shoes_list_frag_title )
}
}

 

Agora são necessárias algumas mínimas atualizações na atividade principal do projeto.

Atualizando a atividade principal

Em getFragment(), na MainActivity, coloque o código em destaque como a seguir:

...
private fun getFragment( fragId: Long ) : Fragment {

return when( fragId ){
R.id.item_about.toLong() -> AboutFragment()
R.id.item_contact.toLong() -> ContactFragment()
R.id.item_privacy_policy.toLong() -> PrivacyPolicyFragment()
else -> AllShoesListFragment()
}
}
...

 

Ainda na MainActivity, porém no método initFragment(), atualize o trecho em destaque:

...
private fun initFragment(){
...
if( fragment == null ){

...
if( fragId == 0 ){
fragId = R.id.item_all_shoes
}

...
}

...
}
...

 

So, let's test it.

Testes e resultados

Em sua instalação do Android Studio, acesse:

  • Menu de topo;
  • Clique em "Build";
  • Clique em "Rebuid project";
  • Ao final do rebuild execute o app em seu emulador, ou aparelho de testes, Android.

Executando o aplicativo e mantendo-o na vertical, portrait, temos:

Animação da listagem de tênis na vertical, portrait

Executando o aplicativo e colocando-o na horizontal, landscape, temos:

Animação da listagem de tênis na horizontal, landscape

Com isso finalizamos a primeira parte de desenvolvimento da tela de apresentação de calçados, apresentação sem filtro de categoria.

Ainda temos bastante coisa a desenvolver nesta tela, trechos que vão entrar em aulas posteriores.

Antes de prosseguir, não esqueça de se inscrever na ðŸ“« lista de e-mails do Blog para receber semanalmente as aulas do projeto Android BlueShoes e ainda mais conteúdos exclusivos.

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

Vídeos

A seguir os vídeos com o passo a passo do desenvolvimento do fragmento de listagem de calçados:

O código do projeto também pode ser acessado no GitHub dele em: https://github.com/viniciusthiengo/blueshoes-kotlin-android.

Conclusão

Os códigos desenvolvidos nesta aula do projeto serão a base de todas as outras telas de listagem de calçados, telas onde o filtro de categoria será aplicado.

Um outro ponto de destaque: nem sempre uma SpannableString, apesar da facilidade, é a melhor opção. Em nosso caso, para layout de item de lista, um drawable, para a aplicação de um design específico, foi a melhor escolha.

Caso você tenha dúvidas no projeto, principalmente sobre esta 19ª aula, não deixe de comentar abaixo que logo eu lhe respondo.

Curtiu o conteúdo? Não esqueça de compartilha-lo. E, por fim, de se inscrever na 📩 lista de e-mails, respondo às suas dúvidas também por lá.

Abraço.

Fontes

Android: Strike-through a TextView

Android GridLayoutManager Example

How do items of a LayerList drawable scale to fit the container View? - Resposta de Christopher Boyd

String.format uses comma instead of point - Resposta de pablisco

UnknownFormatConversionException is caused by symbol '%' in String.format() - Resposta de Reimeus

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

Construindo a Política de Privacidade de Seu Aplicativo Android [Agora Obrigatório]Construindo a Política de Privacidade de Seu Aplicativo Android [Agora Obrigatório]Android
PDF no AndroidPDF no AndroidAndroid
Freelancer AndroidFreelancer AndroidAndroid
BottomNavigationView Android, Como e Quando UtilizarBottomNavigationView Android, Como e Quando UtilizarAndroid

Compartilhar

Comentários Facebook

Comentários Blog

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