PhotoView Android Para a Completa Implementação de Zoom
(6800) (10)
CategoriasAndroid, Design, Protótipo
AutorVinícius Thiengo
Vídeo aulas186
Tempo15 horas
ExercíciosSim
CertificadoSim
CategoriaEngenharia de Software
Autor(es)Vaughn Vernon
EditoraAlta Books
Edição1ª
Ano2024
Páginas160
Tudo bem?
Neste artigo vamos ao estudo, passo a passo, da PhotoView Android, API que nos permiti colocar a funcionalidade completa de zoom nos trechos de carregamento de imagem.
Para aqueles desenvolvedores Android de primeira viagem, mesmo a funcionalidade de zoom sendo, a princípio, algo já esperado como característica nativa do Android, não é. Felizmente a PhotoView API facilita a adição de zoom ao app.
Depois da apresentação da API vamos a um exemplo real de app que se beneficiará da funcionalidade de zoom, mais precisamente: vamos melhorar um aplicativo de vendas de carros.
Neste artigo o termo "API" será utilizado como sinônimo de "biblioteca" (library).
Antes de prosseguir, não esqueça de se inscrever 📩 na lista de emails do Blog para receber os conteúdos exclusivos e em primeira mão.
A seguir os tópicos que estaremos estudando:
- Android PhotoView:
- Instalação da API;
- Carregamento de imagem com PhotoView;
- Carregamento remoto de imagem - Picasso API;
- Trabalhando as escalas de zoom;
- Listener de atualização de escala;
- Verificação e duração de animação;
- Rotação da imagem;
- Listener de toque em imagem;
- Listener de toque fora da imagem;
- Listener de toque na PhotoView;
- Listener de duplo toque, único toque e duplo toque seguido de ação;
- Ouvindo ao evento de drag;
- Ouvindo a atualização da matriz da imagem;
- Hackcode para ViewGroups problemáticos;
- Pontos negativos;
- Pontos positivos;
- Considerações finais.
- Projeto Android:
- Colocando a funcionalidade de zoom nas imagens de álbum:
- Slides;
- Vídeos;
- Conclusão;
- Fontes.
Android PhotoView
A PhotoView API é uma daquelas APIs simples, mas que atendem a vários domínios de problema, exatamente como a Picasso API e o Retrofit, por exemplo.
A diferença da PhotoView para com as APIs citadas anteriormente é que ela é ainda mais simples de ser incorporada a projetos Android e foi desenvolvida por um dos engenheiros do Google Android, Chris Banes.
A PhotoView tem suporte a partir do Android 4, Ice Cream Sandwich. E apesar de oferecer algumas características extras, como a rotação de imagem, a proposta principal, zoom em imagem, é muito bem atendida, mesmo com o carregamento de imagens remotas.
Instalação da API
A API está presente no repositório JVM Jitpack, logo, primeiro temos de adicionar esse repositório no Gradle Project Level, ou build.gradle (Project: NomeAplicativo):
...
allprojects {
repositories {
...
maven { url "https://jitpack.io" }
}
}
...
Depois é adicionar a referência a API no Gradle App Level, ou build.gradle (Module: app):
...
dependencies {
implementation 'com.github.chrisbanes:PhotoView:2.1.4'
}
...
Na época da construção deste artigo a versão mais atual e estável da API era a 2.1.4. Escolha sempre por utilizar a versão mais atual e estável.
Carregamento de imagem com PhotoView
A principal bandeira que faz com que a PhotoView API tenha mais de 13k estrelas no GitHub (isso é um forte indício de que a API tem alta qualidade) é a facilidade de uso. Veja o código de exemplo:
...
<com.github.chrisbanes.photoview.PhotoView
android:id="@+id/pv_image"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scaleType="fitCenter"
android:src="@drawable/painter_and_model" />
...
A PhotoView é uma implementação mais robusta do ImageView, ela herda desta View. Logo, podemos utilizar todos os atributos possíveis no ImageView.
Executando o projeto Android com o trecho de código anterior, temos:
O acesso a PhotoView via código também é simples, utilizando o método findViewById() ou então o ID da View seguindo a sintaxe do plugin kotlin-android-extensions:
...
pv_image.setImageResource( R.drawable.painter_and_model )
...
Carregamento remoto de imagem - Picasso API
A princípio a única API de carregamento remoto de imagem que não tem suporte junto a PhotoView é a API Fresco, para driblar esta limitação, caso você realmente esteja em um projeto onde a Fresco API seja indispensável, a documentação da PhotoView indica a API PhotoDraweeView.
As outras APIs de carregamento remoto funcionam sem problemas junto a PhotoView, ao menos as principais. A mais comum para esse tipo de tarefa é a Picasso.
A seguir um XML PhotoView ainda sem uma imagem referenciada:
...
<com.github.chrisbanes.photoview.PhotoView
android:id="@+id/pv_image"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scaleType="fitCenter" />
...
Agora a referência a Picasso API direto do Gradle App Level, build.gradle (Module: app):
...
dependencies {
...
implementation 'com.github.chrisbanes:PhotoView:2.1.4'
}
...
Então a permissão de Internet no AndroidManifest.xml:
<?xml version="1.0" encoding="utf-8"?>
<manifest ...>
<uses-permission android:name="android.permission.INTERNET" />
...
</manifest>
E assim o código de carregamento de imagem via Picasso API:
...
Picasso
.get()
.load( "https://i.pinimg.com/originals/b3/a1/f3/b3a1f3ebee49af3b76feb6e95f0c9e04.jpg" )
.into( pv_image )
...
Executando o projeto com os códigos anteriormente apresentados, temos:
Trabalhando as escalas de zoom
Se o domínio de problema do aplicativo em desenvolvimento exigir que as três escalas de zoom padrão sejam trabalhadas em outros valores, você consegue isso com as seguintes propriedades:
- minimumScale: defini / obtém a escala mínima de zoom. O valor padrão é 1.0F, valor que representa a escala inicial, onde o zoom ainda não foi aplicado.
- mediumScale: defini / obtém a segunda escala de zoom. O valor padrão é 1.75F;
- maximumScale: defini / obtém a terceira escala de zoom. O valor padrão é 3.0F.
Agora um PhotoView de exemplo:
...
<com.github.chrisbanes.photoview.PhotoView
android:id="@+id/pv_image"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scaleType="fitCenter"
android:src="@drawable/les_femmes" />
...
Então o código de alteração de escalas de zoom:
...
pv_image.minimumScale = 0.5F
pv_image.mediumScale = 1.0F
pv_image.maximumScale = 5.75F
...
Executando o projeto com o código anterior, temos:
O método setScaleLevels() permiti que as três escalas sejam atualizadas em uma única invocação.
Caso seja necessária a atualização da escala de zoom atual em imagem, utilize:
- scale: propriedade que permiti obter / atualizar a escala atual de zoom no PhotoView alvo. Aqui a atualização vai gerar uma mudança brusca de escala de zoom. O valor aceito / retornado é do tipo Float;
- setScale( escala_em_float, animacao_em_boolean ): método que permite definir a nova escala de zoom no PhotoView alvo, mas aqui é possível, em segundo argumento, informar se a animação de mudança de escala deve ou não ser aplicada. O valor true indica que sim, false indica que não.
Segue código de exemplo de atualização de escala de zoom em imagem:
...
Thread{
kotlin.run {
/*
* Aplicando um delay de 2 segundos para dar tempo do
* carregamento da PhotoView em tela.
* */
SystemClock.sleep( 2000 )
runOnUiThread {
/*
* Trabalhar a propriedade scale, ou o método setScale(),
* somente tem efeito depois que a PhotoView já está
* renderizada em tela.
* */
/* pv_image.scale = 5.75F */
pv_image.setScale( 5.75F, true )
}
}
}.start()
...
Como informado em comentário do código anterior: scale e setScale() somente têm efeito depois que a PhotoView alvo já está renderizada em tela, por isso o código de exemplo é acompanhado de uma thread e um SystemClock.sleep().
Importante: o valor definido em scale ou em setScale() deve estar no range [minimumScale, maximumScale], caso contrário uma IllegalArgumentException será gerada.
Executando o projeto com o trecho de código anterior, temos:
Listener de atualização de escala
Para ouvir a cada mudança de fator de escala basta implementar a Interface OnScaleChangedListener como no código a seguir:
class MainActivity : AppCompatActivity(),
OnScaleChangedListener {
override fun onCreate( savedInstanceState: Bundle? ) {
super.onCreate( savedInstanceState )
setContentView( R.layout.activity_main )
pv_image.setOnScaleChangeListener( this )
}
/*
* Método invocado a cada mudança de fator de escala (scaleFactor)
* que ocorre na imagem, mudança de zoom. focusX e focusY
* representam as coordenadas de onde houve o touch para atualização
* de zoom, caso o zoom seja via programação, esses parâmetros ficam
* sempre com os mesmos valores.
* */
override fun onScaleChange(
scaleFactor: Float,
focusX: Float, /* Coordenada X do toque / clique. */
focusY: Float /* Coordenada Y do toque / clique. */
) {
/* TODO */
}
}
Como informado em comentário, focusX e focusY somente terão a mudança de valor se o acionamento do zoom for em tela, em qualquer parte do PhotoView, não necessariamente na parte onde a imagem aparece.
Verificação e duração de animação
Para verificar ou então atualizar o status de zoom na PhotoView, utilize a propriedade isZoomable:
...
if( pv_image.isZoomable ){
pv_image.isZoomable = false
}
...
Para trabalhar o tempo de duração da animação de zoom, utilize o método setZoomTransitionDuration(), veja a seguir:
...
/*
* Definindo que a animação de zoom deverá ocorrer em 4 segundos
* (4000 milissegundos).
* */
pv_image.setZoomTransitionDuration( 4000 )
...
Com a PhotoView de teste a seguir:
...
<com.github.chrisbanes.photoview.PhotoView
android:id="@+id/pv_image"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scaleType="fitCenter"
android:src="@drawable/acrobat" />
...
Podemos executar o código com delay de quatro segundos em animação de zoom:
Rotação da imagem
Por algum motivo foi adicionada a API a funcionalidade de rotação de acordo com o grau definido. São dois métodos passíveis de serem utilizados: setRotationBy() e setRotationTo().
Acredite, ambos os métodos aguardam o mesmo tipo de argumento, graus de rotação em Float, e têm o mesmo código interno.
Os métodos de rotação seguem a mesma regra de negócio que a propriedade e método de mudança de escala de zoom: somente terão efeitos visuais se invocados depois que a PhotoView já estiver renderizada em tela.
A seguir uma PhotoView de exemplo:
...
<com.github.chrisbanes.photoview.PhotoView
android:id="@+id/pv_image"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scaleType="fitCenter"
android:src="@drawable/artist_and_his_model" />
...
Então o código de rotação de imagem:
...
Thread{
kotlin.run {
/*
* Aplicando um delay de 2 segundos para dar tempo do
* carregamento da PhotoView em tela.
* */
SystemClock.sleep( 2000 )
runOnUiThread {
/*
* Rotação somente tem efeito depois que a PhotoView já
* está renderizada em tela. Valores negativos fazem com
* que a rotação seja para a esquerda e valores positivos
* fazem com que a rotação seja para a direita.
* */
pv_image.setRotationBy( 58.0F )
/* pv_image.setRotationTo( 90.0F ) */
}
}
}.start()
...
Executando o projeto com o código anterior, temos:
Listener de toque em imagem
Para ouvir o toque / clique único em imagem, podemos implementar a Interface OnPhotoTapListener como a seguir:
class MainActivity : AppCompatActivity(),
OnPhotoTapListener {
override fun onCreate( savedInstanceState: Bundle? ) {
...
pv_image.setOnPhotoTapListener( this )
}
/*
* Invocado quando o toque / clique é dentro da View que
* contém a imagem, mais precisamente no perímetro que a imagem
* cobre.
* */
override fun onPhotoTap(
view: ImageView?, /* ImageView que contém a foto acionada. */
x: Float, /* Posicionamento na abcissa (x) onde foi o clique. */
y: Float /* Posicionamento na ordenada (y) onde foi o clique. */
) {
/* TODO */
}
}
Note que o método onPhotoTap() somente é invocado se o toque na imagem for exatamente na área coberta por ela e não em toda a área coberta pela PhotoView, onde pode haver espaços não cobertos pela imagem.
Listener de toque fora da imagem
Também é possível ouvir ao toque / clique dentro da PhotoView, porém fora da área de cobertura da imagem, isso implementando a Interface OnOutsidePhotoTapListener como no código a seguir:
class MainActivity : AppCompatActivity(),
OnOutsidePhotoTapListener {
override fun onCreate( savedInstanceState: Bundle? ) {
...
pv_image.setOnOutsidePhotoTapListener( this )
}
/*
* Invocado quando o toque / clique é dentro da View
* que contém a imagem, porém não no perímetro coberto
* pela imagem.
* */
override fun onOutsidePhotoTap(
imageView: ImageView? /* PhotoView que recebeu o toque / clique. */
) {
/* TODO */
}
}
Listener de toque na PhotoView
Para ouvir ao toque ou clique em qualquer parte da PhotoView, com cobertura ou não da imagem carregada, é necessário implementar a Interface OnViewTapListener como a seguir:
class MainActivity : AppCompatActivity(),
OnViewTapListener {
override fun onCreate( savedInstanceState: Bundle? ) {
...
pv_image.setOnViewTapListener( this )
}
/*
* Invocado quando o toque / clique é dentro da View
* que contém a imagem, dentro ou fora do perímetro que
* ela cobre.
* */
override fun onViewTap(
view: View?, /* PhotoView que recebeu o acionamento (toque / clique). */
x: Float, /* Posicionamento X onde houve o toque / clique. */
y: Float /* Posicionamento Y onde houve o toque / clique. */
) {
/* TODO */
}
}
Listener de duplo toque, único toque e duplo toque seguido de ação
Para ouvir ao toque duplo, ou ao toque único, incluindo o listener de ação pós toque duplo, podemos implementar a Interface GestureDetector.OnDoubleTapListener:
class MainActivity : AppCompatActivity(),
GestureDetector.OnDoubleTapListener {
override fun onCreate( savedInstanceState: Bundle? ) {
...
pv_image.setOnDoubleTapListener( this )
}
/*
* Notificado quando ocorre um toque duplo. O retorno true
* indica que o evento foi consumido, false indica o contrário.
* */
override fun onDoubleTap( event: MotionEvent? ): Boolean {
/* TODO */
return true
}
/*
* Notificado quando ocorre um evento dentro de um gesto de
* toque duplo, incluindo os eventos: para baixo; mover; e
* subir. O retorno true indica que o evento foi consumido,
* false indica o contrário.
* */
override fun onDoubleTapEvent( event: MotionEvent? ): Boolean {
/* TODO */
return true
}
/*
* Notificado quando ocorre um único toque. O retorno true
* indica que o evento foi consumido, false indica o contrário.
* */
override fun onSingleTapConfirmed( event: MotionEvent? ): Boolean {
/* TODO */
return true
}
}
O método onSingleTapConfirmed() funciona exatamente com a mesma regra de negócio que o método onViewTap() de OnViewTapListener: o toque único precisa ocorrer dentro da PhotoView.
Ouvindo ao evento de drag
O listener de drag na imagem é tão sensível que até mesmo quando o drag não é passível de ser visualizado em tela, quando o zoom não foi ainda aplicado, esse listener é invocado.
Antes de mostrar o código de listener de drag, vamos a um exemplo do que seria um drag junto ao componente PhotoView:
Então podemos ir ao código que implementa a Interface OnViewDragListener:
class MainActivity : AppCompatActivity(),
OnViewDragListener {
override fun onCreate( savedInstanceState: Bundle? ) {
...
pv_image.setOnViewDragListener( this )
}
/*
* O efeito visual do drag ocorre somente quando a imagem
* está com um zoom maior do que o tamanho da tela em alguma dimensão,
* mas o método onDrag() é invocado independente desta regra de
* negócio, basta o gesto de drag ser realizado na PhotoView.
* */
override fun onDrag(
dx: Float, /* Posição no eixo X do toque / clique drag realizado */
dy: Float /* Posição no eixo Y do toque / clique drag realizado */
) {
/* TODO */
}
}
Ouvindo a atualização da matriz da imagem
Se em seu domínio de problema é preciso algum trabalho quando houver a atualização do objeto Matrix da imagem, para isso é possível implementar a Interface OnMatrixChangedListener como a seguir:
class MainActivity : AppCompatActivity(),
OnMatrixChangedListener {
override fun onCreate( savedInstanceState: Bundle? ) {
...
pv_image.setOnMatrixChangeListener( this )
}
/*
* Invocado quando houver qualquer atualização na imagem:
* posicionamento ou tamanho.
* */
override fun onMatrixChanged(
rect: RectF? /* Contém todas as coordenadas e tamanho atual da imagem. */
) {
/*
* Documentação oficial:
*
* O RectF contém quatro coordenadas de flutuação para um retângulo.
* O retângulo é representado pelas coordenadas de suas 4 bordas
* (esquerda, superior, inferior, direita). Esses campos podem ser
* acessados diretamente. Use width() e height() para recuperar
* a largura e a altura do retângulo.
*
* https://developer.android.com/reference/android/graphics/RectF
* */
/* TODO */
}
}
Hackcode para ViewGroups problemáticos
Alguns ViewGroups que implementam OnInterceptTouchEvent, como o ViewPager e o DrawerLayout, disparam exceções quando alguma PhotoView está dentro deles.
O código hack para burlar esse problema é o seguinte:
class HackyProblematicViewGroup( context: Context ) :
ProblematicViewGroup( context ) {
fun onInterceptTouchEvent( event: MotionEvent ): Boolean {
try {
return super.onInterceptTouchEvent( event )
}
catch ( e: IllegalArgumentException ) {
/*
* Descomente a linha abaixo se você realmente quiser ver
* os erros
* */
/* e.printStackTrace(); */
return false /* false = evento não consumido. */
}
}
}
Note que o código acima é bem genérico, serve para qualquer ViewGroup problemático. Veja, por exemplo, as implementações de hackcode, direto da documentação oficial da PhotoView API, para:
Pontos negativos
- A documentação não mostra, detalhadamente, como obter o máximo da PhotoView, é preciso navegar pelos exemplos (sem comentários) para então entender a API por completo;
- O nome da API é bem confuso em relação a proposta dela. Um rótulo como ZoomView certamente cairia melhor na API;
- A funcionalidade de rotação de imagem poderia ser removida, pois não tem haver com a proposta de ser uma API simples para a aplicação de zoom. Ou:
- É possível que a funcionalidade de rotação tenha sido adicionada devido ao problema que alguns devices Android têm depois que uma fotografia é realizada: a imagem é entregue invertida. Com os métodos de rotação seria simples resolver isso, porém na documentação não há nada informando o porquê da API de rotação.
- A Interface OnSingleFlingListener quando em uso não trás nada diferente do que é conseguido com outras Interfaces da API PhotoView, Interfaces que têm um rótulo muito mais significativo em termos boa de leitura de código.
Pontos positivos
- Código muito simples para já conseguirmos a aplicação completa de zoom;
- Possibilidade de atualização de níveis de escala e de duração do zoom;
- Vários tipos de listeners que permitem o acionamento das mais variadas funções de acordo com o toque do usuário em View;
- Possibilidade de trabalho com APIs de carregamento de imagens remotas.
Considerações finais
Apesar do zoom ser uma funcionalidade simples e que desenvolvedores Android de primeira viagem possam imaginar já ser uma funcionalidade nativa no ImageView, por exemplo, na verdade não é.
A API PhotoView faz o trabalho da melhor maneira possível:
- Exigindo poucas linhas de código;
- E entregando o zoom completo.
Obviamente que sempre é possível obter ainda mais de APIs Android, mas a PhotoView tem realmente uma proposta de ser simples. Logo, seguindo as dicas da documentação, se você precisa de algo ainda mais robusto em termos de escala de imagem, tente a API Subsampling-Scale-Image-View.
Projeto Android
Como projeto de exemplo teremos um simples aplicativo de vendas de carros que na verdade o usuário conseguirá entrar em contato com o vendedor ao invés de passar o cartão por alguma API de pagamento.
Nesse app teremos de trabalhar a melhoria da apresentação das imagens de álbum dos carros, onde o zoom é a parte que falta. Para isso utilizaremos a PhotoView API.
O projeto será desenvolvido em duas partes:
- Na primeira teremos todo o algoritmo necessário que simula um aplicativo real de vendas de carros;
- Na segunda vamos implantação da PhotoView API para a melhoria do aplicativo.
O projeto está por completo no GitHub a seguir: https://github.com/viniciusthiengo/mob-motors-android.
De qualquer forma, não deixe de seguir o exemplo em artigo até o final, pois nele iremos explicando os trechos de código em uso.
Protótipo estático
A seguir as imagens do protótipo estático da primeira parte do projeto de aplicativo:
Tela de entrada | Tela de listagem de carros |
Menu gaveta aberto | Tela de detalhes de carro |
Caixa de diálogo de álbum |
Assim podemos partir para a criação do projeto.
Iniciando o projeto
Em seu Android Studio inicie um novo projeto Kotlin (pode ser Java):
- Nome da aplicação: MobMotors;
- API mínima: 16 (Android Jelly Bean). Mais de 99% dos aparelhos Android em mercado sendo atendidos;
- Atividade inicial: Navigation Drawer Activity;
- Para todos os outros campos, deixe-os com os valores já definidos por padrão.
Ao final do projeto teremos a seguinte arquitetura de projeto:
Configurações Gradle
A seguir as configurações iniciais do Gradle Project Level, ou build.gradle (Project: MobMotors):
buildscript {
ext.kotlin_version = '1.2.60'
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.1.4'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects {
repositories {
google()
jcenter()
/* Para a RoundedImageView */
mavenCentral()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
Então as configurações iniciais do Gradle App Level, ou build.gradle (Module: app):
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
android {
compileSdkVersion 28
defaultConfig {
applicationId "thiengo.com.br.mobmotors"
minSdkVersion 16
targetSdkVersion 28
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'com.android.support:appcompat-v7:28.0.0-rc01'
implementation 'com.android.support:design:28.0.0-rc01'
/* Para a RoundedImageView */
implementation 'com.makeramen:roundedimageview:2.3.0'
/* Para a Picasso API */
implementation 'com.squareup.picasso:picasso:2.71828'
}
Note que estamos utilizando a Picasso API e também RoundedImageView API, isso, pois essas APIs nos ajudarão a obter o mesmo layout desenvolvido em protótipo estático.
Não esqueça de sempre utilizar as versões mais atuais e estáveis de APIs e SDKs.
Configurações AndroidManifest
A seguir as configurações do AndroidManifest.xml:
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="thiengo.com.br.mobmotors">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:hardwareAccelerated="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".DetailsActivity"
android:label="@string/title_activity_details"
android:parentActivityName=".MainActivity"
android:theme="@style/AppTheme.NoActionBar">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="thiengo.com.br.mobmotors.MainActivity" />
</activity>
</application>
</manifest>
Configurações de estilo
As configurações de estilo são um pouco mais complexas quando comparadas a algumas outras de outros projetos já abordados no Blog. Dessa vez teremos vários estilos sendo utilizados para diminuir a quantidade de código em layouts XML.
Vamos iniciar pelo arquivo de definição de cores, /res/values/colors.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#CA2B2F</color>
<color name="colorPrimaryDark">#881A1C</color>
<color name="colorAccent">#FFFF00</color>
<color name="colorBgNavigationView">#F5F5F6</color>
<color name="colorItemNormal">#777777</color>
<color name="colorLightGrey">#DDDDDD</color>
<color name="colorGrey">#666666</color>
<color name="colorDarkGrey">#666666</color>
<color name="colorSubText">#AAAAAA</color>
<color name="colorLink">#00A6FF</color>
</resources>
Agora o arquivo de dimensões, /res/values/dimens.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="activity_horizontal_margin">16dp</dimen>
<dimen name="activity_vertical_margin">16dp</dimen>
<dimen name="default_margin_padding">12dp</dimen>
</resources>
Então o arquivo de Strings, /res/values/strings.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">MobMotors</string>
<string name="page_name">Carros em oferta</string>
<string name="title_activity_details">DetailsActivity</string>
<string name="navigation_drawer_open">Menu gaveta aberto</string>
<string name="navigation_drawer_close">Menu gaveta fechado</string>
<string name="action_search">Buscar</string>
<string name="action_cars">Carros</string>
<string name="action_motorcycles">Motos</string>
<string name="action_call">Ligar</string>
<string name="nav_item_cars_on_sale">Carros em oferta</string>
<string name="nav_item_popular_cars">Carros populares</string>
<string name="nav_item_foreign_cars">Carros importados</string>
<string name="nav_item_motorcycles_on_sale">Motos em oferta</string>
<string name="nav_item_top_sales">Mais vendidos</string>
<string name="iv_desc_year_prod_model">Ano de produção / ano do modelo</string>
<string name="iv_desc_kilometers">Quilômetros já rodados</string>
<string name="iv_desc_car_changes">Tipo de câmbio</string>
<string name="iv_desc_place">Tipo do vendedor</string>
<string name="label_more_info">Mais informações</string>
<string name="label_color">Cor:</string>
<string name="label_bodywork">Carroceria:</string>
<string name="label_plate">Placa final:</string>
<string name="label_gas">Combustível:</string>
<string name="label_seller_obs">Observações do vendedor</string>
<string name="iv_desc_close_dialog">Fechar caixa de diálogo de imagens</string>
<string name="iv_desc_prev_image">Imagem anterior</string>
<string name="iv_desc_next_image">Próxima imagem</string>
</resources>
Agora o arquivo de definição de algumas configurações de tema específicas para aparelhos com o Android 21, Lollipop, ou superior, /res/values-v21/styles.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="AppTheme.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="android:statusBarColor">@android:color/transparent</item>
</style>
</resources>
Por fim o arquivo de definição geral de tema de projeto, /res/values/styles.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Estilo base do aplicativo. -->
<style
name="AppTheme"
parent="Theme.AppCompat.Light.DarkActionBar">
<item name="android:windowBackground">@drawable/background</item>
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
<!--
Estilo que remove a barra de topo padrão e assim deixa esse
trecho de topo livre para ser utilizado, por exemplo, pelo
AppBarLayout.
-->
<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.Dark.ActionBar" />
<!--
Utilizado para a correta apresentação de menus de pop-up.
Apesar de não utilizarmos menus pop-up de topo aqui, vamos
continuar com este tema, pois é algo comum em qualquer
componente Toolbar quando em layout.
-->
<style
name="AppTheme.PopupOverlay"
parent="ThemeOverlay.AppCompat.Light" />
<!--
Estilo que contém algumas configurações padrões para os
ícones utilizados em item de lista de carros. Com isso
economizamos em código no layout XML de item e também
facilitamos a edição de layout, tendo em mente que muitas
configurações estão no mesmo local.
-->
<style name="AppTheme.ItemIcon">
<item name="android:layout_width">20dp</item>
<item name="android:layout_height">20dp</item>
<item name="android:layout_marginTop">1dp</item>
<item name="android:layout_marginRight">6dp</item>
<item name="android:scaleType">center</item>
<item name="android:tint">@color/colorItemNormal</item>
</style>
<!--
Estilo que contém algumas configurações padrões para os
textos que acompanham os ícones utilizados em item de lista
de carros. Com isso economizamos em código no layout XML de
item e também facilitamos a edição de layout.
-->
<style name="AppTheme.ItemTextIcon">
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:textSize">16sp</item>
<item name="android:textColor">@color/colorItemNormal</item>
</style>
<!--
Estilo que contém algumas configurações padrões para os
botões presentes no DialogFragment de visualização de
álbum do carro em foco.
-->
<style name="AppTheme.DialogButton">
<item name="android:layout_width">32dp</item>
<item name="android:layout_height">32dp</item>
<item name="android:scaleType">fitCenter</item>
</style>
<!--
Parte do estilo dos RoundedImageViews do layout de detalhes
de carro à venda.
-->
<style name="AppTheme.DetailsRoundedImageView">
<item name="android:layout_width">0dp</item>
<item name="android:layout_height">42dp</item>
<item name="android:layout_weight">1</item>
<item name="android:scaleType">centerCrop</item>
<item name="android:onClick">openAlbum</item>
</style>
<!--
Estilo que contém algumas configurações padrões para os
ícones utilizados na tela de detalhes de carro à venda.
-->
<style
name="AppTheme.DetailsInfoIcon"
parent="@style/AppTheme.ItemIcon">
<item name="android:tint">@color/colorDarkGrey</item>
</style>
<!--
Estilo que contém algumas configurações padrões para os
textos que acompanham os ícones utilizados na área de
detalhes de carro.
-->
<style
name="AppTheme.DetailsTextIcon"
parent="@style/AppTheme.ItemTextIcon">
<item name="android:textColor">@color/colorDarkGrey</item>
</style>
<!--
Estilo que contém algumas configurações padrões para os
rótulos de informações extras da área de detalhes de carro.
-->
<style
name="AppTheme.DetailsLabelInfo"
parent="@style/AppTheme.ItemTextIcon">
<item name="android:textColor">@color/colorDarkGrey</item>
<item name="android:textSize">14sp</item>
<item name="android:textStyle">bold</item>
</style>
<!--
Estilo que contém algumas configurações padrões para as
informações extras da área de detalhes de carro.
-->
<style
name="AppTheme.DetailsTextInfo"
parent="@style/AppTheme.DetailsLabelInfo">
<item name="android:textStyle">normal</item>
<item name="android:layout_marginLeft">18dp</item>
</style>
<!--
Estilo que contém algumas configurações padrões para os
ícones de círculo contidos na área de detalhes de carro.
-->
<style name="AppTheme.DetailsCircleIcon">
<item name="android:layout_width">12dp</item>
<item name="android:layout_height">12dp</item>
<item name="android:layout_alignParentLeft">true</item>
<item name="android:layout_marginTop">44dp</item>
<item name="android:scaleType">center</item>
<item name="android:src">@drawable/ic_circle</item>
<item name="android:tint">@color/colorDarkGrey</item>
</style>
<!--
Estilo que contém algumas configurações padrões para as
linhas horizontais de separação de conteúdo extra na área
de detalhes de carro.
-->
<style name="AppTheme.DetailsHorizontalLine">
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">0.8dp</item>
<item name="android:layout_marginRight">@dimen/default_margin_padding</item>
<item name="android:layout_marginLeft">24dp</item>
<item name="android:layout_marginTop">6dp</item>
<item name="android:background">@color/colorLightGrey</item>
</style>
<!--
Estilo que contém algumas configurações padrões para os
de informações de extras na área de detalhes de carro.
-->
<style name="AppTheme.DetailsTitleInfo">
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_alignParentRight">true</item>
<item name="android:layout_marginTop">-10dp</item>
<item name="android:textColor">@color/colorGrey</item>
<item name="android:textSize">21sp</item>
</style>
</resources>
Muitos estilos estão sendo utilizados principalmente para mantermos menores os códigos dos layouts XML e com isso deixa-los fáceis de aplicar manutenção.
Essa estratégia de criação de estilos para Views de layouts de projeto é um bom caminho para facilitar a evolução de aplicativos Android, logo, não deixe de também fazer isso em seus projetos de apps.
Classes de domínio
Para este projeto de exemplo teremos três classes de domínio, todas implementando a Interface Parcelable, pois objetos dessas classes serão transferidos de uma atividade a outra por meio de Intent.
Todas as classes de domínio estarão no pacote /domain, logo, crie este caso ainda não o tenha em seu projeto.
A seguir o código da classe MoreInfo, classe responsável por conter informações extras de cada carro:
class MoreInfo(
val color: String,
val bodyWork: String,
val finalPlate: Int,
val gas: String,
val fullDescription: String
) : Parcelable {
constructor(source: Parcel) : this(
source.readString(),
source.readString(),
source.readInt(),
source.readString(),
source.readString()
)
override fun describeContents() = 0
override fun writeToParcel( dest: Parcel, flags: Int ) = with( dest ) {
writeString(color)
writeString(bodyWork)
writeInt(finalPlate)
writeString(gas)
writeString(fullDescription)
}
companion object {
@JvmField
val CREATOR: Parcelable.Creator<MoreInfo> = object : Parcelable.Creator<MoreInfo> {
override fun createFromParcel(source: Parcel): MoreInfo = MoreInfo(source)
override fun newArray(size: Int): Array<MoreInfo?> = arrayOfNulls(size)
}
}
}
Então a classe que representa as informações de vendedor, Seller:
class Seller(
val phone: String,
val type: String,
val cityState: String
) : Parcelable {
/*
* Retorna o número de telefone celular do vendedor no
* formato brasileiro.
* */
fun phoneLabel(): String =
String.format(
"(%s) %s-%s-%s",
phone.substring( 0, 2 ),
phone.substring( 2, 3 ),
phone.substring( 3, 7 ),
phone.substring( 7, 11 )
)
constructor(source: Parcel) : this(
source.readString(),
source.readString(),
source.readString()
)
override fun describeContents() = 0
override fun writeToParcel(dest: Parcel, flags: Int) = with(dest) {
writeString(phone)
writeString(type)
writeString(cityState)
}
companion object {
@JvmField
val CREATOR: Parcelable.Creator<Seller> = object : Parcelable.Creator<Seller> {
override fun createFromParcel(source: Parcel): Seller = Seller(source)
override fun newArray(size: Int): Array<Seller?> = arrayOfNulls(size)
}
}
}
Então a principal classe de domínio, que inclusive conterá objetos das outras duas classes acima. Segue classe Car:
class Car(
val model: String,
val brand: String,
val price: Float,
val shortDescription: String,
val yearProduction: Int,
val yearModel: Int,
val kilometers: Int,
val carChanges: String,
val seller: Seller,
val moreInfo: MoreInfo,
val imagesUrl: List<String>
) : Parcelable {
fun carLabel(): String =
String.format( "%s %s", brand, model )
fun yearProdModelLabel(): String =
String.format( "%d / %d", yearProduction, yearModel )
/*
* A vírgula em "%,d" indica que o separador de milhar deve
* ser utilizado. O valor Locale.GERMANY garante que o
* separador de milhar será um "." e não uma ",".
* */
fun kilometersLabel(): String =
String.format( Locale.GERMANY, "%,d", kilometers )
fun priceLabel(): String =
String.format( Locale.GERMANY, "R$ %,.2f", price )
/*
* A primeira imagem em imagesUrl é sempre tratada com imagem
* principal do carro, em nosso domínio de problema.
* */
fun getMainImage(): String =
imagesUrl[0]
constructor(source: Parcel) : this(
source.readString(),
source.readString(),
source.readFloat(),
source.readString(),
source.readInt(),
source.readInt(),
source.readInt(),
source.readString(),
source.readParcelable<Seller>(Seller::class.java.classLoader),
source.readParcelable<MoreInfo>(MoreInfo::class.java.classLoader),
source.createStringArrayList()
)
override fun describeContents() = 0
override fun writeToParcel(dest: Parcel, flags: Int) = with(dest) {
writeString(model)
writeString(brand)
writeFloat(price)
writeString(shortDescription)
writeInt(yearProduction)
writeInt(yearModel)
writeInt(kilometers)
writeString(carChanges)
writeParcelable(seller, 0)
writeParcelable(moreInfo, 0)
writeStringList(imagesUrl)
}
companion object {
const val KEY = "car"
const val KEY_IMAGE = "image"
@JvmField
val CREATOR: Parcelable.Creator<Car> = object : Parcelable.Creator<Car> {
override fun createFromParcel( source: Parcel ): Car = Car( source )
override fun newArray( size: Int ): Array<Car?> = arrayOfNulls( size )
}
}
}
Não deixe de ler os comentários dos métodos.
Base de dados, mock data
Como é comum em projetos em fase de desenvolvimento ou em projetos de teste, aqui utilizaremos também uma base de dados simulados, ou: mock data.
No pacote /data crie a classe Database como a seguir:
class Database {
companion object {
fun getCars(): List<Car> =
listOf(
Car(
"Ka",
"Ford",
7900.0F,
"1.0 MPI GL 8V GASOLINA 2P MANUAL",
1997,
1998,
180900,
"Manual",
Seller("11999887766", "Loja", "São Bernardo do Campo (SP)"),
MoreInfo(
"Vermelho",
"Hatchback",
9,
"Gasolina",
"Veículo em Bom Estado, Detalhes Apenas de Uso! " +
"Melhor Custo-benefício da Categoria, Super Econômico. " +
"Versão GL 1.0 MPI C/ Travas e Vidros Elétricos + Alarme " +
"+ 4 Pneus Novos. Contate Nossa Central e Fale com um " +
"Personal Car Agora Mesmo: Aprovamos seu Crédito em Minutos; " +
"Trabalhamos com as Menores Taxas do Mercado; Financiamos Com " +
"e Sem Entrada em até 60x; Parcelamos sua Entrada no Cartão " +
"de Crédito; Avaliamos seu Carro em Tempo Real; Garantimos " +
"a Melhor Avaliação do seu Usado na Troca;"
),
listOf(
"https://image.webmotors.com.br/_fotos/anunciousados/gigante/2018/201808/20180810/ford-ka-1.0-mpi-gl-8v-gasolina-2p-manual-wmimagem16503502368.jpg",
"https://image.webmotors.com.br/_fotos/anunciousados/gigante/2018/201808/20180810/ford-ka-1.0-mpi-gl-8v-gasolina-2p-manual-wmimagem16503526729.jpg",
"https://image.webmotors.com.br/_fotos/anunciousados/gigante/2018/201808/20180810/ford-ka-1.0-mpi-gl-8v-gasolina-2p-manual-wmimagem16503555610.jpg",
"https://image.webmotors.com.br/_fotos/anunciousados/gigante/2018/201808/20180810/ford-ka-1.0-mpi-gl-8v-gasolina-2p-manual-wmimagem16503590080.jpg",
"https://image.webmotors.com.br/_fotos/anunciousados/gigante/2018/201808/20180810/ford-ka-1.0-mpi-gl-8v-gasolina-2p-manual-wmimagem16503725378.jpg"
)
),
Car(
"106",
"Peugeot",
7900.0F,
"1.0 XN 8V GASOLINA 4P MANUAL",
1994,
1995,
189200,
"Manual",
Seller("11999887766", "Loja", "São Bernardo do Campo (SP)"),
MoreInfo(
"Branco",
"Hatchback",
9,
"Gasolina",
"Veículo 2º Dono, Muito Bem Conservado por Conta do Ano. " +
"Apenas 106 Unidades Fabricadas no Brasil, Super Raro! Edição " +
"Limitada Kid 1.0 4pts C/ Bancos de Couro Azul, Imperdível. " +
"Exemplar Ótimo Para Colocar Placa Preta, Perfeito Para " +
"Colecionadores. Contate Nossa Central e Fale com um Personal " +
"Car Agora Mesmo: Aprovamos seu Crédito em Minutos; " +
"Trabalhamos com as Menores Taxas do Mercado; Financiamos " +
"Com e Sem Entrada em até 60x; Parcelamos sua Entrada no Cartão " +
"de Crédito;"
),
listOf(
"https://image.webmotors.com.br/_fotos/anunciousados/gigante/2018/201808/20180811/peugeot-106-1.0-xn-8v-gasolina-4p-manual-wmimagem11320892426.jpg",
"https://image.webmotors.com.br/_fotos/anunciousados/gigante/2018/201808/20180811/peugeot-106-1.0-xn-8v-gasolina-4p-manual-wmimagem11320925461.jpg",
"https://image.webmotors.com.br/_fotos/anunciousados/gigante/2018/201808/20180811/peugeot-106-1.0-xn-8v-gasolina-4p-manual-wmimagem11320981874.jpg",
"https://image.webmotors.com.br/_fotos/anunciousados/gigante/2018/201808/20180811/peugeot-106-1.0-xn-8v-gasolina-4p-manual-wmimagem11321041354.jpg",
"https://image.webmotors.com.br/_fotos/anunciousados/gigante/2018/201808/20180811/peugeot-106-1.0-xn-8v-gasolina-4p-manual-wmimagem11321068786.jpg"
)
),
Car(
"Tempra",
"FIAT",
8900.0F,
"2.0 IE 8V GASOLINA 4P MANUAL",
1995,
1995,
157800,
"Manual",
Seller("11999887766", "Loja", "Serra (ES)"),
MoreInfo(
"Cinza",
"Sedã",
3,
"Gasolina",
"Veículo Muito Bem Conservado, Realmente Diferenciado! " +
"Primeiro Que Ver Fecha Negócio, Preço Sensacional. Versão " +
"SX 2.0 IE C/ DH/TE/VE + Alarme + Faróis de Milha + Rodas. " +
"Contate Nossa Central e Fale com um Personal Car Agora " +
"Mesmo: Aprovamos seu Crédito em Minutos; Trabalhamos com " +
"as Menores Taxas do Mercado; Financiamos Com e Sem Entrada " +
"em até 60x; Parcelamos sua Entrada no Cartão de Crédito; " +
"Avaliamos seu Carro em Tempo Real; Garantimos a Melhor " +
"Avaliação do seu Usado na Troca; Outros Opcionais: Farol " +
"de neblina, MP3 Player."
),
listOf(
"https://image.webmotors.com.br/_fotos/anunciousados/gigante/2018/201808/20180810/fiat-tempra-2.0-ie-8v-gasolina-4p-manual-wmimagem16492872565.jpg",
"https://image.webmotors.com.br/_fotos/anunciousados/gigante/2018/201808/20180810/fiat-tempra-2.0-ie-8v-gasolina-4p-manual-wmimagem16492938637.jpg",
"https://image.webmotors.com.br/_fotos/anunciousados/gigante/2018/201808/20180810/fiat-tempra-2.0-ie-8v-gasolina-4p-manual-wmimagem16492975328.jpg",
"https://image.webmotors.com.br/_fotos/anunciousados/gigante/2018/201808/20180810/fiat-tempra-2.0-ie-8v-gasolina-4p-manual-wmimagem16493017038.jpg",
"https://image.webmotors.com.br/_fotos/anunciousados/gigante/2018/201808/20180810/fiat-tempra-2.0-ie-8v-gasolina-4p-manual-wmimagem16493056165.jpg"
)
),
Car(
"Celta",
"Chevrolet",
8990.0F,
"1.0 MPFI VHC 8V GASOLINA 2P MANUAL",
2002,
2003,
199000,
"Manual",
Seller("11999887766", "Loja", "Vitória (ES)"),
MoreInfo(
"Azul",
"Hatchback",
8,
"Gasolina",
"SPINER MOTORS, SEU JEITO INTELIGENTE DE COMPRAR E " +
"VENDER CARRO !!! Veículo em Bom Estado, Detalhes a Serem " +
"Feitos. Excelente Custo-benefício, Vale a Pena Conferir. " +
"Versão VHC 2pts Básico, Preço Sensacional. Contate Nossa " +
"Central e Fale com um Personal Car Agora Mesmo: Aprovamos " +
"seu Crédito em Minutos; Trabalhamos com as Menores Taxas " +
"do Mercado; Financiamos Com e Sem Entrada em até 60x; " +
"Parcelamos sua Entrada no Cartão de Crédito; Avaliamos seu " +
"Carro em Tempo Real;"
),
listOf(
"https://image.webmotors.com.br/_fotos/anunciousados/gigante/2018/201804/20180428/chevrolet-celta-1.0-mpfi-vhc-8v-gasolina-2p-manual-wmimagem15534633290.jpg",
"https://image.webmotors.com.br/_fotos/anunciousados/gigante/2018/201804/20180428/chevrolet-celta-1.0-mpfi-vhc-8v-gasolina-2p-manual-wmimagem15534842822.jpg",
"https://image.webmotors.com.br/_fotos/anunciousados/gigante/2018/201804/20180428/chevrolet-celta-1.0-mpfi-vhc-8v-gasolina-2p-manual-wmimagem15534666961.jpg",
"https://image.webmotors.com.br/_fotos/anunciousados/gigante/2018/201804/20180428/chevrolet-celta-1.0-mpfi-vhc-8v-gasolina-2p-manual-wmimagem15535103319.jpg",
"https://image.webmotors.com.br/_fotos/anunciousados/gigante/2018/201804/20180428/chevrolet-celta-1.0-mpfi-vhc-8v-gasolina-2p-manual-wmimagem15534692958.jpg"
)
),
Car(
"Palio",
"FIAT",
9900.0F,
"1.0 MPI 6M WEEKEND 8V GASOLINA 4P MANUAL",
1999,
2000,
317400,
"Manual",
Seller("11999887766", "Loja", "Fortaleza (CE)"),
MoreInfo(
"Cinza",
"Perua/SW",
0,
"Gasolina",
"Veículo em Bom Estado, Vale a Pena Conferir! Primeiro " +
"Que Ver Fecha Negócio, Ótimo Custo-benefício. Versão " +
"Weekend 1.0 6 Marchas C/ DH/TE/VE + Alarme + Rodas, Preço " +
"Imperdível. Contate Nossa Central e Fale com um Personal " +
"Car Agora Mesmo: Aprovamos seu Crédito em Minutos; " +
"Trabalhamos com as Menores Taxas do Mercado; Financiamos " +
"Com e Sem Entrada em até 60x; Parcelamos sua Entrada no " +
"Cartão de Crédito; Avaliamos seu Carro em Tempo Real; " +
"Garantimos a Melhor Avaliação do seu Usado na Troca; Outros " +
"Opcionais: MP3 Player."
),
listOf(
"https://image.webmotors.com.br/_fotos/anunciousados/gigante/2018/201807/20180704/fiat-palio-1.0-mpi-6m-weekend-8v-gasolina-4p-manual-wmimagem19135786445.jpg",
"https://image.webmotors.com.br/_fotos/anunciousados/gigante/2018/201807/20180704/fiat-palio-1.0-mpi-6m-weekend-8v-gasolina-4p-manual-wmimagem19135872522.jpg",
"https://image.webmotors.com.br/_fotos/anunciousados/gigante/2018/201807/20180704/fiat-palio-1.0-mpi-6m-weekend-8v-gasolina-4p-manual-wmimagem19135816796.jpg",
"https://image.webmotors.com.br/_fotos/anunciousados/gigante/2018/201807/20180704/fiat-palio-1.0-mpi-6m-weekend-8v-gasolina-4p-manual-wmimagem19135895798.jpg",
"https://image.webmotors.com.br/_fotos/anunciousados/gigante/2018/201807/20180704/fiat-palio-1.0-mpi-6m-weekend-8v-gasolina-4p-manual-wmimagem19135843541.jpg"
)
)
)
}
}
Assim podemos ir as classes da camada de visualização: atividades, fragmento e adapter.
Classe adaptadora
Para a classe adaptadora que será utilizada junto ao RecyclerView do projeto, vamos iniciar com o layout de item carregado nela, /res/layout/car.xml:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:layout_marginTop="4dp"
android:background="@drawable/item_car_background"
android:paddingBottom="0dp"
android:paddingLeft="4dp"
android:paddingRight="0dp"
android:paddingTop="4dp">
<com.makeramen.roundedimageview.RoundedImageView
android:id="@+id/iv_car_thumb"
android:layout_width="118dp"
android:layout_height="118dp"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:layout_marginBottom="4dp"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:scaleType="centerCrop"
app:riv_border_color="@android:color/transparent"
app:riv_border_width="2dp"
app:riv_corner_radius="5dp"
app:riv_mutate_background="true"
app:riv_oval="false"
app:riv_tile_mode="clamp" />
<TextView
android:id="@+id/tv_name_brand"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toEndOf="@+id/iv_car_thumb"
android:layout_toRightOf="@+id/iv_car_thumb"
android:ellipsize="end"
android:maxLines="1"
android:textSize="21sp" />
<TextView
android:id="@+id/tv_short_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/tv_name_brand"
android:layout_alignStart="@+id/tv_name_brand"
android:layout_below="@+id/tv_name_brand"
android:ellipsize="end"
android:maxLines="1"
android:textAllCaps="true"
android:textColor="@color/colorSubText"
android:textSize="13sp" />
<ImageView
android:id="@+id/iv_year_prod_model"
style="@style/AppTheme.ItemIcon"
android:layout_alignLeft="@+id/tv_short_description"
android:layout_alignStart="@+id/tv_short_description"
android:layout_below="@+id/tv_short_description"
android:layout_marginTop="9dp"
android:contentDescription="@string/iv_desc_year_prod_model"
android:src="@drawable/ic_year_prod_model" />
<TextView
android:id="@+id/tv_year_prod_model"
style="@style/AppTheme.ItemTextIcon"
android:layout_alignTop="@+id/iv_year_prod_model"
android:layout_toEndOf="@+id/iv_year_prod_model"
android:layout_toRightOf="@+id/iv_year_prod_model" />
<ImageView
android:id="@+id/iv_kilometers"
style="@style/AppTheme.ItemIcon"
android:layout_alignLeft="@+id/tv_short_description"
android:layout_alignStart="@+id/tv_short_description"
android:layout_below="@+id/iv_year_prod_model"
android:contentDescription="@string/iv_desc_kilometers"
android:src="@drawable/ic_kilometers" />
<TextView
android:id="@+id/tv_kilometers"
style="@style/AppTheme.ItemTextIcon"
android:layout_alignTop="@+id/iv_kilometers"
android:layout_toEndOf="@+id/iv_kilometers"
android:layout_toRightOf="@+id/iv_kilometers" />
<ImageView
android:id="@+id/iv_car_changes"
style="@style/AppTheme.ItemIcon"
android:layout_alignLeft="@+id/tv_short_description"
android:layout_alignStart="@+id/tv_short_description"
android:layout_below="@+id/iv_kilometers"
android:contentDescription="@string/iv_desc_car_changes"
android:src="@drawable/ic_car_changes" />
<TextView
android:id="@+id/tv_car_changes"
style="@style/AppTheme.ItemTextIcon"
android:layout_alignTop="@+id/iv_car_changes"
android:layout_toEndOf="@+id/iv_car_changes"
android:layout_toRightOf="@+id/iv_car_changes" />
<View
android:id="@+id/v_vertical_line"
android:layout_width="0.8dp"
android:layout_height="wrap_content"
android:layout_alignBottom="@+id/iv_car_changes"
android:layout_alignTop="@+id/iv_year_prod_model"
android:layout_marginEnd="8dp"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:layout_marginStart="8dp"
android:layout_toEndOf="@+id/tv_year_prod_model"
android:layout_toRightOf="@+id/tv_year_prod_model"
android:background="@color/colorLightGrey" />
<ImageView
android:id="@+id/iv_place"
style="@style/AppTheme.ItemIcon"
android:layout_alignTop="@+id/v_vertical_line"
android:layout_marginTop="0dp"
android:layout_toEndOf="@+id/v_vertical_line"
android:layout_toRightOf="@+id/v_vertical_line"
android:contentDescription="@string/iv_desc_place"
android:src="@drawable/ic_place" />
<TextView
android:id="@+id/tv_place"
style="@style/AppTheme.ItemTextIcon"
android:layout_alignTop="@+id/iv_place"
android:layout_toEndOf="@+id/iv_place"
android:layout_toRightOf="@+id/iv_place" />
<TextView
android:id="@+id/tv_place_full"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/tv_place"
android:layout_alignStart="@+id/tv_place"
android:layout_below="@+id/tv_place"
android:layout_marginEnd="4dp"
android:layout_marginRight="4dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/colorSubText"
android:textSize="13sp" />
<TextView
android:id="@+id/tv_price"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_margin="0dp"
android:background="@drawable/item_price_background"
android:textColor="@android:color/white"
android:textSize="15sp" />
</RelativeLayout>
Note como o atributo style é utilizado já com frequência no layout acima.
Antes de prosseguirmos para a apresentação do diagrama, vamos aos drawables presentes no XML anterior. Primeiro o drawable de background de item, /res/drawable/item_car_background.xml:
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<!--
Cor de background da View. Necessário o valor
"@android:color/transparent", pois em versões do
Android inferiores a versão 19, KitKat, o background
será preto se nenhuma cor, mesmo o transparente,
for definida.
-->
<solid android:color="@android:color/transparent" />
<!-- Cor e largura de borda da View. -->
<stroke
android:width="0.9dp"
android:color="@color/colorLightGrey" />
<!-- Espaçamentos aplicados a View. -->
<padding
android:bottom="5dp"
android:left="5dp"
android:right="5dp"
android:top="5dp" />
<!-- Curvatura dos cantos da View. -->
<corners android:radius="5dp" />
</shape>
Com o drawable anterior conseguimos as bordas com curvatura, como a seguir:
Agora o drawable que permite o correto shape de background na área de apresentação de preço, o arquivo /res/drawable/item_price_background.xml:
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<!-- Cor de background da View. -->
<solid android:color="@color/colorPrimary" />
<!-- Cor e largura de borda da View. -->
<stroke
android:width="0.9dp"
android:color="@color/colorPrimary" />
<!-- Espaçamentos aplicados a View. -->
<padding
android:bottom="2.5dp"
android:left="10dp"
android:right="10dp"
android:top="2.5dp" />
<!--
Curvatura dos cantos inferior direito e superior
esquerdo da View.
-->
<corners
android:bottomRightRadius="5dp"
android:topLeftRadius="5dp" />
</shape>
Com o drawable anterior conseguimos o fundo do preço como a seguir:
Com isso podemos ir ao diagrama do layout car.xml:
Por fim o código Kotlin da classe CarsAdapter:
class CarsAdapter(
private val context: Context,
private val cars: List<Car> ) :
RecyclerView.Adapter<CarsAdapter.ViewHolder>() {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int ) : CarsAdapter.ViewHolder {
val v = LayoutInflater
.from( context )
.inflate( R.layout.car, parent, false )
return ViewHolder( v )
}
override fun onBindViewHolder(
holder: ViewHolder,
position: Int ) {
holder.setData( cars[ position ] )
}
override fun getItemCount(): Int {
return cars.size
}
inner class ViewHolder( itemView: View ) :
RecyclerView.ViewHolder( itemView ),
View.OnClickListener {
var rivCarThumb: RoundedImageView
var tvNameBrand: TextView
var tvShortDescription: TextView
var tvYearProdModel: TextView
var tvKilometers: TextView
var tvCarChanges: TextView
var tvPlace: TextView
var tvPlaceFull: TextView
var tvPrice: TextView
init {
itemView.setOnClickListener( this )
rivCarThumb = itemView.findViewById( R.id.iv_car_thumb )
tvNameBrand = itemView.findViewById( R.id.tv_name_brand )
tvShortDescription = itemView.findViewById( R.id.tv_short_description )
tvYearProdModel = itemView.findViewById( R.id.tv_year_prod_model )
tvKilometers = itemView.findViewById( R.id.tv_kilometers )
tvCarChanges = itemView.findViewById( R.id.tv_car_changes )
tvPlace = itemView.findViewById( R.id.tv_place )
tvPlaceFull = itemView.findViewById( R.id.tv_place_full )
tvPrice = itemView.findViewById( R.id.tv_price )
}
fun setData( car: Car ) {
/*
* A API Picasso já se encarrega de realizar o cache de
* imagem e todas as outras tarefas para evitar o vazamento
* de memória.
* */
Picasso
.get()
.load( car.getMainImage() )
.into( rivCarThumb )
rivCarThumb.contentDescription = car.carLabel()
tvNameBrand.text = car.carLabel()
tvShortDescription.text = car.shortDescription
tvYearProdModel.text = car.yearProdModelLabel()
tvKilometers.text = car.kilometersLabel()
tvCarChanges.text = car.carChanges
tvPlace.text = car.seller.type
tvPlaceFull.text = car.seller.cityState
tvPrice.text = car.priceLabel()
}
/*
* Para abrir a atividade de detalhes de carro.
* */
override fun onClick( view: View ) {
val intent = Intent( context, DetailsActivity::class.java )
intent.putExtra( Car.KEY, cars[ adapterPosition ] )
context.startActivity( intent )
}
}
}
A classe é simples, tem bastante código boilerplate e pouco código especifico de domínio.
Atividade principal
Para a MainActivity vamos também iniciar com os arquivos XML. Primeiro o arquivo que contém o RecyclerView, /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_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay" />
</android.support.design.widget.AppBarLayout>
<android.support.v7.widget.RecyclerView
android:id="@+id/rv_vehicles"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white"
android:paddingBottom="6dp"
android:paddingLeft="10dp"
android:paddingRight="10dp"
android:paddingTop="6dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</android.support.design.widget.CoordinatorLayout>
Então o diagrama do layout anterior:
Agora o XML de menu que será carregado junto ao menu gaveta do layout da atividade principal, /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_car_sale"
android:checked="true"
android:icon="@drawable/ic_car_sale"
android:title="@string/nav_item_cars_on_sale" />
<item
android:id="@+id/nav_popular_car"
android:icon="@drawable/ic_popular_car"
android:title="@string/nav_item_popular_cars" />
<item
android:id="@+id/nav_foreign_car"
android:icon="@drawable/ic_foreign_car"
android:title="@string/nav_item_foreign_cars" />
<item
android:id="@+id/nav_motorcycle_sale"
android:icon="@drawable/ic_motorcycle_sale"
android:title="@string/nav_item_motorcycles_on_sale" />
<item
android:id="@+id/nav_top_sales"
android:icon="@drawable/ic_top_sales"
android:title="@string/nav_item_top_sales" />
</group>
</menu>
Então o diagrama do layout de menu anterior:
Agora o XML do layout que contém todos os outros XMLs da atividade principal, /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"
android:background="@color/colorBgNavigationView"
app:itemBackground="@drawable/nav_item_background"
app:itemIconTint="@color/nav_icon_text"
app:itemTextColor="@color/nav_icon_text"
app:menu="@menu/activity_main_drawer" />
</android.support.v4.widget.DrawerLayout>
Antes de apresentarmos o diagrama do layout anterior, vamos aos arquivos XML vinculados a NavigationView e que permitem a correta apresentação, de acordo com o protótipo estático do projeto, dos itens de menu gaveta.
Primeiro o arquivo /res/color/nav_icon_text.xml (crie o folder /color caso ainda não o tenha em seu /res):
<?xml version="1.0" encoding="utf-8"?>
<selector
xmlns:android="http://schemas.android.com/apk/res/android">
<!--
A ordem dos itens de um arquivo <selector> deve ser
seguida de forma estrita, pois caso contrário os efeitos
esperados não ocorrerão.
-->
<!-- Estado "Selecionado" -->
<item
android:color="@android:color/white"
android:state_checked="true" />
<!-- Estado "Normal", não selecionado -->
<item android:color="@color/colorItemNormal" />
</selector>
Para a definição de cor de texto e de ícone temos de trabalhar com o atributo android:color.
Agora o arquivo drawable /res/drawable/nav_item_background.xml:
<?xml version="1.0" encoding="utf-8"?>
<selector
xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Estado "Selecionado" -->
<item
android:drawable="@color/colorPrimary"
android:state_checked="true" />
<!-- Estado "Normal", não selecionado -->
<item android:drawable="@android:color/transparent" />
</selector>
Diferente da regra de negócio de cores para textos e ícones, para a cor de background de um shape devemos utilizar o atributo android:drawable.
Com os dois arquivos XML anteriores conseguimos a seguinte visualização em menu gaveta:
Ainda temos o XML de menu que é carregado na atividade principal, segue /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_search"
android:icon="@drawable/ic_search"
android:orderInCategory="100"
android:title="@string/action_search"
app:showAsAction="always" />
<item
android:id="@+id/action_cars"
android:icon="@drawable/ic_cars"
android:orderInCategory="100"
android:title="@string/action_cars"
app:showAsAction="always" />
<item
android:id="@+id/action_motorcycles"
android:icon="@drawable/ic_motorcycles"
android:orderInCategory="100"
android:title="@string/action_motorcycles"
app:showAsAction="always" />
</menu>
A seguir o diagrama do menu anterior:
E então o diagrama do layout activity_main.xml:
Por fim o código Kotlin da MainActivity:
class MainActivity : AppCompatActivity(),
NavigationView.OnNavigationItemSelectedListener {
override fun onCreate( savedInstanceState: Bundle? ) {
super.onCreate( savedInstanceState )
setContentView( R.layout.activity_main )
setSupportActionBar( toolbar )
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 )
initRecyclerView()
}
/*
* Método com os códigos de inicialização do RecyclerView.
* */
private fun initRecyclerView(){
rv_vehicles.setHasFixedSize( true )
val layoutManager = LinearLayoutManager( this )
rv_vehicles.layoutManager = layoutManager
rv_vehicles.adapter = CarsAdapter( this, Database.getCars() )
}
/*
* O código dentro de onResume() é um hackcode para que o título da
* toolbar seja atualizado de forma efetiva.
* */
override fun onResume() {
super.onResume()
toolbar.title = getString( R.string.page_name )
}
override fun onBackPressed() {
if( drawer_layout.isDrawerOpen( GravityCompat.START ) ) {
drawer_layout.closeDrawer( GravityCompat.START )
}
else {
super.onBackPressed()
}
}
override fun onCreateOptionsMenu( menu: Menu ): Boolean {
menuInflater.inflate( R.menu.main, menu )
return true
}
override fun onNavigationItemSelected( item: MenuItem ): Boolean {
drawer_layout.closeDrawer( GravityCompat.START )
/*
* Retornando false para que o item selecionado se mantenha
* o mesmo, isso pois o projeto de exemplo funcionará apenas
* para Carros em oferta.
* */
return false
}
}
Não deixe de ler os comentários.
Fragmento de apresentação de imagens
Antes de irmos aos códigos da atividade de detalhes, vamos primeiro a classe ImageDialogFragment, classe responsável por apresentar o álbum do carro em foco.
Primeiro o layout desse DialogFragment, /res/layout/fragment_image_dialog.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/black"
android:orientation="vertical"
tools:context=".DetailsActivity"
tools:showIn="@layout/activity_details">
<ImageView
android:id="@+id/iv_close"
style="@style/AppTheme.DialogButton"
android:layout_gravity="end"
android:layout_marginEnd="20dp"
android:layout_marginRight="20dp"
android:layout_marginTop="20dp"
android:contentDescription="@string/iv_desc_close_dialog"
android:src="@drawable/ic_close" />
<ImageView
android:id="@+id/iv_image"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:scaleType="fitCenter" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="40dp">
<ImageView
android:id="@+id/iv_arrow_left"
style="@style/AppTheme.DialogButton"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_marginLeft="20dp"
android:layout_marginStart="20dp"
android:contentDescription="@string/iv_desc_prev_image"
android:src="@drawable/ic_arrow_left" />
<ImageView
android:id="@+id/iv_arrow_right"
style="@style/AppTheme.DialogButton"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_marginEnd="20dp"
android:layout_marginRight="20dp"
android:contentDescription="@string/iv_desc_next_image"
android:src="@drawable/ic_arrow_right" />
</RelativeLayout>
</LinearLayout>
Então o diagrama do layout anterior:
Assim o código Kotlin de ImageDialogFragment:
class ImageDialogFragment : DialogFragment(), View.OnClickListener {
companion object {
const val KEY = "image_dialog"
/*
* Todas as constantes do projeto são importantes, mas as
* seguintes facilitarão a leitura do código ao invés de
* apenas utilizar "1" ou "-1".
* */
const val COUNT_PLUS = 1
const val COUNT_LESS = -1
}
lateinit var car: Car
var imagePosition = 0
override fun onCreate( savedInstanceState: Bundle? ) {
super.onCreate( savedInstanceState )
/*
* Código que deixa o DialogFragment em modo fullscreen.
* */
setStyle(
DialogFragment.STYLE_NORMAL,
android.R.style.Theme_Black_NoTitleBar_Fullscreen
)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle? ): View? {
/*
* Até o final da invocação deste método ainda não podemos
* acessar as Views do layout fragment_image_dialog com a
* sintaxe permitida pelo plugin kotlin-android-extensions.
* */
return inflater.inflate(
R.layout.fragment_image_dialog,
container, false
)
}
/*
* Os acessos as Views estão no método onResume(), pois somente
* depois de onCreateView() é que podemos utilizar a sintaxe
* do plugin kotlin-android-extensions, sem necessidade de uso
* do método findViewById().
* */
override fun onResume() {
super.onResume()
/*
* Acessando dados enviados de CarAdapter depois do
* acionamento de algum item em lista.
* */
car = arguments!!.getParcelable( Car.KEY ) as Car
imagePosition = arguments!!.getInt( Car.KEY_IMAGE )
iv_close.setOnClickListener( this )
iv_arrow_left.setOnClickListener( this )
iv_arrow_right.setOnClickListener( this )
setImage( false )
}
override fun onClick( view: View ) {
when (view.id) {
R.id.iv_arrow_left -> prevImage()
R.id.iv_arrow_right -> nextImage()
R.id.iv_close -> close()
}
}
private fun prevImage() {
setImage( true, COUNT_LESS )
}
private fun nextImage() {
setImage( true, COUNT_PLUS )
}
private fun setImage( applyCount: Boolean, typeCount: Int = COUNT_PLUS ){
/*
* Verificação se deve ou não ser aplicado o contador a
* propriedade imagePosition, pois logo na abertura do
* DialogFragment não há necessidade de invocar o algoritmo
* contador, posteriormente, na mudança de imagem em tela, o
* contador deve ser invocado para o correto cálculo de qual
* botão passador de imagem poderá ou não ficar em tela.
* */
if( applyCount ){
imagePosition += when( typeCount ){
COUNT_PLUS -> 1
else -> -1
}
}
Picasso
.get()
.load( car.imagesUrl[ imagePosition ] )
.into( iv_image )
verifyButtons()
}
/*
* Verificação se os botões passadores de imagem podem ou não
* permanecer em tela de acordo com a posição atual da imagem
* em exibição.
* */
private fun verifyButtons() {
iv_arrow_left.visibility =
if( imagePosition == 0 )
View.GONE
else
View.VISIBLE
iv_arrow_right.visibility =
if( imagePosition == car.imagesUrl.size - 1)
View.GONE
else
View.VISIBLE
}
private fun close() {
dismiss()
}
}
Assim podemos ir a última atividade do projeto.
Atividade de detalhes de carro à venda
Para a atividade de detalhes, vamos iniciar com o layout de conteúdo, /res/layout/content_details.xml:
<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.NestedScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white"
android:fillViewport="true"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:context=".DetailsActivity"
tools:showIn="@layout/activity_details">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white"
android:padding="@dimen/default_margin_padding">
<com.makeramen.roundedimageview.RoundedImageView
android:id="@+id/iv_car"
style="@style/AppTheme.DetailsRoundedImageView"
android:layout_width="190dp"
android:layout_height="230dp"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
app:riv_border_color="@android:color/transparent"
app:riv_border_width="2dp"
app:riv_corner_radius="5dp"
app:riv_mutate_background="true"
app:riv_oval="false"
app:riv_tile_mode="clamp" />
<View
android:id="@+id/v_vertical_line"
android:layout_width="0.8dp"
android:layout_height="wrap_content"
android:layout_alignBottom="@+id/iv_car"
android:layout_alignTop="@+id/iv_car"
android:layout_marginEnd="@dimen/default_margin_padding"
android:layout_marginLeft="@dimen/default_margin_padding"
android:layout_marginRight="@dimen/default_margin_padding"
android:layout_marginStart="@dimen/default_margin_padding"
android:layout_toEndOf="@+id/iv_car"
android:layout_toRightOf="@+id/iv_car"
android:background="@color/colorLightGrey" />
<TextView
android:id="@+id/tv_short_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@+id/v_vertical_line"
android:layout_toEndOf="@+id/v_vertical_line"
android:layout_toRightOf="@+id/v_vertical_line"
android:textAllCaps="true"
android:textColor="@color/colorDarkGrey"
android:textSize="16sp" />
<ImageView
android:id="@+id/iv_year_prod_model"
style="@style/AppTheme.DetailsInfoIcon"
android:layout_alignLeft="@+id/tv_short_description"
android:layout_alignStart="@+id/tv_short_description"
android:layout_below="@+id/tv_short_description"
android:layout_marginTop="8dp"
android:contentDescription="@string/iv_desc_year_prod_model"
android:src="@drawable/ic_year_prod_model" />
<TextView
android:id="@+id/tv_year_prod_model"
style="@style/AppTheme.DetailsTextIcon"
android:layout_alignTop="@+id/iv_year_prod_model"
android:layout_toEndOf="@+id/iv_year_prod_model"
android:layout_toRightOf="@+id/iv_year_prod_model" />
<ImageView
android:id="@+id/iv_kilometers"
style="@style/AppTheme.DetailsInfoIcon"
android:layout_alignLeft="@+id/tv_short_description"
android:layout_alignStart="@+id/tv_short_description"
android:layout_below="@+id/iv_year_prod_model"
android:contentDescription="@string/iv_desc_kilometers"
android:src="@drawable/ic_kilometers" />
<TextView
android:id="@+id/tv_kilometers"
style="@style/AppTheme.DetailsTextIcon"
android:layout_alignTop="@+id/iv_kilometers"
android:layout_toEndOf="@+id/iv_kilometers"
android:layout_toRightOf="@+id/iv_kilometers" />
<ImageView
android:id="@+id/iv_car_changes"
style="@style/AppTheme.DetailsInfoIcon"
android:layout_alignLeft="@+id/tv_short_description"
android:layout_alignStart="@+id/tv_short_description"
android:layout_below="@+id/iv_kilometers"
android:contentDescription="@string/iv_desc_car_changes"
android:src="@drawable/ic_car_changes" />
<TextView
android:id="@+id/tv_car_changes"
style="@style/AppTheme.DetailsTextIcon"
android:layout_alignTop="@+id/iv_car_changes"
android:layout_toEndOf="@+id/iv_car_changes"
android:layout_toRightOf="@+id/iv_car_changes" />
<View
android:id="@+id/h_vertical_line_01"
android:layout_width="wrap_content"
android:layout_height="0.8dp"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_below="@+id/iv_car_changes"
android:layout_marginBottom="@dimen/default_margin_padding"
android:layout_marginTop="@dimen/default_margin_padding"
android:layout_toEndOf="@+id/v_vertical_line"
android:layout_toRightOf="@+id/v_vertical_line"
android:background="@color/colorLightGrey" />
<ImageView
android:id="@+id/iv_place"
style="@style/AppTheme.DetailsInfoIcon"
android:layout_below="@+id/h_vertical_line_01"
android:layout_marginTop="0dp"
android:layout_toEndOf="@+id/v_vertical_line"
android:layout_toRightOf="@+id/v_vertical_line"
android:contentDescription="@string/iv_desc_place"
android:src="@drawable/ic_place" />
<TextView
android:id="@+id/tv_place"
style="@style/AppTheme.DetailsTextIcon"
android:layout_alignTop="@+id/iv_place"
android:layout_toEndOf="@+id/iv_place"
android:layout_toRightOf="@+id/iv_place" />
<TextView
android:id="@+id/tv_place_full"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/tv_place"
android:layout_alignStart="@+id/tv_place"
android:layout_below="@+id/tv_place"
android:textColor="@color/colorSubText"
android:textSize="13sp" />
<TextView
android:id="@+id/tv_phone"
style="@style/AppTheme.DetailsTextIcon"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_below="@+id/tv_place_full"
android:layout_marginTop="8dp"
android:layout_toEndOf="@+id/v_vertical_line"
android:layout_toRightOf="@+id/v_vertical_line"
android:gravity="center"
android:onClick="callSeller"
android:textColor="@color/colorLink" />
<View
android:id="@+id/h_vertical_line_02"
android:layout_width="wrap_content"
android:layout_height="0.8dp"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_below="@+id/v_vertical_line"
android:layout_marginLeft="@dimen/default_margin_padding"
android:layout_marginStart="@dimen/default_margin_padding"
android:layout_marginTop="@dimen/default_margin_padding"
android:layout_toEndOf="@+id/iv_car"
android:layout_toRightOf="@+id/iv_car"
android:background="@color/colorLightGrey" />
<LinearLayout
android:id="@+id/ll_album_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignEnd="@+id/v_vertical_line"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignRight="@+id/v_vertical_line"
android:layout_below="@+id/iv_car"
android:layout_marginEnd="@dimen/default_margin_padding"
android:layout_marginRight="@dimen/default_margin_padding"
android:layout_marginTop="@dimen/default_margin_padding"
android:background="@drawable/item_car_background"
android:orientation="horizontal"
android:padding="6dp">
<com.makeramen.roundedimageview.RoundedImageView
android:id="@+id/iv_car_thumb_01"
style="@style/AppTheme.DetailsRoundedImageView"
android:layout_marginEnd="5dp"
android:layout_marginRight="5dp"
app:riv_border_color="@android:color/transparent"
app:riv_border_width="2dp"
app:riv_corner_radius="5dp"
app:riv_mutate_background="true"
app:riv_oval="false"
app:riv_tile_mode="clamp" />
<com.makeramen.roundedimageview.RoundedImageView
android:id="@+id/iv_car_thumb_02"
style="@style/AppTheme.DetailsRoundedImageView"
android:layout_marginEnd="5dp"
android:layout_marginRight="5dp"
app:riv_border_color="@android:color/transparent"
app:riv_border_width="2dp"
app:riv_corner_radius="5dp"
app:riv_mutate_background="true"
app:riv_oval="false"
app:riv_tile_mode="clamp" />
<com.makeramen.roundedimageview.RoundedImageView
android:id="@+id/iv_car_thumb_03"
style="@style/AppTheme.DetailsRoundedImageView"
android:layout_marginEnd="5dp"
android:layout_marginRight="5dp"
app:riv_border_color="@android:color/transparent"
app:riv_border_width="2dp"
app:riv_corner_radius="5dp"
app:riv_mutate_background="true"
app:riv_oval="false"
app:riv_tile_mode="clamp" />
<com.makeramen.roundedimageview.RoundedImageView
android:id="@+id/iv_car_thumb_04"
style="@style/AppTheme.DetailsRoundedImageView"
app:riv_border_color="@android:color/transparent"
app:riv_border_width="2dp"
app:riv_corner_radius="5dp"
app:riv_mutate_background="true"
app:riv_oval="false"
app:riv_tile_mode="clamp" />
</LinearLayout>
<TextView
android:id="@+id/tv_price"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@+id/ll_album_container"
android:layout_alignEnd="@+id/h_vertical_line_02"
android:layout_alignRight="@+id/h_vertical_line_02"
android:layout_below="@+id/h_vertical_line_02"
android:layout_marginTop="@dimen/default_margin_padding"
android:layout_toEndOf="@+id/ll_album_container"
android:layout_toRightOf="@+id/ll_album_container"
android:background="@drawable/price_background"
android:gravity="center"
android:textColor="@android:color/white"
android:textSize="20sp" />
<ImageView
android:id="@+id/iv_circle_01"
style="@style/AppTheme.DetailsCircleIcon"
android:layout_below="@+id/ll_album_container" />
<View
android:id="@+id/h_vertical_line_03"
style="@style/AppTheme.DetailsHorizontalLine"
android:layout_alignLeft="@+id/iv_circle_01"
android:layout_alignStart="@+id/iv_circle_01"
android:layout_alignTop="@+id/iv_circle_01"
android:layout_toLeftOf="@+id/tv_more_info"
android:layout_toStartOf="@+id/tv_more_info" />
<TextView
android:id="@+id/tv_more_info"
style="@style/AppTheme.DetailsTitleInfo"
android:layout_alignTop="@+id/iv_circle_01"
android:text="@string/label_more_info" />
<LinearLayout
android:id="@+id/ll_container_more_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/iv_circle_01"
android:layout_marginTop="14dp"
android:baselineAligned="false"
android:orientation="horizontal">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
style="@style/AppTheme.DetailsLabelInfo"
android:text="@string/label_color" />
<TextView
android:id="@+id/tv_color_value"
style="@style/AppTheme.DetailsTextInfo" />
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
style="@style/AppTheme.DetailsLabelInfo"
android:text="@string/label_bodywork" />
<TextView
android:id="@+id/tv_bodywork_value"
style="@style/AppTheme.DetailsTextInfo" />
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
style="@style/AppTheme.DetailsLabelInfo"
android:text="@string/label_plate" />
<TextView
android:id="@+id/tv_plate_value"
style="@style/AppTheme.DetailsTextInfo" />
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
style="@style/AppTheme.DetailsLabelInfo"
android:text="@string/label_gas" />
<TextView
android:id="@+id/tv_gas_value"
style="@style/AppTheme.DetailsTextInfo" />
</LinearLayout>
</LinearLayout>
<ImageView
android:id="@+id/iv_circle_02"
style="@style/AppTheme.DetailsCircleIcon"
android:layout_below="@+id/ll_container_more_info" />
<View
android:id="@+id/h_vertical_line_04"
style="@style/AppTheme.DetailsHorizontalLine"
android:layout_alignLeft="@+id/iv_circle_02"
android:layout_alignStart="@+id/iv_circle_02"
android:layout_alignTop="@+id/iv_circle_02"
android:layout_toLeftOf="@+id/tv_seller_obs"
android:layout_toStartOf="@+id/tv_seller_obs" />
<TextView
android:id="@+id/tv_seller_obs"
style="@style/AppTheme.DetailsTitleInfo"
android:layout_alignTop="@+id/iv_circle_02"
android:text="@string/label_seller_obs" />
<TextView
android:id="@+id/tv_full_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/iv_circle_02"
android:layout_marginTop="14dp"
android:textColor="@color/colorDarkGrey"
android:textSize="14sp" />
</RelativeLayout>
</android.support.v4.widget.NestedScrollView>
Note que dois arquivos XML drawables estão sendo utilizados no layout anterior. Um nós já abordamos anteriormente, o drawable /res/drawable/item_car_background.xml.
O outro é o /res/drawable/price_background.xml:
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<!-- Cor de background da View. -->
<solid android:color="@color/colorPrimary" />
<!-- Cor e largura de borda da View. -->
<stroke
android:width="1dp"
android:color="@color/colorPrimary" />
<!-- Curvatura dos cantos da View. -->
<corners android:radius="5dp" />
</shape>
Com o XML anterior conseguimos a seguinte visualização em preço:
Então o diagrama de content_details.xml:
Agora o arquivo de menu também carregado na DetailsActivity, o arquivo /res/menu/details.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_call"
android:icon="@drawable/ic_phone"
android:orderInCategory="100"
android:title="@string/action_call"
app:showAsAction="always" />
</menu>
Assim o layout principal, /res/layout/activity_details.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=".DetailsActivity">
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay" />
</android.support.design.widget.AppBarLayout>
<include layout="@layout/content_details" />
</android.support.design.widget.CoordinatorLayout>
Então o digrama de activity_details.xml:
Por fim o código Kotlin de DetailsActivity:
class DetailsActivity : AppCompatActivity() {
lateinit var car: Car
override fun onCreate( savedInstanceState: Bundle? ) {
super.onCreate( savedInstanceState )
setContentView( R.layout.activity_details )
setSupportActionBar( toolbar )
supportActionBar?.setDisplayHomeAsUpEnabled( true )
car = intent.getParcelableExtra( Car.KEY )
/*
* Dados do carro.
* */
tv_short_description.text = car.shortDescription
tv_year_prod_model.text = car.yearProdModelLabel()
tv_kilometers.text = car.kilometersLabel()
tv_car_changes.text = car.carChanges
tv_price.text = car.priceLabel()
/*
* Dados do vendedor.
* */
tv_place.text = car.seller.type
tv_place_full.text = car.seller.cityState
tv_phone.text = car.seller.phoneLabel()
/*
* Mais informações
* */
tv_color_value.text = car.moreInfo.color
tv_bodywork_value.text = car.moreInfo.bodyWork
tv_plate_value.text = car.moreInfo.finalPlate.toString()
tv_gas_value.text = car.moreInfo.gas
tv_full_description.text = car.moreInfo.fullDescription
/*
* Imagens.
* */
/*
* Principal
* */
Picasso.get().load( car.getMainImage() ).into( iv_car )
Picasso.get().load( car.imagesUrl[1] ).into( iv_car_thumb_01 )
Picasso.get().load( car.imagesUrl[2] ).into( iv_car_thumb_02 )
Picasso.get().load( car.imagesUrl[3] ).into( iv_car_thumb_03 )
Picasso.get().load( car.imagesUrl[4] ).into( iv_car_thumb_04 )
}
override fun onResume() {
super.onResume()
toolbar.title = car.carLabel()
}
override fun onCreateOptionsMenu( menu: Menu): Boolean {
menuInflater.inflate( R.menu.details, menu )
return true
}
override fun onOptionsItemSelected( item: MenuItem ): Boolean {
/*
* Mesmo tendo somente uma opção no menu details.xml, ainda
* é preciso o condicional neste método, pois a seta de
* "back button" ao topo esquerdo da tela também invocará o
* onOptionsItemSelected().
* */
if( item.itemId == R.id.action_call ){
callSeller( null )
}
return super.onOptionsItemSelected( item )
}
/*
* Método responsável por invocar o aplicativo de ligação, já com o
* número do vendedor pronto para ser chamado.
* */
fun callSeller( view: View? ){
val intent = Intent(
Intent.ACTION_DIAL,
Uri.parse( "tel:" + car.seller.phone )
)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
startActivity( intent )
}
/*
* Método responsável por abrir o DialogFragment que controla a
* apresentação das imagens em modo "álbum".
* */
fun openAlbum( view: View ){
val transaction = supportFragmentManager.beginTransaction()
val fragment = ImageDialogFragment()
val bundle = Bundle()
/*
* Obtendo o posicionamento da imagem acionada, pois é ela
* que inicialmente será carregada no ImageDialogFragment.
* */
val imagePosition = when( view.id ){
R.id.iv_car_thumb_01 -> 1
R.id.iv_car_thumb_02 -> 2
R.id.iv_car_thumb_03 -> 3
R.id.iv_car_thumb_04 -> 4
else -> 0
}
bundle.putParcelable( Car.KEY, car )
bundle.putInt( Car.KEY_IMAGE, imagePosition )
fragment.arguments = bundle
fragment.show( transaction, ImageDialogFragment.KEY )
}
}
Com isso podemos partir para a evolução do projeto.
Colocando a funcionalidade de zoom nas imagens de álbum
Nossa meta aqui é simples:
- Permitir que em ImageDialogFragment os usuários consigam ampliar as imagens com toques de zoom ou com o gesto pinça.
Com o uso da PhotoView API a atualização será trivial e nem mesmo os comuns fluxogramas apresentados em artigos aqui do Blog serão necessários.
Novas telas de protótipo estático
A seguir as novas imagens do protótipo estático do aplicativo:
Caixa de diálogo com zoom 1 | Caixa de diálogo com zoom 2 |
Com isso podemos partir para a atualização do projeto.
Atualizando o Gradle
No Gradle Project Level, ou build.gradle (Project: MobMotors), vamos adicionar o repositório de acesso a PhotoView API:
...
allprojects {
repositories {
google()
jcenter()
/* Para a RoundedImageView */
mavenCentral()
/* Para a PhotoView API */
maven { url "https://jitpack.io" }
}
}
...
Agora a atualização do Gradle App Level, ou build.gradle (Module: app):
...
dependencies {
...
implementation 'com.github.chrisbanes:PhotoView:2.1.4'
}
Sincronize o projeto.
Atualizando o layout do fragmento de imagens
Vamos atualizar o layout fragment_image_dialog.xml removendo o ImageView principal e colocando, com as mesmas configurações, uma PhotoView:
...
<com.github.chrisbanes.photoview.PhotoView
android:id="@+id/iv_image"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginBottom="20dp"
android:layout_marginTop="20dp"
android:layout_weight="1"
android:scaleType="fitCenter" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="40dp">
...
Os layout_marginTop e layout_marginBottom estão sendo utilizados para que quando a imagem estiver em zoom ela não fique totalmente rente aos botões presentes na caixa de diálogo de imagem.
Ampliando a capacidade de zoom máximo da imagem
Na classe ImageDialogFragment, mais precisamente no método onResume() dela, adicione o código a seguir em destaque:
override fun onResume() {
...
/*
* Ampliando a capacidade de zoom máximo na imagem.
* */
iv_image.maximumScale = 5.0F
}
Assim podemos partir para os testes.
Testes e resultados
Acesse o menu de topo do Android Studio. Acione Build e em seguida Rebuild project. Assim execute o aplicativo em seu emulador ou aparelho de testes.
Iniciando o app Android e navegando até a caixa de diálogo de imagens, temos:
Assim terminamos o estudo da PhotoView API.
Não deixe de se inscrever na 📩 lista de emails do Blog, logo acima ou ao lado, para receber em primeira mão os conteúdos exclusivos sobre o dev Android.
Se inscreva também no canal do Blog em: YouTube Thiengo.
Slides
Abaixo os slides com a apresentação completa da Android PhotoView API:
Vídeos
A seguir os vídeos com a atualização passo a passo do projeto de aplicativo Android para vendas de carros:
Para acessar o projeto de exemplo, acesse o GitHub dele em: https://github.com/viniciusthiengo/mob-motors-android.
Conclusão
Poucas linhas de código, não necessitando nenhuma linha em código dinâmico (Kotlin / Java), a PhotoView API entrega por completo a funcionalidade de zoom nos ImageViews que precisam desta característica em projeto.
Poderíamos esperar alguma atualização nativa no Android que já integra-se a capacidade de zoom ao ImageView, mas tendo em mente que o criador da PhotoView é engenheiro de desenvolvimento no Google Android e que a API continua sendo evoluída, é provável que essa melhoria não ocorra por agora.
Assim finalizamos o artigo. Caso você tenha alguma dica ou dúvida sobre melhorias no ImageView Android, deixe logo abaixo nos comentários.
E se 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
Documentação oficial PhotoView
How to make a phone call using intent in Android? - Resposta de Osama Ibrahim
Full Screen DialogFragment in Android - Resposta de Khemraj
How can I use a style within another style for a button? - Resposta de Hossam Oukli
Comentários Facebook