PhotoView Android Para a Completa Implementação de Zoom

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 /PhotoView Android Para a Completa Implementação de Zoom

PhotoView Android Para a Completa Implementação de Zoom

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

Tudo bem?

Neste artigo vamos 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.

Animação com a Android PhotoView API em uso

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

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:

Simples animação com a Android PhotoView API

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:

Animação da PhotoView com carregamento via Picasso API

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:

Animação PhotoView com escala de zoom alterada

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:

Animação PhotoView com escala ajustada em tempo de execução

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:

Animação PhotoView com a duração de zoom em 6 segundos

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()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:

Animação da PhotoView com rotação de imagem aplicada

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:

Animação PhotoView com exemplo de drag de imagem

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 entrada

Tela de listagem de carros

 Tela de listagem de carros

Menu gaveta aberto

 Menu gaveta aberto

Tela de detalhes de carro

Tela de detalhes de carro

Caixa de diálogo de álbum

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:

Arquitetura de projeto no Android Studio

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:

Bordas curvadas do item de lista

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:

textView de preço decorado

Com isso podemos ir ao diagrama do layout car.xml:

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:

Diagrama do layout app_bar_main.xml

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:

Diagrama do menu activity_main_drawer.xml

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:

Itens 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:

Diagrama do menu main.xml

E então o diagrama do layout activity_main.xml:

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:

Diagrama do layout fragment_image_dialog.xml

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:

textView de preço decorado

Então o diagrama de content_details.xml:

Diagrama do layout 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:

Diagrama do layout 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 1

Caixa de diálogo com zoom 2

 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_marginToplayout_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:

Animação de teste com a Android PhotoView API

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

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

Android Studio: Instalação, Configuração e OtimizaçãoAndroid Studio: Instalação, Configuração e OtimizaçãoAndroid
Trabalhando Análise Qualitativa em seu Aplicativo AndroidTrabalhando Análise Qualitativa em seu Aplicativo AndroidAndroid
Utilizando Intenções Para Mapas de Alta Qualidade no AndroidUtilizando Intenções Para Mapas de Alta Qualidade no AndroidAndroid
Lottie API Para Animações no AndroidLottie API Para Animações no AndroidAndroid

Compartilhar

Comentários Facebook

Comentários Blog (10)

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...
Luan Silva (2) (0)
04/09/2018
Eai Thiengo, tudo certo?

Primeiro obrigado por sempre estar dedicando tempo e repassando conhecimento, ajuda demais!

Queria saber se é possível você fazer qualquer dia um post explicando sobre RxJava (ou até ja RxKotlin).  
Ta sendo bastante usado, e bastante elogiado atualmente, mas a curva de aprendizagem é bem ingrime pra alguns.
Se fosse possivel, ajudaria muito!

Abraços.
Responder
Vinícius Thiengo (1) (0)
05/09/2018
Luan, tudo bem aqui.

Obrigado pela dica de conteúdo.

Vou sim desenvolver algo sobre Reactive Programming e posteriormente sobre RxJava / RxKotlin, já está na hora de ter algo sobre aqui no Blog.

Abraço.
Responder
Daniel (1) (0)
28/08/2018
Olá amigo pode me ajudar? E urgente! Estou com um probleminha que é o seguinte: AppComponetFactory sempre aparece quando vou construir o aplicativo.
Responder
Vinícius Thiengo (0) (0)
28/08/2018
Daniel, tudo bem?

Ok, mas para qual aplicativo está sendo apresentado o problema com o AppComponentFactory? O do exemplo acima, MobMotors?

Se possível, coloque aqui a mensagem de erro mostrada nos logs do Android Studio.

Abraço.
Responder
28/08/2018
AIDE Pode me mandar seu e-mail?
Responder
Vinícius Thiengo (0) (0)
28/08/2018
Certo,

Pode contactar pelo email oficial do Blog e canal: thiengocalopsita@gmail.com

Abraço.
Responder
Avelino (1) (0)
27/08/2018
Muito bom Thiengo!
Como seria integrar esse projeto com serviço REST ? tens algum exemplo no seu blog?
Responder
Vinícius Thiengo (0) (0)
27/08/2018
Avelino, tudo bem?

O algoritmo de integração com um servidor REST ainda não tenho, mas fica aqui como dica para conteúdos futuros.

O mais próximo que tenho disso é um aplicativo Android, construído em Java, se comunicando com um servidor de conteúdos, servidor também responsável pelas notificações push.

Coloquei os links desse conteúdo no comentário do João Arthur, logo abaixo, mas vou coloca-los aqui novamente. Segue:

-> FCM Android - Domínio do Problema, Implementação e Testes Com Servidor de Aplicativo [Parte 1]: https://www.thiengo.com.br/fcm-android-dominio-do-problema-implementacao-e-testes-com-servidor-de-aplicativo-parte-1

-> FCM Android - Relatório e Notificação Por Tópicos [Parte 2]: https://www.thiengo.com.br/fcm-android-relatorio-e-notificacao-por-topicos-parte-2

-> FCM Android - Notificação Personalizada com NotificationCompat [Parte 3]: https://www.thiengo.com.br/fcm-android-notificacao-personalizada-com-notificationcompat-parte-3

O app Android é desenvolvido no Android Studio e o lado servidor no PHPStorm. Back-end com:

-> Apache;
-> MySQL;
-> PHP.

Abraço.
Responder
João Arthur (1) (0)
27/08/2018
Bom dia Thiengo você ainda tem algum projeto android em Java ? vi que os últimos você tem feito em Kotlin, qual o mais recente feito no Java ?
Responder
Vinícius Thiengo (0) (0)
27/08/2018
João Arthur, tudo bem?

Os últimos conteúdos em Java foram sobre o Android Firebase Cloud Messaging (FCM):

-> FCM Android - Domínio do Problema, Implementação e Testes Com Servidor de Aplicativo [Parte 1]: https://www.thiengo.com.br/fcm-android-dominio-do-problema-implementacao-e-testes-com-servidor-de-aplicativo-parte-1

-> FCM Android - Relatório e Notificação Por Tópicos [Parte 2]: https://www.thiengo.com.br/fcm-android-relatorio-e-notificacao-por-topicos-parte-2

-> FCM Android - Notificação Personalizada com NotificationCompat [Parte 3]: https://www.thiengo.com.br/fcm-android-notificacao-personalizada-com-notificationcompat-parte-3

O Kotlin também é JVM, os códigos são bem similares ao Java e podem estar dentro de projetos Java, e vice-versa.

Abraço.
Responder