Colocando Telas de Introdução em Seu Aplicativo Android
(14067) (7)
CategoriasAndroid, Design, Protótipo
AutorVinícius Thiengo
Vídeo aulas186
Tempo15 horas
ExercíciosSim
CertificadoSim
CategoriaEngenharia de Software
Autor(es)Vaughn Vernon
EditoraAlta Books
Edição1ª
Ano2024
Páginas160
Tudo bem?
Neste artigo vamos trabalhar a funcionalidade de "telas de introdução" em aplicativos Android. Algo que já é comum em muitos apps e essencial em muitos domínios de problema.
Seguindo o princípio do "não reinventar a roda", vamos utilizar uma API de terceiro para a implementação das telas de introdução, API que é muito bem aceita pela comunidade de desenvolvedores Android e tem um suporte excelente as versões do SO no mercado, cobrindo uma fatia de 99.2% dos usuários.
Aqui no artigo essa API será utilizada em um simples aplicativo, social, de perguntas:
Vamos utilizar a linguagem Kotlin, logo, se esse é seu primeiro contato com essa linguagem, é importante que estude antes do artigo a seguir: Kotlin Android, Entendendo e Primeiro Projeto.
Antes de prosseguir, não esqueça de se inscrever 📫na lista de e-mails do Blog para receber, em primeira mão, todos os conteúdos de desenvolvimento Android exclusivos aqui do Blog.
Abaixo os tópicos que estaremos abordando:
- Atividade de introdução vs ShowCaseScreen vs TapTargets:
- Projeto Android de exemplo:
- Atualizando o aplicativo para trabalho com Atividade de Introdução:
- Atualizando o Gradle App Level;
- Criando a atividade de introdução;
- Configuração do primeiro SlideFragment;
- Configuração do segundo SlideFragment, com solicitação de permissão obrigatória em tempo de execução;
- Configuração do terceiro SlideFragment, com solicitação de permissão opcional em tempo de execução;
- Configuração do quarto SlideFragment, slide customizado;
- Configuração do SharedPreferences para a lógica da IntroActivity;
- Testes e resultados;
- Parallax;
- Animação de transição de slides;
- Pular button, esconder o back button e transição alpha no último slide;
- Slide com imagem de background.
- Vídeo com a implementação da API de atividade de introdução;
- Conclusão;
- Fontes.
Atividade de introdução vs ShowCaseScreen vs TapTargets
Uma atividade de introdução pode sim ser utilizada como um tutorial de uso do aplicativo, mas para isso temos a ShowCaseScreen ou ShowCaseView, que é somente para isso: tutorial de uso.
A seguir o exemplo de uma ShowCase:
A atividade de introdução, ou telas de introdução, é utilizada muitas vezes para apresentar os benefícios do aplicativo e também algumas regras de negócios dele, como por exemplo: a tela de "termos e condições de uso", permitindo o usuário prosseguir somente depois de acordar com os termos do app.
A seguir um exemplo de um slide de uma atividade de introdução:
A TapTarget foi introduzida no Android junto ao Material Design e essa é muito similar a ShowCase, podemos até mesmo dizer que ela é a evolução desta última. A TapTarget utiliza todas as definições de layout do Material Design.
Segue um exemplo de uma tap:
Resumo até aqui: não há uma opção melhor do que a outra, ambas são úteis e podem ser utilizadas no mesmo aplicativo, exceto a ShowCaseScreen e a TapTarget, não faria sentido ter ambas no mesmo app.
Library de terceiro
Por que dessa vez não será construída a funcionalidade em estudo desde o zero, vulgo: from scratch?
Isso, pois não é viável implementar do zero uma característica que já está pronta partindo de uma API pública da comunidade.
Obviamente que para o uso correto de alguma API devemos primeiro ver qual o tipo de licença em uso e se é compatível com a forma de distribuição de nosso app.
Verificar também se a API é consistente, ou seja, se realmente funciona de acordo com o informado na documentação e se atende as necessidades de nosso aplicativo.
Um bom indício de que a API é boa é o número de estrelas que ela tem no GitHub, até o momento da construção deste artigo eu ainda não tive problemas de consistência e qualidade de APIs quando elas tinham mais do que 1000 estrelas.
De qualquer forma, é muito importante o teste completo da library, isso, pois em alguns casos ela não trabalha como informado, nem mesmo nas issues da API há reclamações sobre o correto funcionamento dela.
No segundo capítulo de meu livro, Receitas Para Desenvolvedores Android, tive de desenvolver do zero a funcionalidade de "Rate App", isso depois de testar as principais APIs públicas e notar que nenhuma delas funcionava como prometido.
No projeto de exemplo deste artigo vamos trabalhar com a API Material-Intro-Screen: https://github.com/TangoAgency/material-intro-screen.
Essa API é muito bem recomendada pela comunidade (número de estrelas, mais do que 1900) e atendeu a todas as características necessárias para o artigo e projeto de exemplo, permitindo até mesmo o trabalho com permissões em tempo de execução.
Ressaltando que aqui trabalharemos somente com atividade de introdução. A instalação e características da API Material-Intro-Screen serão apresentadas no decorrer da configuração desta no projeto de exemplo.
Projeto Android de exemplo
Neste artigo vamos manter a linha de estudo: aplicação da API em foco junto a um projeto de exemplo que simula um aplicativo real, que colocaríamos na Google Play Store.
Dessa vez o projeto é de uma rede mobile de perguntas. Ela não estará funcional, mas será útil para uso de um domínio de problema que realmente ganharia em eficácia caso utilizando uma atividade de introdução.
Para acessar o projeto por completo, digo, até mesmo os arquivos de configuração do Android Studio e imagens, o que não é possível colocar aqui, entre no GitHub dele em: https://github.com/viniciusthiengo/questions-intro-api.
Em seu Android Studio inicie um novo projeto com uma Empty Activity, coloque como nome "Questions?".
Caso esteja com o Android Studio 3+ inicie esse projeto como um Kotlin. Caso contrário, depois de o projeto já ter sido criado, aplique as configurações necessárias para torna-lo um projeto Kotlin.
Ao final dessa primeira parte, sem a API de telas de introdução, teremos o seguinte aplicativo:
E a seguinte estrutura de projeto:
Configurações Gradle
A seguir a configuração do Gradle Project Level, ou build.gradle(Project: Questions):
buildscript {
ext.kotlin_version = '1.1.3'
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.3.3'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects {
repositories {
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
A cima está em destaque as partes que foram adicionadas, além da configuração inicial de um novo projeto Empty Activity no Android, todas relacionadas ao Kotlin.
Assim a configuração 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 25
buildToolsVersion "25.0.2"
defaultConfig {
applicationId "br.com.thiengo.introapitests"
minSdkVersion 15
targetSdkVersion 25
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
compile 'com.android.support:appcompat-v7:25.3.1'
testCompile 'junit:junit:4.12'
compile "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
/* PARA O RECYCLER VIEW */
compile 'com.android.support:design:25.3.1'
/* IMAGEVIEW CIRCULAR */
compile 'de.hdodenhof:circleimageview:2.1.0'
}
repositories {
mavenCentral()
}
Também em destaque, as partes adicionadas devido ao uso do Kotlin, RecyclerView e CircularImageView. Posteriormente voltaremos a essa versão do gradle para adicionar a referência a library Material-Intro-Screen.
Configurações AndroidManifest
O AndroidManifest.xml tem algumas configurações extras, nenhuma ainda relacionada a API Material-Intro-Screen, todas devido ao domínio do problema que estaremos trabalhando. Segue:
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="br.com.thiengo.introapitests">
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<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=".QuestionsActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
As permissões adicionadas são devido ao domínio do problema, simulando uma regra de negócio, no aplicativo, de que é possível utilizar: tecnologias de obtenção de coordenadas no device e também a câmera dele.
Posteriormente você verá que a API em estudo permite que seja colocada a solicitação dessas permissões em tempo de execução, isso com poucas linhas de código. Lembrando que todas elas são dangerous permissions.
Configurações de estilo
Nossos arquivos XML de configurações de estilo são bem simples. Vamos iniciar com o arquivo de configuração de cores, /res/values/colors.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#8BC34A</color>
<color name="colorPrimaryDark">#689F38</color>
<color name="colorAccent">#FF5722</color>
</resources>
Agora o arquivo de String, /res/values/strings.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Questions?</string>
</resources>
E por fim o arquivo XML de definição de estilo, /res/values/styles.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<item name="android:windowBackground">@drawable/background</item>
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
</resources>
Classe de domínio
Teremos uma única classe de domínio. No pacote /domain acrescente a classe Kotlin, Question:
class Question(
val userImage: Int,
val question: String
)
Como informado: o exemplo é simples. Somente essa classe, com essa estrutura, nos atenderá no projeto de rede mobile de perguntas.
Camada de dados, classe Mock
Mesmo com uma única classe de domínio, preferi manter a separação de conceitos e colocar o código de inicialização do projeto de exemplo, digo, de inicialização de dados, em uma classe de dados simulados, mock data.
No pacote /data acrescente a classe Mock:
class Mock {
companion object{
fun generateQuestionList() = listOf<Question>(
Question(R.drawable.person_1, "O que é possível fazer com o Anko Layouts DSL?"),
Question(R.drawable.person_2, "O que é e para que realmente serve o Gradle?"),
Question(R.drawable.person_3, "Qual das linguagens (em opções) é nativa no Android?"),
Question(R.drawable.person_4, "Qual o valor do novo Volvo XC60?"),
Question(R.drawable.person_5, "Quais foram os primeiros exploradores que chegaram a África?"),
Question(R.drawable.person_6, "Quais as principais montadoras de carros asiáticas?"),
Question(R.drawable.person_7, "Qual o estado brasileiro que protege a Ilha da Trindade?"),
Question(R.drawable.person_8, "Quais são os países participantes da copa das confederações?")
)
}
}
Note que companion object está sendo utilizado para que possamos acessar o método generateQuestionList() sem a necessidade de criação de instância.
Classe adaptadora
Para nossa classe adaptadora, que será utilizada em um RecyclerView, iniciaremos com o layout dela, /res/layout/item_question.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:padding="16dp">
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/iv_user"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
app:civ_border_width="0dp" />
<TextView
android:id="@+id/tv_question"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginLeft="12dp"
android:layout_marginStart="12dp"
android:layout_toEndOf="@+id/iv_user"
android:layout_toRightOf="@+id/iv_user"
android:textSize="16sp" />
</RelativeLayout>
Abaixo o diagrama do layout anterior:
Assim o código Kotlin da classe QuestionsAdapter que está presente no pacote /adapter:
class QuestionsAdapter(
private val context: Context,
private val questions: List<Question>) :
RecyclerView.Adapter<QuestionsAdapter.ViewHolder>() {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int) : QuestionsAdapter.ViewHolder {
val v = LayoutInflater
.from(context)
.inflate(R.layout.item_question, parent, false)
return ViewHolder(v)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.setData( questions[position] )
}
override fun getItemCount(): Int {
return questions.size
}
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var ivUser: CircleImageView
var tvQuestion: TextView
init {
ivUser = itemView.findViewById(R.id.iv_user) as CircleImageView
tvQuestion = itemView.findViewById(R.id.tv_question) as TextView
}
fun setData(question: Question) {
ivUser.setImageResource(question.userImage)
tvQuestion.text = question.question
}
}
}
Com exceção do método setData() de ViewHolder, o restante é código padrão que tem de existir em um adapter de RecyclerView.
Atividade principal
Vamos iniciar com o layout de QuestionsActivity, /res/layout/activity_questions.xml:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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/white"
tools:context="br.com.thiengo.introapitests.QuestionsActivity">
<android.support.v7.widget.RecyclerView
android:id="@+id/rv_questions"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentTop="true" />
</RelativeLayout>
A seguir o simples diagrama do layout anterior:
E assim o algoritmo Kotlin de QuestionsActivity:
class QuestionsActivity : AppCompatActivity() {
val questions = ArrayList<Question>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_questions)
questions.addAll( Mock.generateQuestionList() )
initRecycler()
}
private fun initRecycler() {
rv_questions.setHasFixedSize(true)
val mLayoutManager = LinearLayoutManager(this)
rv_questions.layoutManager = mLayoutManager
val divider = DividerItemDecoration( this, mLayoutManager.orientation)
rv_questions.addItemDecoration(divider)
val adapter = QuestionsAdapter(this, questions)
rv_questions.adapter = adapter
}
}
Note que mesmo sabendo que nosso algoritmo trabalha com permissões, digo, elas existem no AndroidManifest.xml, aqui omiti o código de solicitação de permissão em tempo de execução para deixar o exemplo mais leve e também porque a API Material-Intro-Screen nos permiti essa implementação de maneira simples.
Atualizando o aplicativo para trabalho com Atividade de Introdução
Em nosso projeto, como regra de negócio, é importante que os usuários somente consigam acesso a atividade principal depois de terem liberado ao menos as permissões ACCESS_FINE_LOCATION e ACCESS_COARSE_LOCATION, além de terem acordado com os termos e condições de uso do app.
Depois de atendidas as necessidades iniciais, a atividade de introdução, que conterá também algumas dicas de funcionalidades do app, não mais poderá ser apresentada ao usuário.
A seguir o fluxograma de como deve proceder o algoritmo final que também trabalha com a atividade de introdução:
Nossa permissão opcional é a permissão de uso da câmera, android.permission.CAMERA. Nosso último slide não prosseguirá se os termos e condições de uso apresentados não forem acordados pelo usuário.
Para saber se a IntroActivity foi ou não apresentada ao usuário e se ele já acordou com os termos e permissões, utilizaremos um SharedPreferences, pois é uma persistência local, simples e nos atende.
Assim podemos partir para a atualização do projeto.
Atualizando o Gradle App Level
No Gradle App Level, build.gradle (Module: app), adicione a seguinte linha de código, em destaque, para trabalharmos a API Material-Intro-Screen:
...
dependencies {
...
compile 'agency.tango.android:material-intro-screen:0.0.5'
}
...
A versão 0.0.5 era a mais atual quando na construção deste artigo. Na documentação tem a recomendação de sempre utilizarmos a versão mais nova, para isso, não deixe de acompanhar, no GitHub da API, a versão mais recente liberada.
Agora é somente sincronizar o projeto. Note que a API Android mínima aceita, minSdkVersion, com essa nova library é a 15, Ice Cream Sandwich.
Criando a atividade de introdução
Nossa atividade de introdução vai ter o nome relativo ao que ela faz, logo, vamos chama-la de IntroActivity, mas você pode nomea-la como quiser, somente lembre que outros programadores estarão lendo seus algoritmos futuramente, é importante que eles sejam auto-comentados.
Segue:
class IntroActivity : MaterialIntroActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
}
Note que é preciso herdar de MaterialIntroActivity para trabalharmos as características da API adicionada.
Agora vamos atualizar o AndroidManifest.xml para que seja possível abrir primeiro a IntroActivity. Segue atualização:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="br.com.thiengo.introapitests">
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<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=".IntroActivity"
android:theme="@style/Theme.Intro">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".QuestionsActivity" />
</application>
</manifest>
Outro ponto importante: a atividade de introdução tem de referenciar ao tema Theme.Intro que é inserido junto a API Material-Intro-Screen.
Note que sua atividade de introdução não precisa ser a atividade principal, aqui optamos por esse caminho, pois assim nossa lógica de negócio, discutida anteriormente, é implementada de maneira mais simples.
Ainda não temos nenhum slide definido, digo, nenhum SlideFragment para ser apresentado junto ao ViewPager interno da API. Vamos a eles.
Configuração do primeiro SlideFragment
Nosso primeiro slide que será apresentado é apenas informativo, falaremos de algo que é possível com o aplicativo. Também deixaremos que o usuário obtenha um pouco mais de detalhe caso clique no botão "Quais os tipos de perguntas?".
No onCreate() da IntroActivity adicione o código em destaque:
...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
addSlide( SlideFragmentBuilder()
.backgroundColor( R.color.slide_1 )
.buttonsColor( R.color.slide_button )
.image( R.drawable.slide_1 )
.title( resources.getString(R.string.slide_1_title) )
.description( resources.getString(R.string.slide_1_description) )
.build(),
MessageButtonBehaviour( object : View.OnClickListener {
override fun onClick(view: View?) {
showMessage( resources.getString(R.string.slide_1_button_message) )
}
}, resources.getString(R.string.slide_1_button_label)
)
)
}
...
Com SlideFragmentBuilder estamos utilizando uma implementação padrão de slide da API em estudo. Posteriormente criaremos um slide customizado.
Todos os métodos são bem intuitivos e, acredite, partindo de um SlideFragmentBuilder somente não trabalhamos ainda os métodos: neededPermissions() e possiblePermissions(). Estes serão utilizados em slides posteriores.
Com exceção de build() em SlideFragmentBuilder, todos os métodos são opcionais, até o argumento MessageButtonBehaviour. Aliás, esse último é utilizado para quando precisamos de alguma ação no slide, ação partindo do clique do usuário em um Button.
Antes de prosseguir, devemos ainda adicionar alguns textos, referentes ao primeiro slide, em /res/values/strings.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Questions?</string>
<string name="slide_1_title">Perguntas a comunidade</string>
<string name="slide_1_description">
Escolha a categoria de sua pergunta e então
\n
a libere para a comunidade correta, com
\n
apenas um clique!
</string>
<string name="slide_1_button_label">Quais os tipos de perguntas?</string>
<string name="slide_1_button_message">
De múltiplas escolhas, com: texto, mapa, imagem, vídeo e / ou áudio.
</string>
</resources>
E também algumas cores, agora em /res/values/colors.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
...
<color name="slide_button">#66000000</color>
<color name="slide_1">#88c441</color>
</resources>
Notei que você está utilizando uma imagem local, R.drawable.slide_1, que provavelmente é a imagem de centro do slide. Minha dúvida é sobre o tamanho dessa imagem e se ela deve ser replicada pelos folders drawable-mdpi, hdpi, xhdpi e xxhdpi?
Sim, ela deve ser replicada pelos folders drawable-mdpi, hdpi, xhdpi e xxhdpi. Na documentação não há nada indicando sobre o tamanho ideal da imagem de centro. Nos exemplos presentes lá todos eles são variados.
Em nosso projeto de exemplo utilizei como base o tamanho 275px em mdpi, logo, temos: 275 pixels em mdpi; 413 pixels em hdpi; 550 pixels em xhdpi; e 825 pixels em xxhdpi. Isso para cada imagem de centro de slide.
Antes de prosseguirmos, saiba que o método addSlide() tem uma sobrecarga que somente recebe o primeiro parâmetro, utilizada quando não precisamos de um MessageButtonBehaviour.
Configuração do segundo SlideFragment, com solicitação de permissões obrigatórias em tempo de execução
Antes de adicionarmos os códigos Kotlin do segundo slide, vamos primeiro atualizar o /res/values/strings.xml com as Strings que utilizaremos nesse novo slide:
<?xml version="1.0" encoding="utf-8"?>
<resources>
...
<string name="slide_2_title">Rastreador de membros</string>
<string name="slide_2_description">
Permita, ou não, que pessoas próximas a
\n
você, localmente, recebam suas perguntas
\n
por meio do rastreador de membros.
</string>
<string name="grant_permissions">Liberar permissões</string>
<string name="mis_grant_permissions">Liberar permissões</string>
<string name="please_grant_permissions">
Para poder prosseguir é importante liberar as permissões de localização
</string>
</resources>
As definições em português de grant_permissions, mis_grant_permissions e please_grant_permissions são necessárias, pois a API, como limitação, não oferece suporte a outras línguas senão o inglês.
Colocando nossas próprias traduções para as mesmas Strings references da API, conseguimos sobrescrever o valor primário que está em inglês.
Essa atitude é comum quando encontramos uma API que atende a alguma de nossas necessidades, porém ela ainda não tem a tradução. Somente precisamos identificar o XML de Strings em uso e então traduzi-lo. Acredite, isso é simples e muito útil quando a API é de uma funcionalidade complexa.
Agora vamos a atualização do XML de cores, /res/values/colors.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
...
<color name="slide_2">#6734b9</color>
</resources>
E então, ainda no onCreate() de IntroActivity, adicione os códigos em destaque:
...
override fun onCreate(savedInstanceState: Bundle?) {
...
val neededPermissions = arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION )
addSlide( SlideFragmentBuilder()
.backgroundColor( R.color.slide_2 )
.buttonsColor( R.color.slide_button )
.neededPermissions( neededPermissions )
.image( R.drawable.slide_2 )
.title( resources.getString(R.string.slide_2_title) )
.description( resources.getString(R.string.slide_2_description) )
.build() )
}
...
O método neededPermissions() está esperando um array de String, como fizemos utilizando o método arrayOf().
Caso o usuário não dê as permissões solicitadas, a atividade de introdução não permite o acesso ao próximo slide, somente a volta ao slide anterior.
Configuração do terceiro SlideFragment, com solicitação de permissão opcional em tempo de execução
Nosso próximo slide é muito similar ao anterior em termos de regras de negócio, isso, pois ele também trabalha com solicitação de permissão, porém aqui é opcional.
Vamos iniciar com as novas Strings em /res/values/strings.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
...
<string name="slide_3_title">Fotos e vídeos</string>
<string name="slide_3_description">
Para passar um melhor entendimento, forneça
\n
também nas perguntas algumas fotos e
\n
até mesmo vídeos.
</string>
</resources>
E então a nova cor em /res/values/colors.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
...
<color name="slide_3">#ff5607</color>
</resources>
Assim a atualização no onCreate() da atividade de introdução:
...
override fun onCreate(savedInstanceState: Bundle?) {
...
val possiblePermissions = arrayOf( Manifest.permission.CAMERA )
addSlide( SlideFragmentBuilder()
.backgroundColor( R.color.slide_3 )
.buttonsColor( R.color.slide_button )
.possiblePermissions( possiblePermissions )
.image( R.drawable.slide_3 )
.title( resources.getString(R.string.slide_3_title) )
.description( resources.getString(R.string.slide_3_description) )
.build() )
}
...
O Button de solicitação de permissão ainda aparece no slide com o uso de possiblePermissions(), mas o usuário pode prosseguir sem conceder nenhuma permissão, pois elas, neste caso, são opcionais.
Configuração do quarto SlideFragment, slide customizado
Nosso quarto, e último, slide terá: o texto de termos e condições de uso, um título e também um CheckBox de aceitação de termos.
A API Material-Intro-Screen oferece um caminho simples para a construção de slide customizado, criando um fragmento que herde de SlideFragment e então sobrescrevendo os métodos:
- onCreateView() para ao menos fornecermos nosso próprio layout;
- canMoveFurther() para fornecermos a lógica de negócio que indicará se o usuário poderá ou não partir para o próximo slide, ou finalizar a atividade de introdução caso esse seja o último slide;
- cantMoveFurtherErrorMessage() retorna a mensagem que será apresentada em um SnackBar caso o retorno de canMoveFurther() seja false;
- backgroundColor() para definir a cor de background do slide, pois não poderemos invocar o método backgroundColor() na instância do slide customizado;
- buttonsColor() para definir a cor dos botões do slide, no caso, o botão para próximo slide, botão para slide anterior e os pontos de posição de slide.
Primeiro vamos aos trechos adicionados em /res/values/strings.xml, trechos deste novo slide:
<?xml version="1.0" encoding="utf-8"?>
<resources>
...
<string name="slide_4_title">Termos e condições de uso</string>
<string name="slide_4_checkbox_label">Concordo com os termos e condições de uso</string>
<string name="slide_4_checkbox_error">
Você precisa concordar com os termos e condições de uso para prosseguir
</string>
<string name="slide_4_content">
Passagem padrão original de Lorem Ipsum, usada desde o século XVI.
\n\n
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut
labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco
laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in
voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat
non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
\n\n
Seção 1.10.32 de "de Finibus Bonorum et Malorum", escrita por Cícero em 45 AC
\n\n
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium,
totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae
vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit
aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.
Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit,
sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat
voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit
laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui
in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo
voluptas nulla pariatur?
\n\n
Tradução para o inglês por H. Rackha, feita em 1914
\n\n
But I must explain to you how all this mistaken idea of denouncing pleasure and praising pain was
born and I will give you a complete account of the system, and expound the actual teachings of
the great explorer of the truth, the master-builder of human happiness. No one rejects, dislikes,
or avoids pleasure itself, because it is pleasure, but because those who do not know how to
pursue pleasure rationally encounter consequences that are extremely painful. Nor again is there
anyone who loves or pursues or desires to obtain pain of itself, because it is pain, but because
occasionally circumstances occur in which toil and pain can procure him some great pleasure. To
take a trivial example, which of us ever undertakes laborious physical exercise, except to obtain
some advantage from it? But who has any right to find fault with a man who chooses to enjoy a
pleasure that has no annoying consequences, or one who avoids a pain that produces no resultant
pleasure?
\n\n
Seção 1.10.33 de "de Finibus Bonorum et Malorum", escrita por Cícero em 45 AC
\n\n
At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum
deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non
provident, similique sunt in culpa qui officia deserunt mollitia animi, id est laborum et dolorum
fuga. Et harum quidem rerum facilis est et expedita distinctio. Nam libero tempore, cum soluta
nobis est eligendi optio cumque nihil impedit quo minus id quod maxime placeat facere possimus,
omnis voluptas assumenda est, omnis dolor repellendus. Temporibus autem quibusdam et aut officiis
debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non
recusandae. Itaque earum rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus
maiores alias consequatur aut perferendis doloribus asperiores repellat.
\n\n
Tradução para o inglês por H. Rackha, feita em 1914
\n\n
On the other hand, we denounce with righteous indignation and dislike men who are so beguiled and
demoralized by the charms of pleasure of the moment, so blinded by desire, that they cannot foresee
the pain and trouble that are bound to ensue; and equal blame belongs to those who fail in their
duty through weakness of will, which is the same as saying through shrinking from toil and pain.
These cases are perfectly simple and easy to distinguish. In a free hour, when our power of choice
is untrammelled and when nothing prevents our being able to do what we like best, every pleasure
is to be welcomed and every pain avoided. But in certain circumstances and owing to the claims of
duty or the obligations of business it will frequently occur that pleasures have to be repudiated
and annoyances accepted. The wise man therefore always holds in these matters to this principle of
selection: he rejects pleasures to secure other greater pleasures, or else he endures pains to
avoid worse pains.
</string>
</resources>
Assim vamos a atualização do XML de cores, /res/values/colors.xml, para conter a cor de background deste slide:
<?xml version="1.0" encoding="utf-8"?>
<resources>
...
<color name="slide_4">#5f7c8b</color>
</resources>
Agora a apresentação do layout, /res/layout/fragment_terms_conditions_slide.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:orientation="vertical"
android:padding="16dp"
tools:context="br.com.thiengo.introapitests.QuestionsActivity">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:layout_marginTop="36dp"
android:text="@string/slide_4_title"
android:textColor="@android:color/white"
android:textSize="26sp" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginBottom="16dp"
android:layout_weight="1"
android:padding="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/slide_4_content" />
</ScrollView>
<CheckBox
android:id="@+id/cb_concordo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="76dp"
android:text="@string/slide_4_checkbox_label"
android:textColor="@android:color/white" />
</LinearLayout>
Abaixo o diagrama do layout anterior:
E assim o código Kotlin da classe que representa o novo slide, TermsConditionsSlide. No pacote /fragment adicione a classe a seguir:
class TermsConditionsSlide : SlideFragment() {
override fun onCreateView(
inflater: LayoutInflater?,
container: ViewGroup?,
savedInstanceState: Bundle?): View? {
return inflater?.inflate( R.layout.fragment_terms_conditions_slide, container, false )
}
override fun canMoveFurther(): Boolean {
if( cb_concordo.isChecked ){
val intent = Intent(activity, QuestionsActivity::class.java)
intent.setFlags( Intent.FLAG_ACTIVITY_CLEAR_TOP )
startActivity( intent )
activity.finish()
}
return cb_concordo.isChecked
}
override fun cantMoveFurtherErrorMessage(): String {
return activity.resources.getString(R.string.slide_4_checkbox_error)
}
override fun backgroundColor(): Int {
return R.color.slide_4
}
override fun buttonsColor(): Int {
return R.color.slide_button
}
}
Como esse é o código do último slide, caso o usuário tenha concordado com os termos e condições de uso, dado um check na View cb_concordo, em canMoveFurther() podemos invocar a próxima atividade.
Mas note que foi necessária a definição de uma flag, Intent.FLAG_ACTIVITY_CLEAR_TOP, e também a invocação do finish() da IntroActivity.
Isso, pois aparentemente há um bug na API que quando o usuário tenta passar o último slide e a condição de "próximo" é aceita, aqui o CheckBox marcado, o método canMoveFurther() é invocado inúmeras vezes e a IntroActivity não é removida da pilha de atividades neste caso. Comportamento diferente de quando passando para o "próximo" com o clique / touch no next button, onde não precisaríamos nem de flag e nem do método finish().
Estamos acessando diretamente cb_concordo, pois estamos também fazendo uso do plugin kotlin-android-extensions.
Assim podemos seguramente atualizar o onCreate() da IntroActivity:
...
override fun onCreate(savedInstanceState: Bundle?) {
...
addSlide( TermsConditionsSlide() )
}
...
Simples, certo?
Agora o que nos resta é o trabalho com o SharedPreferences.
Configuração do SharedPreferences para a lógica da IntroActivity
Primeiro devemos criar uma classe que contenha funções de atualização e obtenção da flag de visualização de atividade de introdução.
No pacote /data adicione a classe SPInfo:
class SPInfo(val context: Context) {
fun updateIntroStatus(status : Boolean){
context
.getSharedPreferences("PREF", Context.MODE_PRIVATE)
.edit()
.putBoolean("status", status)
.apply()
}
fun isIntroShown() = context
.getSharedPreferences("PREF", Context.MODE_PRIVATE)
.getBoolean("status", false)
}
Agora precisamos colocar as invocações de updateIntroStatus() e isIntroShown() nos locais corretos do projeto. Primeiro vamos a atualização do método canMoveFurther() de TermsConditionsSlide:
...
override fun canMoveFurther(): Boolean {
if( cb_concordo.isChecked ){
SPInfo(activity).updateIntroStatus(true)
val intent = Intent(activity, QuestionsActivity::class.java)
intent.setFlags( Intent.FLAG_ACTIVITY_CLEAR_TOP )
startActivity( intent )
activity.finish()
}
return cb_concordo.isChecked
}
...
Note que com o Kotlin, o acesso a activity é equivalente ao acesso a getActivity() no Java.
No trecho de código anterior podemos seguramente atualizar o valor de status no SharedPreferences, pois naquele ponto o usuário já aceitou as permissões obrigatórias e também está de acordo com os termos e condições de uso.
Agora a atualização na IntroActivity, isso para quando o usuário entrar no aplicativo sabermos se ele já passou ou não pelos slides de introdução, digo, passou e aceitou as permissões e termos e condições de uso:
class IntroActivity : MaterialIntroActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
verifyIntroActivity()
...
}
private fun verifyIntroActivity(){
if( SPInfo(this).isIntroShown() ){
val intent = Intent(this, QuestionsActivity::class.java)
startActivity(intent)
finish()
}
}
}
Assim podemos partir para os testes.
Testes e resultados
Depois de dar o rebuild em seu projeto, indo em Menu / Build / Rebuild Project, execute ele no emulador ou device de testes que está a sua disposição. Assim terá a primeira tela do slide (clique em "QUAIS OS TIPOS DE PERGUNTAS"):
No segundo slide, tente passar sem liberar as permissões, terá algo como:
Clicando em "LIBERAR PERMISSÕES" temos:
Com as permissões solicitadas liberadas, podemos seguramente prosseguir para o slide três. Veja que dessa vez podemos passar sem a necessidade de liberar a permission. Mesmo assim, libere ela e prossiga:
No quarto e último slide, caso tente prosseguir sem acordar com os termos e condições de uso, não marcando o CheckBox, uma mensagem será apresentada e o "prosseguir" não será possível:
Acordando com os termos e prosseguindo, temos:
Com isso seguramente podemos ir a algumas outras características da API.
Parallax
O efeito parallax, resumidamente, é o efeito de mover imagens de fundo de maneira mais lenta do que as imagens de primeiro plano.
Com a API Material-Intro-Screen esse efeito é aplicado aos elementos em tela, e não as imagens de fundo, até porque não temos essa opção, digo, não na interface publica da API.
Veja um exemplo de mudança de slide, veja como o movimento dos elementos, principalmente a imagem e os textos, como não é síncrono, isso além da mudança de cor de background:
Em nosso último slide, onde trabalhamos com um customizado, não foi necessário utilizar nenhuma das tags Parallax, isso, pois não foi enxergada a necessidade desse movimento assíncrono.
Mas caso esse slide estivesse entre outros, você poderia sim manter o efeito nele também, utilizando algumas das Views parallax disponíveis na API:
- ParallaxFrameLayout;
- ParallaxLinearLayout;
- ParallaxRelativeLayout.
Juntamente ao atributo de fator nas Views que receberiam a animação.
Veja o layout a seguir, o /res/layout/fragment_terms_conditions_slide.xml atualizado:
<?xml version="1.0" encoding="utf-8"?>
<agency.tango.materialintroscreen.parallax.ParallaxLinearLayout
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:orientation="vertical"
android:padding="16dp"
tools:context="br.com.thiengo.introapitests.QuestionsActivity">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:layout_marginTop="36dp"
android:text="@string/slide_4_title"
android:textColor="@android:color/white"
android:textSize="26sp"
app:layout_parallaxFactor="0.6" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginBottom="16dp"
android:layout_weight="1"
android:padding="16dp"
app:layout_parallaxFactor="0.1">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/slide_4_content" />
</ScrollView>
<CheckBox
android:id="@+id/cb_concordo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="76dp"
android:text="@string/slide_4_checkbox_label"
android:textColor="@android:color/white"
app:layout_parallaxFactor="0.9" />
</agency.tango.materialintroscreen.parallax.ParallaxLinearLayout>
Executando e seguindo ao último slide, temos:
Os elementos de menor fator aparecem primeiro, pois os de maior fator são mais rápidos.
Note que para abrir a IntroActivity novamente você terá de: ou remover o aplicativo; ou limpar os dados dele em seu device de testes.
Animação de transição de slides
Caso você queira mudar a animação de alguns elementos do slide, até ele próprio, quando ocorre a transição entre slides, você tem a disposição os seguintes métodos:
- getNextButtonTranslationWrapper() para o botão de próximo slide;
- getBackButtonTranslationWrapper() para o botão de slide anterior;
- getPageIndicatorTranslationWrapper() para o indicador central de slide atual;
- getViewPagerTranslationWrapper() para o slide completo em si;
- getSkipButtonTranslationWrapper() para o botão de "pular introdução" que é possível colocar no local do botão de "slide anterior".
Em nosso projeto de exemplo, caso quiséssemos que o botão de "slide anterior" tivesse uma aparição rápida e uma remoção lenta, isso utilizando a animação de fade, poderíamos fazer o seguinte no onCreate() da IntroActivity:
class IntroActivity : MaterialIntroActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
verifyIntroActivity()
backButtonTranslationWrapper
.setEnterTranslation {
view, percentage -> view.alpha = percentage * 5
}
...
}
...
}
Se você executar o projeto, verá que a apresentação do back button será rápida e a remoção dele, quando voltando do slide atual para o slide anterior, será bem mais lenta que quando não utilizando nosso código de backButtonTranslationWrapper().
Pular button, esconder o back button e transição alpha no último slide
Para colocar o botão de pular, recomendado quando todos os slides da trilha de introdução são opcionais, apenas acrescente o método setSkipButtonVisible() a sua atividade de introdução, exatamente como podemos fazer em nossa IntroActivity:
class IntroActivity : MaterialIntroActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
verifyIntroActivity()
setSkipButtonVisible()
...
}
...
}
Executando o aplicativo, temos:
Onde deveria aparecer o back button para o slide anterior, aparece o skip button.
Em nosso caso, como há restrição de permissão obrigatória e acordo com os termos e condições de uso, o skip button não funciona, digo, ele até tenta finalizar os slides, mas as mensagens de restrição são apresentadas.
Resumo: skip button é somente para quando sua atividade de introdução é opcional, assim o usuário poderá pular direto para a lógica de negócio pós slides, em nosso caso, se não houvesse restrições, seria a invocação da business logic em canMoveFurther() na classe TermsConditionsSlide.
Caso você não queira que o back button seja apresentado, invoque o método hideBackButton() em sua atividade de introdução:
class IntroActivity : MaterialIntroActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
verifyIntroActivity()
hideBackButton()
...
}
...
}
Assim, temos:
Note que o uso de hideBackButton() não influencia em nada no funcionamento do "arrastar" do slide, ainda é possível voltar ao slide anterior somente arrastando o atual.
Por fim o método enableLastSlideAlphaExitTransition() que tem como objetivo permitir que o último slide tenha a animação de transição onde ele vai desaparecendo de acordo com o movimento (ou clique) de "próximo". No código, temos:
class IntroActivity : MaterialIntroActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
verifyIntroActivity()
enableLastSlideAlphaExitTransition( true )
...
}
...
}
E então executando o aplicativo e aplicando o "arrastar para próximo" no último slide, temos:
O problema do uso do método enableLastSlideAlphaExitTransition() com o valor true é que sua restrição de bloqueio, caso o usuário aplique o "arrastar para próximo", é ignorada e nenhuma mensagem de alerta é apresentada, logo, tenha cautela em utilizar essa funcionalidade em slides finais que tenham restrições para poder prosseguir.
Slide com imagem de background
Uma das limitações da API é que não temos uma simples interface pública para a definição de uma imagem de background. Porém, utilizando um custom slide, é possível definirmos os nosso background image.
Primeiro saiba que a imagem de background, caso ela seja definida no View root do layout do slide customizado, deve ter as definições dela ao menos nos quatro principais folders drawable, definições como a seguir:
- drawable-mdpi: 320x480 pixels;
- drawable-hdpi: 480x800 pixels;
- drawable-xhdpi: 640x960 pixels;
- drawable-xxhdpi: 960x1600 pixels.
Caso queira fornecer uma imagem de background também para os folders ldpi e xxxhdpi, siga os seguintes tamanhos:
- drawable-ldpi: 240x320 pixels;
- drawable-xxxhdpi: 1280x1920 pixels.
Em nosso projeto, no slide quatro, temos a possibilidade de utilizarmos uma imagem de background. Atualize o /res/layout/fragment_terms_conditions_slide.xml como abaixo:
<?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="@drawable/slide_4_background"
android:orientation="vertical"
android:padding="16dp"
tools:context="br.com.thiengo.introapitests.QuestionsActivity">
...
Executando o aplicativo e caminhando até o último slide, temos:
Assim terminamos nosso projeto proposto junto a explicação e implementação de uma funcionalidade que é comum e útil há vários domínios de problema.
Não esqueça de se inscrever na lista de e-mails 📩 do Blog, logo ao lado ou ao final do artigo.
Se inscreva também no canal: YouTube channel Thiengo Calopsita.
Vídeo com a implementação da API de atividade de introdução
A seguir o vídeo com a implementação passo a passo da API Material-Intro-Screen:
Para acesso ao conteúdo completo do projeto, entre no GitHub a seguir: https://github.com/viniciusthiengo/questions-intro-api.
Conclusão
Como informado inicialmente: dependendo de seu domínio do problema, trabalhar com uma atividade de introdução pode até mesmo ajudar a reter usuários, isso, pois eles saberão logo de início o que será capaz de realizar no app.
Como para qualquer outra funcionalidade, antes de iniciar a codificação do zero, busque primeiro por libraries que já tenham a funcionalidade implementada, libraries bem aceitas na comunidade de desenvolvedores.
É possível que haja limitações na API encontrada, mesmo assim vale o teste (e uso) mesmo que seja ao menos por um curto prazo.
A API que utilizamos no projeto de exemplo da atividade de introdução tem certas limitações que não nos atrapalharam. A principal, e não comentada até aqui, é o "não trabalho com a persistência de slide atual". Ou seja, quando rotacionamos a tela, caso a atividade não esteja travada em uma única orientação, o slide inicial é apresentado novamente.
De qualquer forma, com o domínio de nossos aplicativos, conseguimos implementar uma importante característica, slides de introdução, e ainda colocamos a solicitação de permissões, cruciais ao nosso aplicativo, incluindo o acordo com nossos termos e condições de uso... isso tudo com poucas linhas de código, nos dando mais tempo para focar no business logic do projeto.
Não esqueça de compartilhar e comentar abaixo o que achou além de dar suas dicas de API.
E... inscreva-se na 📩 lista de e-mails do Blog para receber o conteúdo exclusivo e em primeira mão.
Abraço.
Fontes
Documentação oficial da API Material-Intro-Screen
O que é Efeito Parallax? Como funciona?
Documentação oficial da API CircularImageView
Comentários Facebook