Login com ConstraintLayout e TextWatcher Para Validação - Android M-Commerce
(7553)
CategoriasAndroid, Design, Protótipo
AutorVinÃcius Thiengo
VÃdeo aulas186
Tempo15 horas
ExercÃciosSim
CertificadoSim
CategoriaDesenvolvimento Web
Autor(es)Robert C. Martin
EditoraAlta Books
Edição1ª
Ano2023
Páginas416
Tudo bem?
Neste artigo vamos a construção da atividade de login do projeto Android de mobile-commerce, BlueShoes.
Devido ao tamanho dos códigos desta atividade, nesta aula vamos focar somente na construção dos códigos "brutos", posteriormente, em refatoração, vamos melhora-los.
Antes de prosseguir com a leitura, não deixe de se inscrever 📩 na lista de emails do Blog para ter acesso exclusivo aos novos conteúdos do projeto e também a outros artigos e vídeos sobre desenvolvimento Android.
A seguir os pontos abordados:
- Estou começando agora no projeto;
- Por que não utilizar uma API específica de OAuth?:
- Estratégia para a tela de login:
- Trabalhando a atividade de login:
- Arquivo de cores;
- Arquivo de Strings;
- Criando a LoginActivity;
- Configuração do AndroidManifest;
- Removendo a transparência da statusBar;
- Layout de conteúdo;
- Potencializando a usabilidade com imeOptions;
- Melhorando o foco dos EditTexts;
- Layout principal;
- Hackcode para background consistente;
- Validação de e-mail em inserção de conteúdo;
- Validação de senha em inserção de conteúdo;
- Configuração de tela proxy para processamento em background;
- SnackBar para feedback do back-end Web;
- Bloqueio de campos e botão, quando em envio;
- Para mudança de rótulo de botão;
- Métodos de simulação de envio de dados;
- OnEditorActionListener para envio de dados;
- O problema do link de políticas quando em landscape;
- Início da solução para o link de políticas - com dois layouts;
- Fim da solução para o link de políticas - KeyboardUtils API.
- Atualização da atividade principal:
- Testes e resultados;
- Vídeos;
- Conclusão;
- Fontes.
Estou começando agora no projeto
Se você chegou agora no projeto Android de mobile-commerce, BlueShoes, já existem cinco aulas, também com vídeos, que você precisa consumir antes de continuar com está sexta. Segue:
- 1ª aula - Android Mobile-Commerce, Apresentação e Protótipo do Projeto;
- 2ª aula - Início de Projeto e Menu Gaveta Customizado - Android M-Commerce;
- 3ª aula - Fragmento da Tela Sobre e Links Sociais - Android M-Commerce;
- 4ª aula - Localização com Rota GPS, E-mail e Telefones - Android M-Commerce;
- 5ª aula - Políticas de Privacidade e Porque não a GDPR - Android M-Commerce.
Por que não utilizar uma API específica de OAuth?
Se você já trabalha a algum tempo com algoritmos de autenticação de usuário, você já deve saber que ao menos a discussão sobre a possiblidade de uso de alguma API de OAuth deve ser levada em consideração.
Eu particularmente confio em APIs de OAuth de empresas que somente trabalham com isso. Vejo essas empresas como muito mais especialistas em autenticação do que eu ou qualquer time de desenvolvedores que não têm foco somente em autenticação.
Mas o mundo real é diferente do mundo ideal. Algoritmos de login com vinculo a tradicionais bancos de dados como MySQL e PostgreSQL ainda são comuns em qualquer região, não somente no Brasil.
Com o objetivo de também atender aos inúmeros pedidos de CRUD Android com MySQL, foi escolhido seguir o projeto com os comuns algoritmos de:
- login;
- cadastro;
- e recuperação de acesso.
Algoritmos de autenticação vão acabar?
Opinião minha: certamente os algoritmos tradicionais de autenticação vão acabar. Isso, pois algoritmos de login não são parte especifica do domínio de problema de aplicativos, mobile ou não.
Esses algoritmos sempre terão muita importância, mas cada vez mais é necessária a entrega rápida do projeto de software sem perder em qualidade.
Sendo assim, APIs como Account Kit e OAuth vêm com a proposta de entregar o melhor em autenticação por custos "baixos" quando comparados ao ganho em qualidade e prazo de entrega do projeto.
Outra coisa, os algoritmos de login e cadastro tradicionais tendem a ser vinculados a bases de dados SQL, estas que cada vez mais perdem espaço para bases de dados NoSQL, que dependendo do pacote de APIs, como o Firebase, já vêm com o próprio algoritmo de autenticação.
Estratégia para a tela de login
Diferente das telas de fragmentos desenvolvidas até este ponto do projeto, a atividade de login terá muitos códigos estáticos e dinâmicos para, principalmente, manter a qualidade da apresentação da tela independente da orientação e tamanho dela.
Sendo assim, vamos:
- Primeiro colocar todas as configurações estáticas que dispensam a necessidade de a atividade de login já estar criada;
- Depois partir para os códigos dinâmicos, incluindo atualizações em trechos estáticos devido aos novos códigos dinâmicos;
- Assim finalizaremos com o vinculo da atividade principal com a atividade de login.
Protótipo estático
Abaixo o protótipo estático da tela de login:
Login | Tentativa de login |
Erro no login |
Trabalhando a atividade de login
Como informado anteriormente, vamos primeiro aos códigos que podem ser adicionados sem a necessidade da atividade de login já estar criada, então vamos a criação da LoginActivity e de todos os códigos vinculados a ela.
Antes de prosseguir, saiba que o projeto está disponível no GitHub a seguir:https://github.com/viniciusthiengo/blueshoes-kotlin-android.
Mesmo com o projeto disponível do GitHub, não deixe de seguir o artigo, pois é nele que tem as explicações de cada trecho de código.
Arquivo de cores
No arquivo /res/values/colors.xml adicione os trechos em destaque a seguir:
<?xml version="1.0" encoding="utf-8"?>
<resources>
...
<!--
Cor de background para EditText em foco.
-->
<color name="colorFieldFocused">#F2F9FF</color>
<!--
Cor de background da tela de proxy, quando
os dados de formulário estão sendo enviados
ao back-end Web.
-->
<color name="colorBackgroundProxy">#55FFFFFF</color>
</resources>
Note o uso do hexadecimal, 55, em colorBackgroundProxy. Isso para também trabalharmos o canal alpha da cor (opacidade). Assim conseguimos uma transparência próxima aos 30% da tela de proxy definidos em protótipo estático.
Arquivo de Strings
No arquivo /res/values/strings.xml adicione os trechos em destaque logo abaixo:
<resources>
...
<!-- LoginActivity -->
<string name="title_activity_login">
Login
</string>
<string name="invalid_email">
Forneça um e-mail válido.
</string>
<string name="invalid_password">
Mínimo de 6 caracteres.
</string>
<string name="hint_email">E-mail</string>
<string name="hint_password">Senha</string>
<string name="forgot_my_password">
Esqueci minha senha
</string>
<string name="sign_in">Entrar</string>
<string name="sign_in_going">Entrando…</string>
<string name="or">ou</string>
<string name="sign_up">
Criar minha conta
</string>
<string name="privacy_policy">
Políticas de privacidade
</string>
<string name="invalid_login">
Dados de login inválidos.
</string>
</resources>
Thiengo, novamente estou vendo o texto "Políticas de privacidade", porém em diferente definição de variável em código XML. Não seria melhor reaproveitar as outras definições?
A principio: não. Pois as outras definições são para contextos diferentes, contextos que podem passar por atualizações que não implicarão em atualizações do link "Políticas de privacidade" na tela de login.
Vale ressaltar que os textos em arquivos XML de Strings são para possíveis novos idiomas que serão atendidos pelo aplicativo, sendo assim, na tradução tudo será levado em conta, mesmo trechos repetidos de diferentes contextos.
Agora note o código HTML … em sign_in_going. Com este código conseguimos colocar as reticências, ..., na String sem confundir a leitura do arquivo strings.xml por parte de outros desenvolvedores, como informado pelo Android Studio. Sim, o IDE pediu que fosse realizada essa alteração.
Criando a LoginActivity
No pacote /view do projeto, siga:
- Clique com o botão direito do mouse;
- Acesse New;
- Então acesse Activity;
- Clique em Basic Activity;
- Na caixa de diálogo aberta:
- Em Activity Name coloque LoginActivity;
- Em Layout Name continue com activity_login;
- Não marque a opção Launcher Activity;
- Não marque a opção Use a Fragment;
- Em Hierarchical Parent coloque "Nome do pacote".view.MainActivity;
- Em Package Name permaneça com"Nome do pacote".view;
- Em Source Language permaneça com Kotlin;
- Por fim clique em Finish.
Pode ser que uma tela de alerta seja apresentada. Caso sim, clique em Proceed anyway.
Ao final da criação da atividade LoginActivity nós teremos o seguinte código:
class LoginActivity : AppCompatActivity() {
override fun onCreate( savedInstanceState: Bundle? ) {
super.onCreate( savedInstanceState )
setContentView( R.layout.activity_login )
setSupportActionBar( toolbar )
fab.setOnClickListener { view ->
Snackbar
.make(
view,
"Replace with your own action",
Snackbar.LENGTH_LONG
)
.setAction(
"Action",
null
)
.show()
}
supportActionBar?.setDisplayHomeAsUpEnabled( true )
}
}
Seguramente remova o código do FloatingActionButton. Assim teremos:
class LoginActivity : AppCompatActivity() {
override fun onCreate( savedInstanceState: Bundle? ) {
super.onCreate( savedInstanceState )
setContentView( R.layout.activity_login )
setSupportActionBar( toolbar )
supportActionBar?.setDisplayHomeAsUpEnabled( true )
}
}
Configuração do AndroidManifest
A seguir a configuração do AndroidManifest.xml depois da adição da LoginActivity e da MainActivity como atividade ancestral dessa:
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="thiengo.com.br.blueshoes">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".view.MainActivity"
android:label="@string/app_name"
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity
android:name=".view.LoginActivity"
android:label="@string/title_activity_login"
android:parentActivityName=".view.MainActivity"
android:theme="@style/AppTheme.NoActionBar">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="thiengo.com.br.blueshoes.view.MainActivity"/>
</activity>
</application>
</manifest>
Os novos atributos na <activity> de LoginActivity são bem intuitivos, digo, os atributos para definição de atividade ancestral:
- android:parentActivityName;
- E todos da <meta-data>.
Essas definições de atividade ancestral direto no AndroidManifest.xml nos permitem também evitar códigos dinâmicos, códigos envolvendo configuração de menu, na atividade de login para termos a seta de voltar a atividade anterior, aqui à atividade ancestral, MainActivity.
Em código dinâmico ainda precisaremos da seguinte definição, em destaque, já presente em nossa LoginActivity:
...
override fun onCreate( savedInstanceState: Bundle? ) {
...
supportActionBar?.setDisplayHomeAsUpEnabled( true )
}
...
Um último detalhe: o uso da tag <meta-data> é necessário para que a definição de atividade ancestral também funcione em aparelhos com o Android 4.0, Ice Cream Sandwich, ou inferior.
Removendo a transparência da statusBar
Caso o código abaixo não seja removido do arquivo /res/values-v21/styles.xml:
...
<item name="android:statusBarColor">@android:color/transparent</item>
...
Teremos o seguinte resultado na statusBar do aplicativo quando em execução:
Ao invés de:
Logo, remova o <item> indicado.
Layout de conteúdo
O layout que conterá os campos de formulário, links e botão de envio é o /res/layout/content_login.xml:
<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.NestedScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical"
android:fillViewport="true"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:showIn="@layout/activity_login"
tools:context=".view.LoginActivity">
<android.support.constraint.ConstraintLayout
android:padding="16dp"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/ll_container_fields"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintLeft_toLeftOf="parent">
<EditText
android:id="@+id/et_email"
android:layout_width="300dp"
android:layout_height="wrap_content"
android:paddingTop="13dp"
android:paddingBottom="13dp"
android:paddingLeft="17dp"
android:paddingRight="17dp"
android:inputType="textEmailAddress"
android:textSize="14sp"
android:hint="@string/hint_email"/>
<EditText
android:id="@+id/et_password"
android:layout_marginTop="-1dp"
android:layout_width="300dp"
android:layout_height="wrap_content"
android:paddingTop="13dp"
android:paddingBottom="13dp"
android:paddingLeft="17dp"
android:paddingRight="17dp"
android:inputType="textPassword"
android:textSize="14sp"
android:hint="@string/hint_password"/>
</LinearLayout>
<TextView
android:id="@+id/tv_forgot_password"
style="@style/TextViewLink"
android:layout_marginTop="12dp"
app:layout_constraintTop_toBottomOf="@+id/ll_container_fields"
app:layout_constraintLeft_toLeftOf="@+id/ll_container_fields"
android:text="@string/forgot_my_password"/>
<Button
android:id="@+id/bt_login"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:paddingLeft="38dp"
android:paddingRight="38dp"
app:layout_constraintTop_toBottomOf="@+id/ll_container_fields"
app:layout_constraintRight_toRightOf="@+id/ll_container_fields"
android:background="@drawable/bt_nav_header_login_bg"
android:textColor="@android:color/white"
android:textAllCaps="false"
android:text="@string/sign_in"/>
<TextView
android:id="@+id/tv_or"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="26dp"
app:layout_constraintTop_toBottomOf="@+id/bt_login"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:textColor="@color/colorText"
android:text="@string/or"/>
<TextView
android:id="@+id/tv_sign_up"
style="@style/TextViewLink"
app:layout_constraintTop_toBottomOf="@+id/tv_or"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:text="@string/sign_up"/>
<TextView
android:id="@+id/tv_privacy_policy"
style="@style/TextViewLink"
android:layout_marginTop="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:text="@string/privacy_policy"/>
</android.support.constraint.ConstraintLayout>
</android.support.v4.widget.NestedScrollView>
Você deve estar se perguntando: por que o uso do ConstraintLayout ao invés do simples RelativeLayout?
Dessa vez a simplicidade do RelativeLayout não foi útil. Quando o teclado virtual abria, por exemplo, os links, mesmo com os atributos e posicionamentos corretos, ficavam mal formatados.
Com o ConstraintLayout, mesmo sabendo da verbosidade dos atributos dele, o resultado saiu como esperado.
O layout apresentado passará, ainda nesta aula, por atualizações. Mas até o momento temos o seguinte diagrama para ele:
Note a definição, em XML, dos tipos dos campos:
...
<EditText
...
android:inputType="textEmailAddress"
.../>
<EditText
...
android:inputType="textPassword"
.../>
...
Definições muito importantes, e que alguns aplicativos aparentemente ignoram, para abrir o teclado virtual com somente as teclas possíveis de acordo com o tipo de dado esperado.
A seguir um exemplo de quando o campo de e-mail está em foco:
Potencializando a usabilidade com imeOptions
Vamos permitir que pelo teclado virtual o usuário possa prosseguir entre os campos e também finalizar o preenchimento do formulário quando no último campo.
Nos EditTexts do layout de conteúdo, /res/layout/content_login.xml, adicione os atributos em destaque:
...
<EditText
...
android:imeOptions="actionNext"/>
<EditText
...
android:imeOptions="actionDone"/>
...
O atributo imeOptions tem vários possíveis valores, em nosso caso precisamos de um para permitir seguir para o próximo campo e um para indicar a finalização do preenchimento quando em último campo e ele já preenchido:
- actionNext: próximo campo;
- actionDone: último campo para preenchimento.
Ainda chegaremos ao ponto, tópico em artigo, onde o acionamento da tecla actionDone também acionará o envio de dados do formulário de login.
Melhorando o foco dos EditTexts
Os EditTexts ainda não estão como definidos em protótipo estático. A seguir a imagem de como eles estão até o momento:
Além da definição correta devemos também colocar mais ênfase no campo que estiver em foco.
Para isso vamos criar alguns arquivos drawable que além de darem os formatos corretos aos campos vão também destacar o background daquele que estiver em foco em foco.
Em /res/drawable crie o arquivo bg_form_field_top_corners.xml com o XML a seguir:
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<!--
Definindo a cor de background - branca.
-->
<solid android:color="@android:color/white" />
<!--
Definindo bordas de 1dp de espessura e na cor
cinza.
-->
<stroke
android:color="@color/colorViewLine"
android:width="1dp" />
<!--
Definindo bordas arredondadas no topo da View
retangular.
-->
<corners
android:topLeftRadius="5dp"
android:topRightRadius="5dp" />
</shape>
O arquivo acima é para quando o campo não estiver com foco.
Agora a versão do arquivo anterior para quando o campo está em foco. Em /res/drawable crie o arquivo bg_form_field_top_corners_focused.xml com o XML a seguir:
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<!--
Definindo a cor de background - azul claro.
-->
<solid android:color="@color/colorFieldFocused" />
<!--
Definindo bordas de 1dp de espessura e na cor
cinza.
-->
<stroke
android:color="@color/colorViewLine"
android:width="1dp" />
<!--
Definindo bordas arredondadas no topo da View
retangular.
-->
<corners
android:topLeftRadius="5dp"
android:topRightRadius="5dp" />
</shape>
Ainda é preciso o arquivo XML que permita a mudança entre design com foco e design quando sem foco. Ainda em /res/drawable crie o arquivo bg_form_field_top.xml com o XML abaixo:
<?xml version="1.0" encoding="utf-8"?>
<selector
xmlns:android="http://schemas.android.com/apk/res/android">
<!--
Drawable utilizado quando a View alvo está com
foco.
-->
<item
android:state_focused="true"
android:drawable="@drawable/bg_form_field_top_corners_focused" />
<!--
Drawable utilizado quando a View alvo não está
com foco.
-->
<item
android:drawable="@drawable/bg_form_field_top_corners" />
</selector>
A ordem dos <item>s em arquivos <selector> é importante. O <item> de estado normal tem de ser o último.
Assim podemos partir para a criação dos arquivos drawable para o campo de baixo do design, o EditText de senha.
Em /res/drawable crie o arquivo bg_form_field_bottom_corners.xml com o código XML a seguir:
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<!--
Definindo a cor de background - branca.
-->
<solid android:color="@android:color/white" />
<!--
Definindo bordas de 1dp de espessura e na cor
cinza.
-->
<stroke
android:color="@color/colorViewLine"
android:width="1dp" />
<!--
Definindo bordas arredondadas no fundo da View
retangular.
-->
<corners
android:bottomLeftRadius="5dp"
android:bottomRightRadius="5dp" />
</shape>
Agora crie o arquivo bg_form_field_bottom_corners_focused.xml, ainda em /res/drawable, com o código XML a seguir:
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<!--
Definindo a cor de background - azul claro.
-->
<solid android:color="@color/colorFieldFocused" />
<!--
Definindo bordas de 1dp de espessura e na cor
cinza.
-->
<stroke
android:color="@color/colorViewLine"
android:width="1dp" />
<!--
Definindo bordas arredondadas no fundo da View
retangular.
-->
<corners
android:bottomLeftRadius="5dp"
android:bottomRightRadius="5dp" />
</shape>
Assim o arquivo que permite a alternância do drawable de campo em foco com o drawable de campo de fundo sem foco. Em /res/drawable crie o arquivo bg_form_field_bottom.xml com o seguinte XML:
<?xml version="1.0" encoding="utf-8"?>
<selector
xmlns:android="http://schemas.android.com/apk/res/android">
<!--
Drawable utilizado quando a View alvo está com
foco.
-->
<item
android:state_focused="true"
android:drawable="@drawable/bg_form_field_bottom_corners_focused" />
<!--
Drawable utilizado quando a View alvo não está
com foco.
-->
<item
android:drawable="@drawable/bg_form_field_bottom_corners" />
</selector>
Por fim, temos somente que vincular os drawables de alternância de design em seus respectivos EditTexts.
O drawable de topo para o EditText que fica na parte de cima e o drawable de fundo, bottom, para o EditText que fica na parte de baixo.
Em /res/layout/content_login.xml atualize os EditTexts com os atributos em destaque a seguir:
...
<EditText
...
android:background="@drawable/bg_form_field_top"/>
<EditText
...
android:background="@drawable/bg_form_field_bottom"/>
...
Com isso temos o formato esperado nos campos do formulário de login:
Layout principal
O layout principal, que contém o layout de formulário, é bem simples. Em /res/layout/activity_login.xml teremos:
<?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=".view.LoginActivity">
<android.support.design.widget.AppBarLayout
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:theme="@style/AppTheme.AppBarOverlay">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay"/>
</android.support.design.widget.AppBarLayout>
<include layout="@layout/content_login"/>
</android.support.design.widget.CoordinatorLayout>
A seguir o diagrama do layout anterior:
Note que a View FloatingActionButton, foi removida do layout anterior, exatamente a View a seguir:
<android.support.design.widget.FloatingActionButton
.../>
Ela não será útil a este layout.
Hackcode para background consistente
Você deve estar se perguntando o porquê de nós ainda não termos definido em nenhum dos layouts da atividade de login o background de sapatos, comum em todo o aplicativo e também apresentado em tela de login no protótipo estático.
Primeiro saiba que como estamos em uma tela de formulário, com campos que acionarão o teclado virtual, não devemos colocar uma imagem de background utilizando o atributo android:background, caso contrário, teremos o seguinte resultado:
Veja a segunda imagem quando o teclado virtual está aberto, como a imagem de background encolhi para se ajustar ao tamanho da tela.
Para resolver isso vamos utilizar um hackcode no onCreate() da LoginActivity:
...
override fun onCreate( savedInstanceState: Bundle? ) {
...
/*
* Hackcode para que a imagem de background do layout não
* se ajuste de acordo com a abertura do teclado de
* digitação. Caso utilizando o atributo
* android:background, o ajuste ocorre, desconfigurando o
* layout.
* */
window.setBackgroundDrawableResource( R.drawable.bg_activity )
}
...
Assim, como informado em comentário, não teremos problemas com auto-ajuste de imagem de background, ela permanece estática.
Validação de e-mail em inserção de conteúdo
Vamos também adicionar uma validação de campo enquanto o usuário informa os dados. Alias validação é algo crítico, que deve vir em pontos chaves no lado Android e também no lado Web.
Aqui, para o campo de e-mail, utilizaremos uma implementação de TextWatcher junto ao Patterns.EMAIL_ADDRESS.
No onCreate() de LoginActivity, adicione o código em destaque a seguir:
...
override fun onCreate( ... ) {
...
/*
* Colocando configuração de validação de campo de email
* para enquanto o usuário informa o conteúdo deste campo.
* */
et_email.addTextChangedListener( object: TextWatcher{
override fun afterTextChanged( content: Editable ) {
val message = getString(R.string.invalid_email)
et_email.error =
if( content.isNotEmpty()
&& Patterns.EMAIL_ADDRESS.matcher(content).matches() )
null
else
message
}
override fun beforeTextChanged(
content: CharSequence?,
start: Int,
count: Int,
after: Int ) {}
override fun onTextChanged(
content: CharSequence?,
start: Int,
before: Int,
count: Int) {}
} )
}
...
Thiengo, não seria melhor estudar a possibilidade de encapsular o código do TextWatcher de alguma maneira, pois provavelmente o código de validação de outros campos de formulário do projeto serão parecidos?
Sim, seria e será. Faremos isso em uma refatoração específica para a tela de login. Nesta aula nós vamos nos preocupar somente em terminar o código para ter uma tela exatamente como definida em protótipo estático.
Enquanto o usuário informa algo no campo de e-mail e este informe não é compatível com um endereço de e-mail, o que ocorre é o seguinte:
Validação de senha em inserção de conteúdo
Para o campo de senha vamos seguir a mesma estratégia do campo de e-mail, também validar enquanto o usuário informa o conteúdo.
Aqui somente verificaremos se a senha informada tem no mínimo seis caracteres.
Ainda no onCreate() de LoginActivity, adicione o código em destaque:
...
override fun onCreate( ... ) {
...
/*
* Colocando configuração de validação de campo de senha
* para enquanto o usuário informa o conteúdo deste campo.
* */
et_password.addTextChangedListener( object: TextWatcher{
override fun afterTextChanged( content: Editable ) {
val message = getString(R.string.invalid_password)
et_password.error =
if( content.length > 5 )
null
else
message
}
override fun beforeTextChanged(
content: CharSequence?,
start: Int,
count: Int,
after: Int ) {}
override fun onTextChanged(
content: CharSequence?,
start: Int,
before: Int,
count: Int) {}
} )
}
...
Em digitação, teríamos:
Configuração de tela proxy para processamento em background
Quando o usuário informou os dados corretos, ao menos corretos para a validação local, e assim realizou o envio deles ao back-end Web, em tela será apresentado o load, melhor dizendo: um proxy.
Este tem a função de informar ao usuário que algo está sendo processado e que ele deve aguardar o processamento antes de seguir para alguma outra ação, fora ou dentro do aplicativo.
Nosso primeiro passo para a adição do proxy é colocar o trecho visual no layout, digo, no XML do layout de formulário. Para isso vamos:
- Colocar um novo ViewGroup como ViewGroup container do conteúdo de formulário e do trecho proxy. Aqui utilizaremos um FrameLayout, pois ele facilita a apresentação do proxy sobre o formulário;
- Colocar o trecho de código proxy, que basicamente será um outro FrameLayout com um ProgressBar como View filha.
Em /res/layout/content_login.xml adicione os trechos de código em destaque a seguir:
<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.NestedScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical"
android:fillViewport="true"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:showIn="@layout/activity_login"
tools:context=".view.LoginActivity">
<FrameLayout
android:id="@+id/fl_form_container"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.constraint.ConstraintLayout
...>
...
</android.support.constraint.ConstraintLayout>
<FrameLayout
android:id="@+id/fl_proxy_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
android:background="@color/colorBackgroundProxy">
<ProgressBar
android:layout_gravity="center"
android:layout_width="50dp"
android:layout_height="50dp"
android:theme="@style/ProgressBarGreyProxy"/>
</FrameLayout>
</FrameLayout>
</android.support.v4.widget.NestedScrollView>
Note que as configurações para NestedScrollView e ConstraintLayout continuam sendo as mesmas.
Você deve ter notado o uso de um novo tema no <ProgressBar>, mais precisamente o tema ProgressBarGreyProxy, certo?
Este tema veio como meio de conseguirmos mudar a cor do progress. Em /res/values/styles.xml adicione o tema como a seguir:
<resources>
...
<style name="ProgressBarGreyProxy">
<item name="colorAccent">@color/colorPrimaryDark</item>
</style>
</resources>
A seguir o novo diagrama do layout content_login.xml:
Agora, em código dinâmico, mais precisamente em LoginActivity, adicione o método a seguir:
...
/*
* Apresenta a tela de bloqueio que diz ao usuário que
* algo está sendo processado em background e que ele
* deve aguardar.
* */
private fun showProxy( status: Boolean ){
fl_proxy_container.visibility = if( status )
View.VISIBLE
else
View.GONE
}
...
Com este método poderemos, em outros algoritmos de controle de envio de dados ao back-end Web, mudar o status do proxy em tela. Ainda nesta aula chegaremos aos métodos que também acionarão o método showProxy().
SnackBar para feedback do back-end Web
Para o projeto BlueShoes o SnackBar tem como principal proposta apresentar as mensagens de retorno do back-end Web.
Note que no protótipo estático foi utilizado SnackBars com ícones, algo que fere as regras de negócio para SnackBar no Material Design Android. Mas aqui nós vamos continuar como solicitado em protótipo estático, pois são os ícones que primeiro dizem ao usuário se tudo deu certo ou não.
Antes de partirmos para o método responsável por toda a configuração de apresentação do SnackBar com a mensagem correta, vamos primeiro adicionar ao projeto os ícones que poderão estar no snack.
Estes ícones foram descarregados como todos os outros já apresentados até está parte do projeto:
Acesse Material Design Icons. Na caixa de busca informe os termos "close" ou "check" e então, clicando nos ícones similares aos apresentados em protótipo estático, na caixa de diálogo acione "Icon Package" e em seguida "Android 5.x".
Para adiantar todo o processo de download de ícones, a seguir tem os links dos ícones diretamente do GitHub do projeto:
- Ícone de Close:
- Ícone de Check:
Agora, em LoginActivity, adicione o método snackBarFeedback() como a seguir:
...
/*
* Método responsável por apresentar um SnackBar com as
* corretas configurações de acordo com o feedback do
* back-end Web.
* */
private fun snackBarFeedback(
viewContainer: ViewGroup,
status: Boolean,
message: String ){
val snackBar = Snackbar
.make(
viewContainer,
message,
Snackbar.LENGTH_LONG
)
/*
* Acessando o TextView padrão do SnackBar para assim
* colocarmos um ícone nele via objeto Spannable.
* */
val snackBarView = snackBar.view
val textView = snackBarView.findViewById(
android.support.design.R.id.snackbar_text
) as TextView
/*
* Criando o objeto Drawable que entrará como ícone
* inicial no texto do SnackBar.
* */
val iconResource = if( status )
R.drawable.ic_check_black_18dp
else
R.drawable.ic_close_black_18dp
val img = ResourcesCompat
.getDrawable(
resources,
iconResource,
null
)
img!!.setBounds(
0,
0,
img.intrinsicWidth,
img.intrinsicHeight
)
val iconColor = if( status )
ContextCompat
.getColor(
this,
R.color.colorNavButton
)
else
Color.RED
img.setColorFilter( iconColor, PorterDuff.Mode.SRC_ATOP )
val spannedText = SpannableString( " ${textView.text}" )
spannedText.setSpan(
ImageSpan( img, ImageSpan.ALIGN_BOTTOM ),
0,
1,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
textView.setText( spannedText, TextView.BufferType.SPANNABLE )
snackBar.show()
}
...
Novamente realizando o uso de SpannableString, desta vez para a adição de imagem em texto.
Em métodos que ainda serão adicionados nesta aula, colocaremos a invocação ao método snackBarFeedback().
Bloqueio de campos e botão, quando em envio
Note que mesmo com a tela de proxy sobre os campos de formulário, o usuário mesmo assim consegue acionar os campos e até mesmo o botão e links disponíveis em formulário.
Como ainda não trabalharemos os links nesta aula, vamos criar um método responsável por bloquear todos os campos e botão de envio de dados quando o formulário já estiver em envio. E desbloquear quando já houver um feedback.
Em LoginActivity adicione o método blockFields() como a seguir:
...
/*
* Necessário para que os campos de formulário não possam
* ser acionados depois de enviados os dados.
* */
private fun blockFields( status: Boolean ){
et_email.isEnabled = !status
et_password.isEnabled = !status
bt_login.isEnabled = !status
}
...
Para mudança de rótulo de botão
O principal botão para envio dos dados mudará de rótulo de acordo com o status de envio do formulário. Também criaremos um método para esta tarefa.
Em LoginActivity adicione o método a seguir:
...
/*
* Muda o rótulo do botão de login de acordo com o status
* do envio de dados de login.
* */
private fun isSignInGoing( status: Boolean ){
bt_login.text = if( status )
getString( R.string.sign_in_going ) /* Entrando... */
else
getString( R.string.sign_in ) /* Entrar */
}
...
Métodos de simulação de envio de dados
Aqui na verdade teremos de desenvolver mais dois métodos:
- Um responsável pela invocação dos métodos de UI criados anteriormente e também, este não é de simulação, ele permanecerá em projeto;
- Outro responsável por simular o envio / delay de dados ao back-end Web.
Primeiro vamos ao método simulador, que tem a tarefa de também aplicar um delay de latência além de invocar os métodos de UI criados anteriormente.
Em LoginActivity adicione:
...
private fun backEndFakeDelay(){
Thread{
kotlin.run {
/*
* Simulando um delay de latência de
* 1 segundo.
* */
SystemClock.sleep( 1000 )
runOnUiThread {
blockFields( false )
isSignInGoing( false )
showProxy( false )
snackBarFeedback(
fl_form_container,
false,
getString( R.string.invalid_login )
)
}
}
}.start()
}
...
Agora vamos ao método login(), que também será vinculado ao Button em tela. Em LoginActivity adicione o método abaixo:
...
fun login( view: View? = null ){
blockFields( true )
isSignInGoing( true )
showProxy( true )
backEndFakeDelay()
}
...
Em login() colocamos a assinatura com valor padrão, view: View? = null, pois este método será também invocado em outro trecho da atividade, trecho que será adicionado ainda nesta aula.
No Button do layout /res/layout/content_login.xml adicione o onClick como a seguir:
...
<Button
...
android:onClick="login"/>
...
OnEditorActionListener para envio de dados
Como informado em tópico anterior, ainda há outro ponto da LoginActivity que acionará o método login().
Lembra do imeOptions? Onde no campo de senha colocamos a opção actionDone. Lembra? Então, o listener do imeOptions, para actionDone, será o outro ponto de acionamento de login().
Lembrando que o uso do imeOptions é para melhorarmos a experiência do usuário permitindo a ele também uma maneira de navegar entre os campos e envio de dados somente usando o teclado virtual.
Antes de irmos direto aos códigos do listener de actionDone, vamos primeiro adicionar um método responsável por fechar o teclado virtual assim que o botão actionDone é acionado.
Em LoginActivity adicione:
...
private fun closeVirtualKeyBoard( view: View ){
val imm = getSystemService(
Activity.INPUT_METHOD_SERVICE
) as InputMethodManager
imm.hideSoftInputFromWindow( view.windowToken, 0 )
}
...
Assim, ainda em LoginActivity, adicione os códigos em destaque:
class LoginActivity :
AppCompatActivity(),
TextView.OnEditorActionListener {
override fun onCreate( ... ) {
...
et_password.setOnEditorActionListener( this )
}
...
/*
* Caso o usuário toque no botão "Done" do teclado virtual
* ao invés de tocar no botão "Entrar". Mesmo assim temos
* de processar o formulário.
* */
override fun onEditorAction(
view: TextView,
actionId: Int,
event: KeyEvent? ): Boolean {
if( actionId == EditorInfo.IME_ACTION_DONE ){
closeVirtualKeyBoard( view )
login()
return true /* Indica que o algoritmo do método consumiu o evento. */
}
return false
}
}
Lembrando que como somente o campo de senha é que tem o actionDone, somente ele recebe o vinculo com o listener de imeOption action.
Assim temos o segundo ponto, que poderá acionar o método login(), configurado.
O problema do link de políticas quando em landscape
Mesmo que ainda não seja possível a ti ver como está a tela de login, posso lhe adiantar que se ela for colocada na horizontal, landscape, o seguinte problema ocorre:
O link de "Políticas de privacidade" fica sobre o link "Criar minha conta".
A Thiengo, mas o usuário nem mesmo notará isso.
Não se engane, alguns notarão e isso é algo que nós desenvolvedores também temos de resolver e, acredite, não é de solução tão crítica e fica como lição para outros pontos que tiverem o mesmo problema.
Início da solução para o link de políticas - com dois layouts
A solução é composta de duas partes:
- Uma somente com mudanças em códigos estáticos;
- Outra com mudanças também em códigos dinâmicos, Kotlin.
Vamos aqui primeiro desenvolver a parte da solução em códigos estáticos.
Em /res/layout siga:
- Clique com o botão direito do mouse;
- Acesse New;
- Clique em Directory;
- Informe como nome layout-land;
- Clique em OK.
Criamos o folder que conterá a configuração de layout, apenas do TextView de Políticas de privacidade, que será apresentado quando o formulário, o aparelho, estiver em landscape.
Neste novo folder crie um novo layout. Em /res/layout-land:
- Clique com o botão direito do mouse;
- Acesse New;
- Clique em Layout resource file;
- Em File name forneça text_view_privacy_policy_login.xml;
- Clique em OK.
O código de text_view_privacy_policy_login.xml deverá ser o seguinte:
<?xml version="1.0" encoding="utf-8"?>
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:showIn="@layout/content_login"
tools:context=".view.LoginActivity"
android:id="@+id/tv_privacy_policy"
style="@style/TextViewLink"
android:layout_marginTop="12dp"
app:layout_constraintTop_toBottomOf="@+id/tv_sign_up"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:text="@string/privacy_policy"/>
Veja que em landscape o link de "Políticas de privacidade" fica posicionado abaixo do link de "Criar minha conta": app:layout_constraintTop_toBottomOf="@+id/tv_sign_up".
Agora, em /res/layout, crie um novo arquivo de recurso com o mesmo nome e configuração inicial do anterior, text_view_privacy_policy_login.xml.
Porém o código deste em /res/layout será o seguinte:
<?xml version="1.0" encoding="utf-8"?>
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:showIn="@layout/content_login"
tools:context=".view.LoginActivity"
android:id="@+id/tv_privacy_policy"
style="@style/TextViewLink"
android:layout_marginTop="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:text="@string/privacy_policy"/>
Note que agora, como já era definido em content_login.xml, o link de "Políticas de privacidade", quando em tela na vertical, fica posicionado junto ao fundo do ConstraintLayout, este que ocupa toda a extensão da tela: app:layout_constraintBottom_toBottomOf="parent".
Agora é colocar o <include> em /res/layout/content_login.xml no lugar do TextView de Políticas de privacidade:
<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.NestedScrollView
...>
<FrameLayout
...>
<android.support.constraint.ConstraintLayout
...>
...
<TextView
android:id="@+id/tv_sign_up"
.../>
<include layout="@layout/text_view_privacy_policy_login"/>
</android.support.constraint.ConstraintLayout>
...
</FrameLayout>
</android.support.v4.widget.NestedScrollView>
Com isso temos o seguinte novo diagrama para o layout content_login.xml:
Fim da solução para o link de políticas - KeyboardUtils API
Mesmo com a solução do tópico anterior, ainda temos um problema. Com o aparelho na vertical, modo portrait, posso lhe adiantar que se o teclado virtual for acionado o problema de sobreposição de links ainda continua:
Uma possível solução é atualizar as configurações do TextView de link de Políticas de privacidade assim que o teclado mudar de status e a tela estiver em modo portrait.
Para isso, primeiro vamos adicionar uma biblioteca já discutida aqui no Blog que nos permitirá, com facilidade, identificar a mudança de status do teclado virtual, aberto ou não, e também identificar a orientação da tela, portrait ou landscape.
No Gradle Nível de Aplicativo, build.gradle (Module: app), adicione a library em destaque a seguir:
...
dependencies {
...
/* AndroidUtilCode API */
implementation 'com.blankj:utilcode:1.23.7'
}
É isso mesmo, a AndroidUtilCode. Mais sobre está API, no link a seguir: Facilitando o Desenvolvimento de Apps Android Com a Biblioteca AndroidUtilCode.
Sincronize o projeto.
Primeiro vamos adicionar o método que é responsável por mudar a configuração do TextView de políticas de privacidade quando o teclado está aberto.
Em LoginActivity adicione changePrivacyPolicyConstraints() como a seguir:
...
private fun changePrivacyPolicyConstraints(
isKeyBoardOpened: Boolean
){
val privacyId = tv_privacy_policy.id
val parent = tv_privacy_policy.parent as ConstraintLayout
val constraintSet = ConstraintSet()
/*
* Definindo a largura e a altura da View em
* mudança de constraints, caso contrário ela
* fica com largura e altura em 0dp.
* */
constraintSet.constrainWidth(
privacyId,
ConstraintLayout.LayoutParams.WRAP_CONTENT
)
constraintSet.constrainHeight(
privacyId,
ConstraintLayout.LayoutParams.WRAP_CONTENT
)
/*
* Centralizando a View horizontalmente no
* ConstraintLayout.
* */
constraintSet.centerHorizontally(
privacyId,
ConstraintLayout.LayoutParams.PARENT_ID
)
if( isKeyBoardOpened ){
/*
* Se o teclado virtual estiver aberto, então
* mude a configuração da View alvo
* (tv_privacy_policy) para ficar vinculada a
* View acima dela (tv_sign_up).
* */
constraintSet.connect(
privacyId,
ConstraintLayout.LayoutParams.TOP,
tv_sign_up.id,
ConstraintLayout.LayoutParams.BOTTOM,
(12 * ScreenUtils.getScreenDensity()).toInt()
)
}
else{
/*
* Se o teclado virtual estiver fechado, então
* mude a configuração da View alvo
* (tv_privacy_policy) para ficar vinculada ao
* fundo do ConstraintLayout ancestral.
* */
constraintSet.connect(
privacyId,
ConstraintLayout.LayoutParams.BOTTOM,
ConstraintLayout.LayoutParams.PARENT_ID,
ConstraintLayout.LayoutParams.BOTTOM
)
}
constraintSet.applyTo( parent )
}
...
O método acima somente pode ser invocado se a tela do aparelho estiver na vertical, portrait.
Assim vamos adicionar a LoginActivity toda a configuração de listener de status de teclado virtual junto a verificação de orientação de tela:
class LoginActivity :
AppCompatActivity(),
TextView.OnEditorActionListener,
KeyboardUtils.OnSoftInputChangedListener {
override fun onCreate( ... ) {
...
/*
* Com a API KeyboardUtils conseguimos de maneira
* simples obter o status atual do teclado virtual (aberto /
* fechado) e assim prosseguir com algoritmos de ajuste de
* layout.
* */
KeyboardUtils.registerSoftInputChangedListener( this, this )
}
...
override fun onDestroy() {
KeyboardUtils.unregisterSoftInputChangedListener(this)
super.onDestroy()
}
override fun onSoftInputChanged( height: Int ) {
if( ScreenUtils.isPortrait() ){
changePrivacyPolicyConstraints(
KeyboardUtils.isSoftInputVisible( this )
)
}
}
}
Com a solução finalizada os links não mais se sobrepõe nem na vertical e nem na horizontal.
Atualização da atividade principal
Para a atividade principal somente vamos adicionar o listener de clique ao botão de login que está presente no menu gaveta versão "quando o usuário não está conectado".
Também adicionaremos o código que invoca a LoginActivity.
Listener de clique para o botão de login
Na MainAcivity adicione o método a seguir:
...
fun callLoginActivity( view: View ){
val intent = Intent( this, LoginActivity::class.java )
startActivity( intent )
}
...
Agora em /res/layout/nav_header_user_not_logged.xml adicione o onClick ao Button:
...
<Button
...
android:onClick="callLoginActivity"/>
...
Assim podemos partir para os testes.
Testes e resultados
Abra o Android Studio, no menu de topo dele siga para "Build", assim clique em "Rebuid project". Ao final do rebuild rode o aplicativo em seu aparelho ou emulador Android de testes.
Tendo em teste o usuário com o status "não conectado", objeto presente na MainActivity:
...
val user = User(
"Thiengo Vinícius",
R.drawable.user,
false
)
...
Temos, em portrait:
Agora em landscape:
Assim finalizamos a sexta parte do projeto Android de mobile-commerce.
Não deixe de se inscrever na 📩 lista de emails do Blog para receber todas as aulas do projeto Android BlueShoes.
Se inscreva também no canal do Blog em: YouTube Thiengo.
Vídeos
A seguir os vídeos com o passo a passo do desenvolvimento da tela de login do projeto Android BlueShoes.
O projeto também pode ser acessado pelo GitHub dele em: https://github.com/viniciusthiengo/blueshoes-kotlin-android.
Conclusão
Também para a tela de login conseguimos seguir exatamente o que foi proposto em protótipo estático.
Em alguns pontos, para manter a qualidade, tivemos de adicionar hackcodes. Como no problema da sobreposição de links, algo que o usuário jamais saberá o nível de complexidade necessária somente para sempre manter o link "Políticas de privacidade" abaixo do link "Criar minha conta".
Porém, experimente não colocar o código que faça a experiência do usuário ser agradável. Neste caso certamente ele notará o bug.
Ainda não adicionamos todas as validações possíveis, isso, pois essas vão vier em camadas diferentes da camada de UI. Em próximas aulas estaremos refatorando a tela de login para já deixar "meio caminho andado" para as outras telas com formulários.
Caso você tenha dúvidas ou dicas para este projeto, deixe logo abaixo nos comentários.
Curtiu o conteúdo? Não esqueça de compartilha-lo. E, por fim, não deixe de se inscrever na 📩 lista de emails, respondo às suas dúvidas também por lá.
Abraço.
Fontes
Build a Responsive UI with ConstraintLayout
Two simple ways to make your users aware of incorrect input
Android keyboard next button issue on EditText - Resposta de Harsh Mittal e de Naveed Ahmad
How to center the elements in ConstraintLayout - Resposta de Pycpik
How to retain EditText data on orientation change? - Resposta de Carlos López Marí
Software keyboard resizes background image on Android - Resposta de M.A.R
Android imeOptions=“actionDone” not working - Resposta de Qianqian
Close/hide the Android Soft Keyboard
Kotlin create a snackbar - Resposta de SSB
How to change progress bar's progress color in Android - Resposta de kirtan403
Soft keyboard open and close listener in an activity in Android - Resposta de Gal Rom
Android Constraint Layout Programmatically? - Resposta de Martin Marconcini e de ZooMagic
Comentários Facebook