Chips Android, Quando e Como Utilizar

Receba em primeira mão, e com prioridade, os conteúdos Android exclusivos do Blog. Você receberá um email de confirmação. Somente depois de confirma-lo é que poderei lhe enviar os conteúdos exclusivos.

Email inválido.
Blog /Android /Chips Android, Quando e Como Utilizar

Chips Android, Quando e Como Utilizar

Vinícius Thiengo03/01/2018
(1291) (158)
Go-ahead
"Se você quer construir um navio, não faça subir os homens para coletar madeira, dividir o trabalho e dar ordens. Em vez disso, ensine-os a ansiar pelo vasto e infinito mar."
Antoine de Saint-Exupéry
Treinamento Oficial
Android: Prototipagem Profissional de Aplicativos
CursoAndroid: Prototipagem Profissional de Aplicativos
CategoriaAndroid
InstrutorVinícius Thiengo
NívelTodos os níveis
Vídeo aulas156
PlataformaUdemy
Acessar Curso
Receitas Android
Capa do livro Receitas Para Desenvolvedores Android
TítuloReceitas Para Desenvolvedores Android
CategoriaDesenvolvimento Android
AutorVinícius Thiengo
Edição
Ano2017
Capítulos20
Páginas934
Acessar Livro
Código Limpo
Capa do livro Refatorando Para Programas Limpos
TítuloRefatorando Para Programas Limpos
CategoriaEngenharia de Software
AutorVinícius Thiengo
Edição
Ano2017
Capítulos46
Páginas598
Acessar Livro
Quer aprender a programar para Android? Acesse abaixo o curso gratuito no Blog.
Conteúdo Exclusivo
Receba em primeira mão, e com prioridade, os conteúdos Android exclusivos do Blog.
Email inválido

Opa, tudo bem?

Neste artigo vamos, passo a passo, ao estudo das views ChipsInput e ChipView que representam os Chips do Material Design Android.

Depois da apresentação dos componentes vamos ao trabalho de implementação de chips em um projeto que simula um aplicativo Android real, um app de envio de emails:

Aplicativo de email utilizando Chips componete

Antes de prosseguir, não deixe de se inscrever na 📩 lista de emails do Blog, logo acima, para receber em primeira mão os conteúdos exclusivos sobre o dev Android.

Abaixo os tópicos que estaremos abordando:

O componente Chip

O chip componente é uma pequena caixa que pode conter até: imagem de perfil; texto; e ícone. Na documentação do Material Design este componente é descrito como "pequena caixa complexa" podendo ser utilizado em vários contextos: tags; contatos; marcações em texto; entre outros.

Para o chip componente, diferente do BottomNavigationView e de outros componentes visuais apresentados na documentação do Material Design, não temos, até o momento da publicação deste artigo, uma API nativa e sim várias APIs de terceiros, algumas mais completas, gerais, e outras mais especificas e menores.

Aqui vamos utilizar uma library que internamente contém outras três bibliotecas de chips que se complementam, assim teremos somente uma sendo referenciada diretamente, porém com uma reprodução mais próxima do demonstrado na documentação do Material Design.

Especificações de uso

A seguir algumas regras de negócio quanto ao uso de componentes chip não somente em aplicativos Android, mas também em qualquer app que faz uso do Material Design como linguagem de design:

Maneiras de uso do Chips componente

  • Qualquer contexto onde seja necessário, de maneira destacada e completa, a apresentação de pequenos itens que tenham ao menos um pequeno trecho de texto:
    • Representação de contato, tag de conteúdo, informe publicitário, notificação interna, entre outros; 
    • Essa parte de "ao menos um pequeno trecho de texto" é definida aqui de maneira explícita, pois na documentação do chips no Material Design isso fica implícito: não fazer sentido ter um ChipView sem um pequeno texto, mesmo que ele já tenha imagem e ícone.

Mais sobre as especificações de uso, no conteúdo a seguir: Components - Chips - Usage.

Especificações de comportamento

A seguir algumas regras quanto ao comportamento dos chips de seu aplicativo:

  • O acionamento de um chip pode abrir uma nova tela, mesmo que seja uma flutuante em um card, ou abrir um novo menu, também flutuante. Essa nova entidade aberta deve ter conteúdo relacionado ao chip acionado;

Menu aberto depois do acionamento de um ChipView

  • A opção de deletar pode fazer parte do chip, um ícone de remoção estará presente nele. A invocação da tecla de deletar também deverá acionar a remoção do chip.

Deletando um Chip componente

Mais sobre as especificações de comportamento, no conteúdo a seguir: Components - Chips - Behavior.

Especificações para chip de contato

Por ser o provável uso deste componente, há na documentação de chip uma área somente para a formatação dele na apresentação de informações de contato.

Isso, pois chips de contato permitem um contexto mais eficiente ao usuário emissor da mensagem. Ele, seguramente, com as informações dos chips dos usuários selecionados, saberá para quem está enviando o conteúdo.

Há somente duas regras de negócio além das já apresentadas anteriormente:

  • O chip de contato tem quatro possíveis estados:
    • Normal (normal);
    • Com focus (focused);
    • Pressionado (pressed);
    • Ativo (activated).

Estados de um chip de contato

  • Quando acionado, deverá ser aberto um menu com algumas informações extras sobre cada possível contato do chip acionado. 

Chip de contato acionado - apresentação de menu

Na documentação do Material Design, quando o chip de contato é acionado e então um menu de opções é aberto, este é considerado também um chip, porém no modo "aberto", algo não verdadeiro quando o chip não é um de contato.

Mais sobre as especificações de chips de contato, no conteúdo a seguir: Components - Chips - Contact chips.

Especificações de design

A seguir algumas regras de negócio para o correto design dos chips:

  • Somente textos curtos, nada de quebras de linha:
    • A exceção é para chips de contato quando aberto, onde pode haver ainda mais linhas nos itens de menu.
  • Os textos devem ter um tamanho de 13sp. Como cor, o preto (ou a cor de contraste com o background do chip) com o canal alpha ligado, opacidade de 87%:
    • Para o tamanho da fonte há uma exceção quando o chip for de contato. Quando fechado ele deve ter 14sp ao invés dos 13sp.
  • A seguir as regras de dp para um single chip:

Especificações de um single chip

  • A seguir as regras de dp para um chip de contato:

Especificações de um chip de contato

Mais detalhes sobre as especificações de chips sozinhos e chips de contato, no conteúdo a seguir: Components - Chips - Specs.

API externa

Como informado no início dos estudos do componente chip: não temos, até o momento da construção deste conteúdo, uma API nativa. Aqui vamos utilizar a API MaterialChipsInput que a princípio é a mais completa API presente na comunidade Android e atende a partir do Android 15, Ice Cream Sandwich, mais de 99% dos devices em mercado.

Dentro desta library há também códigos de outras APIs sobre o chip componente e que podem ser melhores opções a ti caso o seu domínio do problema seja mais específico do que o apresentado neste artigo:

Instalação

Primeiro no Gradle Project Level, build.gradle (Project: ...). Coloque a seguinte referência em destaque:

...
allprojects {
repositories {
...
maven { url "https://jitpack.io" }
}
}
...

 

Logo depois o Gradle App Level, build.gradle (Module: app). Coloque a referência em destaque:

...
dependencies {
...
implementation 'com.github.pchmn:MaterialChipsInput:1.0.8'
}

 

Ao final, sincronize o projeto. Sempre utilize a versão mais atual e estável da API, aqui era a versão 1.0.8.

Configuração ChipsInput

Com o ChipsInput é simples trabalhar com os chips de contato. Primeiro a definição no XML de layout:

...
<com.pchmn.materialchips.ChipsInput
android:id="@+id/chips_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:hint="Entrar com um nome"
app:hintColor="@color/corDeHint"
app:textColor="@color/corDeTexto"
app:maxRows="3"
app:chip_labelColor="@color/corDoRotuloDosChips"
app:chip_hasAvatarIcon="true"
app:chip_backgroundColor="@color/corDeBackgroundDosChips"
app:chip_deletable="true"
app:chip_deleteIconColor="@color/corDoIconeDeRemocao"
app:chip_detailed_textColor="@color/corDoRotuloESubrotuloQuandoOMenuChipEstaAberto"
app:chip_detailed_backgroundColor="@color/corDeBackgroundQuandoOMenuChipEstaAberto"
app:chip_detailed_deleteIconColor="@color/corDoIconeDeRemocaoQuandoOMenuChipEstaAberto"
app:filterable_list_backgroundColor="@color/corDeBackgroundDaListaDeContatosDeSugestao"
app:filterable_list_textColor="@color/corDoTextoDosItensDaListaDeContatosDeSugestao" />
...

 

Todos os atributos de app: do XML acima são opcionais, isso, pois o ChipsInput já tem uma configuração padrão para cada um deles.

Como o ChipsInput faz uso de vários componentes visuais do Android: RecyclerView, LayoutManager e EditText. Temos também que trabalhar a inicialização via código:

...
val chipsInput = findViewById(R.id.chips_input) as ChipsInput

/*
* Lista de contatos do tipo Chip que ficará vinculada
* ao adapter de ChipsInput.
* */
val contatosList = arrayListOf<Chip>( Chip("Blog Thiengo", "blog@thiengo.com.br˜") )

/*
* A vinculação de uma lista de dados, mesmo que vazia,
* ao ChipsInput é necessária, caso contrário não será
* possível entrar com dados nele quando na tela do device.
* */
chipsInput.setFilterableList( contatosList )
...

 

Para obter os itens presentes (selecionados) dentro do ChipsInput:

...
val contatosSelecionados = chipsInput.selectedChipList
...

 

Note que os itens de lista vinculados via setFilterableList() não são equivalentes aos itens selecionados pelo usuário. Os itens selecionados são os que foram apresentados ao usuário e ele os escolheu depois dessa apresentação, colocando-os dentro do ChipsInput.

Sendo assim, podemos assumir que o vinculo de itens selecionados para com ChipsInput é um vinculo de parent, já o vinculo da lista de contatos para sugestão com esse mesmo componente é um vinculo de siblings.

Voltando a adição de um novo chip, há ainda mais interfaces públicas para esta tarefa:

...
chipsInput.addChip( ChipInterface chip )

chipsInput.addChip( Any id, Drawable icone, String rotulo, String informacao )

chipsInput.addChip( Drawable icone, String rotulo, String informacao )

chipsInput.addChip( Any id, Uri uriIcone, String rotulo, String informacao )

chipsInput.addChip( Uri uriIcone, String rotulo, String informacao )

chipsInput.addChip( String rotulo, String informacao )
...

 

A remoção de chip também é simples e tem mais de uma interface:

...
chipsInput.removeChip( ChipInterface chip )

chipsInput.removeChipById( Any id )

chipsInput.removeChipByLabel( String rotulo )

chipsInput.removeChipByInfo( String informacao )
...

Configuração com ChipsInterface

Caso você queira criar as suas próprias configurações de objeto a ser utilizado como item de ChipsInput, crie uma classe que implemente a Interface ChipInterface, assim o uso de objetos Chip é dispensado:

class User(
private val id: Any,
private val avatarUri: Uri,
private val avatarDrawable: Drawable,
private val label: String,
private val info: String ) : ChipInterface {

override fun getId() = id

override fun getAvatarUri() = avatarUri

override fun getAvatarDrawable() = avatarDrawable

override fun getLabel() = label

override fun getInfo() = info
}

 

Em uso, teríamos:

...
val contatosList = arrayListOf<User>( User(...) )
...

 

No aplicativo de exemplo do artigo, criaremos nossa própria classe ChipInterface, isso, pois precisaremos de alguns algoritmos como: o de carregamento de imagens remotas.

Aqui permaneci criando coleções com arrayListOf<>(), mas você pode seguramente utilizar outros tipos de collections, inclusive manter o código somente em Java.

ChipsListener para eventos

Caso em seu domínio do problema seja necessário ouvir: a entrada de texto do usuário; a criação de novos chips; ou a remoção de deles. Para isso utilize o ChipsListener:

class MainActivity :
AppCompatActivity(),
ChipsInput.ChipsListener {

override fun onCreate(savedInstanceState: Bundle?) {
...
chipsInput.addChipsListener( this )
}

override fun onTextChanged(texto: CharSequence?) {
/* Algo digitado pelo usuário no ChipsInput */
}
override fun onChipAdded(chip: ChipInterface?, novoTamanhoDoChipsInput: Int) {
/* Novo chip adicionado no ChipsInput */
}
override fun onChipRemoved(user: ChipInterface?, novoTamanhoDoChipsInput: Int) {
/* chip removido do ChipsInput */
}
}

 

O listener será muito útil caso em seu ChipsInput seja possível a entrada de contatos não presentes na lista de sugestão. Ao menos com o onTextChanged() é possível criar um novo chip e adiciona-lo ao ChipsInput caso algumas regras sejam encontradas no texto digitado, por exemplo: quando o usuário entra com um novo email.

Carregamento remoto de dados do ChipsInput

Para dados assíncronos, que chegam ao aplicativo depois do ChipsInput já ter sido iniciado e apresentado em tela, é preciso a modificação da lista de contatos vinculada ao ChipsInput, como a seguir:

...
/*
* FilterableList vazio. Mesmo vazio deve haver algum
* vinculado ao ChipsInput.
* */
chipsInput.setFilterableList( arrayListOf() )

/*
* O código de Thread a seguir simula o carregamento remoto
* de contatos a serem colocados no ChipsInput. Funciona sem
* problemas, o FilterableList anterior é substituído pelo
* atual, que está vazio de contatos.
* */
thread {
SystemClock.sleep(3000) /* Simulando um delay de 3 segundos de latência de rede */

/*
* A vinculação dos novos contatos deve ocorrer na Thread
* principal, pois componentes visuais serão modificados.
* */
runOnUiThread {
chipsInput.setFilterableList( DataBase.getContatos(this) )
}
}
...

 

Note que um novo objeto adicionado via chipsInput.addChip() não é adicionado a lista de sugestões, somente a lista de contatos selecionados.

Uma outra opção a substituição completa de lista é a adição da nova à já existente: chipsInput.filterableList.addAll( contatos ).

Configuração ChipView

O ChipView responde a todos os outros possíveis usos do componente chip via MaterialChipsInput. A seguir o XML de configuração dele em layout:

...
<com.pchmn.materialchips.ChipView
android:id="@+id/chip_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:label="Rótulo"
app:labelColor="@color/branco"
app:avatarIcon="@drawable/imagem"
app:backgroundColor="@color/azul"
app:deletable="true"
app:deleteIconColor="@color/branco" />
...

 

Todos os atributos app: são opcionais, mas como informado anteriormente nas especificações de chip componente: não faz sentido utilizar um chip sem um rótulo em texto. Por isso, não deixe de especificar ao menos o app:label.

A seguir o código de acesso ao ChipView anterior:

...
val chipView = findViewById(R.id.chip_view) as ChipView
chipView.label = "Rótulo novo"
...
chipView.setDeletable(false)
...

 

Como acontece com o ChipsInput, é possível criar o ChipView e toda a configuração dele também em código de programação. Porém não há necessidade do acesso via algoritmo como com o ChipsInput, podemos ter somente o XML definido e resultados visuais similares a:

Exemplos de ChipView

Somente trabalhe com o ChipView via algoritmo se a sua lógica de negócio exigir, isso para manter a separação conceitos a mais precisa possível.

Listeners de click e de remoção de ChipView

Caso queira também trabalhar eventos nos ChipViews de seu layout, tem as opções de listener de clique e listener de remoção de chip:

...
chipView.setOnChipClicked( object : View.OnClickListener() {
fun onClick( view: View ) {
/* Chip clicado - view é o Chip */
}
})

chipView.setOnDeleteClicked( object : View.OnClickListener() {
fun onClick( view: View ) {
/* Chip removido - view é o ícone de delete */
}
})
...

Fluxo para o carregamento de imagens remotas em chips de contato

Depois de vários testes é possível notar como é o comportamento interno do MaterialChipsInput API. Propriedades com o valor null não são utilizadas, mesmo que o null seja atribuído quando o objeto ChipInterface já tenha sido inicializado.

Para o trabalho com APIs de carregamento de imagens remotas, uma possibilidade é construir um algoritmo que siga o fluxo abaixo:

Fluxograma para baixar imagens no MaterialChipsInput

No aplicativo de exemplo deste artigo utilizaremos a API Picasso para este algoritmo de carregamento de imagem remota.

A seguir algumas outras APIs, de carregamento de imagens na Web, que poderão lhe ajudar na construção deste script:

Limitações da API

  • Quando a área de listagem de contatos selecionados no ChipsInput é pequena em tela, a abertura de algum dos chips, o menu flutuante, somente pode ser posteriormente fechada se deletarmos o item aberto, algo que ocasiona na remoção também do chip da listagem de contatos selecionados. Isso é um bug, pois ocorre somente nesta condição "área pequena";
  • Quando há a remoção de um chip de contato por meio da listagem de chips selecionados e este chip também estava no estado "aberto", uma exceção é gerada e o app é fechado. A API deveria gerenciar este caso e remover também o chip aberto;
  • Não há uma maneira trivial de obter imagens remotas, algo que possivelmente será necessário quando trabalhando com chips de contato;
  • A documentação falha em informar sobre possibilidades de carregamentos remotos, de contatos e de imagens, e não comenta sobre o comportamento interno da API de acordo com as propriedades preenchidas. Nós developers temos de descobrir por "tentativa e erro".;
  • Quando no campo de ChipsInput é fornecido o contato de algum usuário presente na lista de sugestões, ele não é adicionado automaticamente se o usuário não seleciona-lo manualmente pela lista, fica como se fosse um novo contato. Temos de construir na mão o algoritmo dessa funcionalidade simples;
  • Se o addChip() for utilizado sequencialmente, uma exceção é gerada.

Outras APIs

Caso queira estudar ainda mais APIs de chips componente, não deixe de acessar o link a seguir no Android-Arsenal: Chips API Android Arsenal.

Projeto Android

Para um teste real com a API de Material Chips, vamos construir um aplicativo de email, na verdade somente a tela de envio de email, onde teremos a possibilidade de seleção de contatos e de definição de tags.

Na primeira parte do algoritmo vamos ter somente o básico, sem uso de ChipsInput e ChipView. Na segunda parte vamos a essas atualizações, incluindo o uso de ainda mais APIs, como o Picasso, para completar o domínio do problema, dando também a possibilidade de carregamento remoto de imagens.

Note que o algoritmo completo, sem explicações completas, do projeto final você acessa no GitHub dele em: https://github.com/viniciusthiengo/faster-email.

Recomendo que acompanhe o conteúdo do artigo até o fim, pois os trechos de códigos serão explicados e assim o entendimento do chips componente junto a library MaterialChipsInput será tranquilo.

Protótipo estático

A seguir as imagens do protótipo estático, desenvolvido antes mesmo de iniciar um novo projeto no Android Studio:

Tela de entrada do app FasterEmail

Tela de entrada

Tela de envio de email do app FasterEmail

Tela de envio de email (preenchida)

Note, na tela de envio de email, como os contatos e as tags de conteúdo (com #) estão em texto simples, sem nenhuma formatação, algo que facilitaria ao menos a identificação dos contatos selecionados, para saber se são os desejados em envio.

Iniciando o projeto

Com o Android Studio aberto, inicie um novo projeto Kotlin. Pode seguir com um projeto Java, caso prefira. Coloque como nome "FasterEmail", como API mínima escolha o Android Jelly Bean, API 16. Como atividade inicial coloque uma a "Basic Activity".

Ao final dessa primeira parte continuaremos com a seguinte arquitetura de projeto:

Estrutura do app FasterEmail no Android Studio

Configurações Gradle

A seguir as configurações iniciais do Gradle Project Level, ou build.gradle (Project: FasterEmail):

buildscript {
ext.kotlin_version = '1.2.10'
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.0.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}

allprojects {
repositories {
google()
jcenter()
}
}

task clean(type: Delete) {
delete rootProject.buildDir
}

 

Assim as configurações iniciais do Gradle App Level, ou build.gradle (Module: app):

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'

android {
compileSdkVersion 26
defaultConfig {
applicationId "thiengo.com.br.fasteremail"
minSdkVersion 16
targetSdkVersion 26
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}

dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation"org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
implementation 'com.android.support:appcompat-v7:26.1.0'
implementation 'com.android.support:design:26.1.0'
}

 

Note que para ambos os arquivos de configuração temos o código inicialmente gerado pelo Android Studio para um novo projeto Kotlin, somente foram removidas as referências a APIs de testes, que não trabalharemos aqui.

Configurações AndroidManifest

Abaixo as configurações iniciais do AndroidManifest.xml:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="thiengo.com.br.fasteremail">

<application
android:allowBackup="true"
android:hardwareAccelerated="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">

<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:screenOrientation="portrait"
android:theme="@style/AppTheme.NoActionBar">

<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

Configurações de estilo

Para as configurações de tema, estilo, vamos iniciar com o arquivo de definição de cores, /res/values/colors.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#f47b00</color>
<color name="colorPrimaryDark">#ba4c00</color>
<color name="colorAccent">#1976d2</color>
<color name="colorHint">#969FAA</color>
<color name="colorDivider">#DCDCDC</color>
</resources>

 

Então o arquivo de definição de String, /res/values/strings.xml:

<resources>
<string name="app_name">FasterEmail</string>
<string name="ht_email_to">Para:</string>
<string name="ht_email_subject">Assunto</string>
<string name="ht_email_message">Mensagem de email</string>
<string name="send_button">Enviar</string>
</resources>

 

E por fim o arquivo de definição de temas, /res/values/styles.xml:

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

<style
name="AppTheme.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
</style>

<style
name="AppTheme.AppBarOverlay"
parent="ThemeOverlay.AppCompat.Dark.ActionBar" />

<style
name="AppTheme.PopupOverlay"
parent="ThemeOverlay.AppCompat.Light" />
</resources>

Atividade principal

A primeira parte do projeto, sem uso de API de chips, é bem simples, por isso já estamos na atividade principal. Aqui vamos iniciar com a apresentação do XML de conteúdo, /res/layout/content_main.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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:context="thiengo.com.br.fasteremail.MainActivity"
tools:showIn="@layout/activity_main">

<!-- Views de "Para" -->
<TextView
android:id="@+id/tv_to"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:paddingEnd="24dp"
android:paddingLeft="16dp"
android:paddingRight="24dp"
android:paddingStart="16dp"
android:paddingTop="26dp"
android:text="@string/ht_email_to"
android:textColor="@color/colorHint"
android:textSize="18sp" />

<EditText
android:id="@+id/et_to"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"
android:layout_toEndOf="@+id/tv_to"
android:layout_toRightOf="@+id/tv_to"
android:background="@android:color/transparent"
android:inputType="textEmailAddress"
android:paddingBottom="26dp"
android:paddingEnd="16dp"
android:paddingRight="16dp"
android:paddingTop="26dp" />

<View
android:id="@+id/vw_divider_1"
android:layout_width="match_parent"
android:layout_height="0.8dp"
android:layout_below="@+id/et_emails"
android:background="@color/colorDivider" />

<!-- Views de "Assunto" -->
<EditText
android:id="@+id/et_subject"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/vw_divider_1"
android:background="@android:color/transparent"
android:hint="@string/ht_email_subject"
android:inputType="textEmailSubject"
android:paddingBottom="26dp"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingTop="26dp"
android:textColorHint="@color/colorHint" />

<View
android:id="@+id/vw_divider_2"
android:layout_width="match_parent"
android:layout_height="0.8dp"
android:layout_below="@+id/et_subject"
android:background="@color/colorDivider" />

<!-- View de "Mensagem" -->
<EditText
android:id="@+id/et_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_below="@+id/vw_divider_2"
android:background="@android:color/transparent"
android:gravity="top"
android:hint="@string/ht_email_message"
android:inputType="textMultiLine"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingTop="26dp"
android:textColorHint="@color/colorHint" />
</RelativeLayout>

 

Então o diagrama do layout anterior:

Diagrama do XML de content_main.xml

Assim o XML do layout que contém o layout anterior, /res/layout/activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white"
tools:context="thiengo.com.br.fasteremail.MainActivity">

<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay">

<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay" />

</android.support.design.widget.AppBarLayout>

<include layout="@layout/content_main" />
</android.support.design.widget.CoordinatorLayout>

 

Agora o diagrama do layout activity_main.xml:

Diagrama do XML de activity_main.xml

E por fim o último arquivo XML que dispensa o uso de diagrama por ser muito simples, o XML do menu que será inflado na MainActivity para a apresentação do ícone de "Enviar email", /res/menu/menu_main.xml:

<menu
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"
tools:context="thiengo.com.br.fasteremail.MainActivity">

<item
android:id="@+id/action_settings"
android:orderInCategory="100"
android:title="@string/send_button"
android:icon="@drawable/ic_send_white_24dp"
app:showAsAction="always" />
</menu>

 

Agora o código Kotlin da MainActivity:

class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(toolbar)
}

/*
* Para a atualização dos título e ícone de topo
* esquerdo da Toolbar.
* */
override fun onResume() {
super.onResume()
toolbar.setNavigationIcon(R.drawable.ic_account_plus_white_24dp)
toolbar.setTitle("Email social para quem?")
}

/*
* Para o carregamento do XML de menu e assim a
* apresentação do ícone de "Enviar"
* */
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_main, menu)
return true
}
}

 

Assim podemos partir para a evolução do aplicativo, facilitando a seleção de contatos e o design de tags de conteúdo.

Melhorando a UX com ChipsInput e ChipView

O funcionamento atual do aplicativo de email é aceitável, mas pode melhorar consideravelmente se colocarmos chips de contato para trabalhar junto ao campo de entrada de email e ChipView para as tags definidas na corpo do email.

Estas atualizações permitirão uma melhor experiência do usuário, principalmente o uso do ChipsInput que permitirá que o usuário emissor saiba exatamente para quem está enviando a mensagem.

Protótipo estático da nova versão

A baixo o protótipo estático da tela que modificaremos na nova versão do aplicativo:

Tela com chips de contato e chips de tag

 Tela com chips de contato e chips de tag

Tela com chips de contato aberto

 Tela com chips de contato aberto

Atualizando os arquivos Gradle do projeto

Iniciando com as novas configurações, em destaque, do arquivo Gradle Project Level, ou build.gradle (Project: FasterEmail):

...
allprojects {
repositories {
google()
jcenter()
maven { url "https://jitpack.io" }
}
}
...

 

Assim o arquivo Gradle App Level, ou build.gradle (Module: app), configuração nova também em destaque:

...
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation"org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
implementation 'com.android.support:appcompat-v7:26.1.0'
implementation 'com.android.support:design:26.1.0'

implementation 'com.github.pchmn:MaterialChipsInput:1.0.8'
}

 

Por fim, sincronize o projeto. Caso tenha problemas de incompatibilidade de versão para a referência com.android.support:design:X.X.X, uma solução rápida é comentar e referência direta a API do Material Support Library e ficar somente com a versão presente em com.github.pchmn:MaterialChipsInput:X.X.X.

Colocando o ChipsInput na atividade principal

Agora podemos colocar o ChipsInput funcionando na tela onde os contatos podem ser informados, na MainActivity.

Logo, no layout /res/layout/content_main.xml coloque ChipsInput no lugar do EditText de id et_emails:

<?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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:context="thiengo.com.br.fasteremail.MainActivity"
tools:showIn="@layout/activity_main">

<!-- Views de "Para" -->
<TextView
android:id="@+id/tv_to"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:paddingEnd="24dp"
android:paddingLeft="16dp"
android:paddingRight="24dp"
android:paddingStart="16dp"
android:paddingTop="26dp"
android:text="@string/ht_email_to"
android:textColor="@color/colorHint"
android:textSize="18sp" />

<com.pchmn.materialchips.ChipsInput
android:id="@+id/ci_contacts"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"
android:layout_marginBottom="12dp"
android:layout_marginTop="18dp"
android:layout_toEndOf="@+id/tv_to"
android:layout_toRightOf="@+id/tv_to"
android:background="@android:color/transparent"
android:inputType="textEmailAddress"
android:paddingBottom="0dp"
android:paddingEnd="16dp"
android:paddingRight="16dp"
android:paddingTop="0dp"
app:chip_deletable="true"
app:chip_hasAvatarIcon="true"
app:filterable_list_backgroundColor="#f1f1f1"
app:maxRows="1" />

<View
android:id="@+id/vw_divider_1"
android:layout_width="match_parent"
android:layout_height="0.8dp"
android:layout_below="@+id/ci_contacts"
android:background="@color/colorDivider" />
...
</RelativeLayout>

 

Note que foram colocados vários atributos e valores no ChipsInput para que a visualização em tela seja exatamente como era quando sem o este novo componente visual.

Com a atualização de layout, o diagrama de content_main.xml é agora como a seguir:

Diagrama do layout XML content_main.xml

Assim temos ainda de inicializar o ChipsInput no código dinâmico da MainActivity:

class MainActivity :
AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
...

ci_contacts.setFilterableList( arrayOfList<Chip>() )
}
}

 

Como informado na área de estudos somente da API MaterialChipsInput: mesmo que vazia, deve haver uma lista de objetos ChipInterface vinculada ao ChipsInput. Até está seção do artigo vamos deixar assim, com uma lista vazia do tipo Chip, pois a seguir iremos construir nossa própria classe de domínio com carregamento de imagem remota.

Criando uma nova classe chip, User

Primeiro devemos criar um novo package, /domain. No pacote raíz do projeto:

  1. Clique com o botão direito do mouse;
  2. Acesse "New";
  3. Clique em "Package";
  4. Digite "domain" e clique em "Ok".

Criando um novo pacote no Android Studio

Logo depois, no novo package, crie uma nova classe, User, já implementando a Interface ChipInterface:

class User(
private val avatarUri: Uri,
private val label: String,
private val info: String ) : ChipInterface {

override fun getId() = null /* Não utilizaremos id. */

override fun getAvatarUri() = avatarUri

override fun getAvatarDrawable() = null /* Ainda vamos estudar o uso de avatarDrawable. */

override fun getLabel() = label

override fun getInfo() = info
}

 

Temos de ter propriedades privadas para avatarUri, label e info, pois são as propriedades que inicialmente iremos utilizar em nossa implementação de ChipInterface e também porque está Interface nos obriga a implementar os métodos get de: id, avatarUri, avatarDrawable, label e info.

Caso estas propriedades fossem públicas o Kotlin se encarregaria de fornecer as versões de get de cada uma delas, fazendo com que a sobrescrita dos get exigisse um código menos trivial do que o anterior com entidades privadas.

Algoritmo de carregamento de imagem remota

Nossa classe User ainda não está como desejamos, com a possibilidade de carregamento de imagens na Web. Para isso vamos utilizar a library Picasso, por ser simples e robusta quanto a tarefa de carregamento de imagens.

No Gradle App Level, build.gradle (Module: app), coloque a seguinte nova referência, em destaque, e sincronize o projeto:

...
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation"org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
implementation 'com.android.support:appcompat-v7:26.1.0'
implementation 'com.android.support:design:26.1.0'

implementation 'com.squareup.picasso:picasso:2.5.2'
implementation 'com.github.pchmn:MaterialChipsInput:1.0.8'
}

 

Agora temos de lembrar que o MaterialChipsInput trabalha da seguinte forma com a apresentação de imagem de perfil, avatar:

  • Com avatarUri presente, tente o carregamento da imagem local;
  • Com avatarDrawable presente e avatarUri igual a null, apresente o valor de avatarDrawable como imagem de avatar no chips de contato.

Nosso algoritmo de imagem de avatar em User funcionará exatamente como explicado em Fluxo para o carregamento de imagens remotas em chips de contato, relembrando:

Fluxograma de carregamento de imagem remota com o MaterialChipsInput

A imagem local será uma imagem da "letra inicial" do valor de label, que em nosso caso é o nome do contato, pois em info teremos o email dele. Note que você tem acesso a todas essas imagens locais do projeto e também a outros recursos, indo no GitHub dele.

Antes de partirmos para o código com a Picasso API, vamos primeiro criar o algoritmo que fornecerá os identificadores corretos das imagens locais.

Na raíz do projeto crie um novo pacote com o nome util. Em seguida crie nele uma nova classe Kotlin com o rótulo Util:

abstract class Util {
companion object {
fun getImageResource(text: String?) : Int {
val t = text ?: ""
val letra =
if( t[0].equals('a', true) ) R.drawable.a
else if( t[0].equals('b', true) ) R.drawable.b
else if( t[0].equals('c', true) ) R.drawable.c
else if( t[0].equals('d', true) ) R.drawable.d
else if( t[0].equals('e', true) ) R.drawable.e
else if( t[0].equals('f', true) ) R.drawable.f
else if( t[0].equals('g', true) ) R.drawable.g
else if( t[0].equals('h', true) ) R.drawable.h
else if( t[0].equals('i', true) ) R.drawable.i
else if( t[0].equals('j', true) ) R.drawable.j
else if( t[0].equals('k', true) ) R.drawable.k
else if( t[0].equals('l', true) ) R.drawable.l
else if( t[0].equals('m', true) ) R.drawable.m
else if( t[0].equals('n', true) ) R.drawable.n
else if( t[0].equals('o', true) ) R.drawable.o
else if( t[0].equals('p', true) ) R.drawable.p
else if( t[0].equals('q', true) ) R.drawable.q
else if( t[0].equals('r', true) ) R.drawable.r
else if( t[0].equals('s', true) ) R.drawable.s
else if( t[0].equals('t', true) ) R.drawable.t
else if( t[0].equals('u', true) ) R.drawable.u
else if( t[0].equals('v', true) ) R.drawable.v
else if( t[0].equals('w', true) ) R.drawable.w
else if( t[0].equals('x', true) ) R.drawable.x
else if( t[0].equals('y', true) ) R.drawable.y
else if( t[0].equals('z', true) ) R.drawable.z
else R.drawable.background

return letra
}
}
}

 

getImageResource() poderia ser utilizado em vários outros contextos e não tinha vinculo direto com a responsabilidade da classe User, assim foi preferível coloca-lo em uma classe utilitária.

O uso de um companion object é devido a não necessidade de criação explicita de instância de Util somente para acesso ao método getImageResource(), por isso também definimos está classe como uma entidade abstrata. Esse é um modelo de trabalho que eu utilizo em meus algoritmos quando com classes utilitárias.

Assim podemos partir para os códigos que contém entidades da biblioteca Picasso. Iniciando pela criação de uma classe Target do pacote com.squareup.picasso.

No pacote /util crie a classe ImageReceiver:

class ImageReceiver(val user: User): Target {
override fun onPrepareLoad(placeHolderDrawable: Drawable?) {}

override fun onBitmapFailed(errorDrawable: Drawable?) {}

override fun onBitmapLoaded(bitmap: Bitmap?, from: Picasso.LoadedFrom?) {
user.setAvatarDrawable(
BitmapDrawable(
user.getContext().resources,
bitmap )
)
}
}

 

Iremos passar user como parâmetro para que seja possível o fácil acesso a avatarDrawable, que ainda temos de definir em User, e também o fácil acesso a um objeto de contexto. Assim será possível a atualização da imagem de perfil caso o carregamento ocorra com sucesso.

Agora em User, coloque as seguintes atualizações em destaque:

class User(
private val context: Context, /* Para que seja possível o uso da API de carregamento de imagens remotas */
private var avatarUri: Uri?, /* Agora var para que seja possível colocar null quando avatarDrawable estiver preenchido. */
private val label: String,
private val info: String ) : ChipInterface {

/*
* Necessário ter o Target (ImageReceiver) como propriedade
* de classe. Caso contrário o carregamento das imagens
* na primeira abertura do aplicativo não é consistente.
* */
private val receiver: ImageReceiver
private var avatarDrawable: Drawable? = null

init {
receiver = ImageReceiver(this)
downloadImage()
}

fun getContext() = context

override fun getId() = null /* Não utilizaremos id. */

override fun getAvatarUri() = avatarUri

override fun getAvatarDrawable() = avatarDrawable

override fun getLabel() = label

override fun getInfo() = info

/*
* Método necessário para que ao final do carregamento
* da imagem remota, ela possa ser colocada no lugar
* da imagem local (letra de nome) inicialmente colocada
* em avatarDrawable.
* */
fun setAvatarDrawable( drawable: Drawable ){
avatarDrawable = drawable
}

private fun downloadImage(){
/*
* Hack code para que enquanto o status da imagem
* remota seja "carregando", uma imagem de perfil
* com a primeira letra do nome do usuário seja
* utilizada.
* */
avatarDrawable = ContextCompat.getDrawable(
context,
Util.getImageResource(label) )

/*
* Carregando a imagem remota com a API Picasso.
* */
if( avatarUri != null ) {
Picasso.with( context )
.load( avatarUri.toString() )
.into( receiver )
}

/*
* Colocando avatarUri com o valor null nós estamos
* garantindo que somente avatarDrawable será utilizado
* como recurso de imagem nos chips da API em uso. Ele
* é colocado como null aqui, pois primeiro temos de
* passar o path da imagem para a Picasso API realizar
* o download correto.
* */
avatarUri = null
}
}

 

Ok Thiengo, lendo os comentários do novo código de User eu entendi tudo, exceto a verificação de null, no método downloadImage(), antes de invocar a API Picasso. Isso é mesmo necessário?

Sim, é necessário. Apesar dessa verificação ser "um possível caminho" devido ao avatarUri poder ser null, há um caso em específico em que o usuário nunca terá uma imagem de perfil remota, uma URL. Quando o usuário informado em ChipsInput não constar na lista de sugestões vinculada a este componente. Neste caso utilizaremos somente a imagem de letra.

Importante ressaltar que a classe Target, ImageReceiver, precisa ser uma propriedade de classe, caso contrário não haverá carregamento inicial de imagem, pois o sistema Android, não encontrando referências fortes para objetos deste tipo, removerá todos da memória.

Atualizando o AndroidManifest

É preciso colocar a permissão de Internet no AndroidManifest.xml, caso contrário não teremos a possibilidade de carregamento de imagem:

<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="thiengo.com.br.fasteremail">

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

...
</manifest>

 

Agora precisamos acessar alguma base de contatos dos usuários.

Base de dados simulados, mock data

Como já fizemos em vários projetos aqui do Blog, vamos seguir este com uma base de dados simulados, base local. Principalmente porque já mostramos no início do artigo como seria o trabalho com dados carregados de bases remotas, onde atualizar em ChipsInput.

Na raíz do projeto crie um novo pacote nomeado data. No novo pacote crie uma nova classe Kotlin, DataBase:

/*
* Classe mock para simular um banco de dados no
* exemplo com a API MaterialChipsInput
* */
class DataBase {
companion object {

fun getContacts(context: Context) = arrayListOf<User>(
User(
context,
Uri.parse("https://i.pinimg.com/736x/dc/5c/ca/dc5ccad5bd921a27a657ecfada3f00de--live-life-anti-aging.jpg"),
"Mathilda Gallop",
"mathilda.gallop@gmail.com" ),
User(
context,
Uri.parse("https://i.pinimg.com/736x/d5/7a/e1/d57ae1e0abaa478e79388007b6d6dd09--woman-face-woman-style.jpg"),
"Mathilda Gallop Souza",
"mathilda.gallop@gmail.com" ),
User(
context,
Uri.parse("https://static1.squarespace.com/static/560c6ac4e4b081f0a96d5b42/560dee8ce4b035d279a65441/560dee92e4b0631d94ac0d31/1443753769057/Ilona+Concetta+Cute.jpg"),
"Concetta Hartson",
"concetta.hartson@gmail.com" ),
User(
context,
Uri.parse("http://www2.pictures.zimbio.com/gi/Elmer+Figueroa+Arce+XcBUdTwIuJEm.jpg"),
"Elmer Malick",
"elmer.malick@gmail.com" ),
User(
context,
Uri.parse("https://pbs.twimg.com/profile_images/692429203254280193/E7BfX3FW.jpg"),
"Denita Konecny",
"denita.konecny@gmail.com" ),
User(
context,
Uri.parse("http://provenmgmt.com/wp-content/uploads/2014/09/denita-conway.jpg"),
"Denita Konecny",
"denita.konecny.dogs.house@gmail.com" ),
User(
context,
Uri.parse("http://strengthforthesoul.com/wp-content/uploads/2015/10/Cindi-McMenamin-sm.jpg"),
"Cindi Saylor",
"cindi.saylor@gmail.com" ),
User(
context,
Uri.parse("https://onmilwaukee.com/images/articles/pa/packers-aaron-rodgers-face-gif/packers-aaron-rodgers-face-gif_fullsize_story1.jpg"),
"Aaron Hope",
"aaron.hope@gmail.com" ),
User(
context,
Uri.parse("https://i.pinimg.com/736x/8e/2c/c6/8e2cc6c9a93d362bf159c0f288414ad7--childhood-director.jpg"),
"Eugenio Pinales",
"eugenio.pinales@gmail.com" ),
User(
context,
Uri.parse("https://www.milbank.com/images/content/9/1/v2/91034/Yusman-Rosaline-SG-web.jpg"),
"Rosaline Roehrig",
"rosaline.roehrig@gmail.com" )
)
}
}

 

Novamente trabalhando com companion object para não ser necessária a criação explícita de uma instância de DataBase, facilitando assim o trabalho quando utilizando essa base mock.

Agora seguramente podemos atualizar a MainActivity para que ChipsInput seja inicializado com uma base real de contatos:

class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
...

ci_contacts.setFilterableList( DataBase.getContacts(this) )
}
...
}

Algoritmo de novo contato e de contato já existente em lista

O projeto ainda tem problemas na apresentação no novos contatos, aqueles ainda não presentes em lista de sugestão, e também problemas com contatos presentes em lista.

Também com contatos em lista?

Sim, caso o usuário digite todo o email de um dos usuários presente em lista de sugestão, o algoritmo atual simplesmente ignora esta informação, uma limitação da MaterialChipsInput, e deixa o texto de email como está. Alias, em nosso domínio do problema, qualquer email fornecido em ChipsInput deve ficar com formatação de um chips de contato, mesmo um novo email.

Sendo assim, é óbvio que teremos de trabalhar com a Interface ChipsInput.ChipsListener, pois com ela será possível obter o texto informado pelo usuário no ChipsInput e então saber se é um email. Sendo um email: trabalhar a criação de um novo User ou obter um já em lista de contatos.

Com isso nossa primeira tarefa é construir um algoritmo de verificação de email. Na classe Util coloque o seguinte novo método:

abstract class Util {
companion object {
fun isEmail(email: String) =
Pattern
.compile("^(([\\w-]+\\.)+[\\w-]+|([a-zA-Z]|[\\w-]{2,}))@"
+ "((([0-1]?[0-9]{1,2}|25[0-5]|2[0-4][0-9])\\.([0-1]?"
+ "[0-9]{1,2}|25[0-5]|2[0-4][0-9])\\."
+ "([0-1]?[0-9]{1,2}|25[0-5]|2[0-4][0-9])\\.([0-1]?"
+ "[0-9]{1,2}|25[0-5]|2[0-4][0-9]))|"
+ "([a-zA-Z]+[\\w-]+\\.)+[a-zA-Z]{2,4}[ ,]{1})$")
.matcher(email)
.matches()
}
}

 

Agora na MainActivity coloque os códigos em destaque:

class MainActivity :
AppCompatActivity(),
ChipsInput.ChipsListener {

override fun onCreate(savedInstanceState: Bundle?) {
...

ci_contacts.addChipsListener( this )
ci_contacts.setFilterableList( DataBase.getContacts(this) )
}
...

/*
* Para adicionar um novo contato como um ChipView,
* contato não presente na lista do usuário. Assim
* que o email é reconhecido o contato é adicionado.
* */
override fun onTextChanged(contact: CharSequence?) {
if( Util.isEmail( contact.toString() ) ){
ci_contacts.addChip( getUserByEmail( contact.toString() ) )
}
}
override fun onChipAdded(user: ChipInterface?, p1: Int) {}
override fun onChipRemoved(user: ChipInterface?, p1: Int) {}

/*
* Retorna um usuário já presente na lista contida no
* ChipsInput ou um novo User caso seja um novo endereço
* de email.
* */
private fun getUserByEmail(contact: String): ChipInterface {
/*
* Para que a vírgula ou o espaço em branco não faça
* parte do email.
* */
val email = contact.substring(0, contact.length - 1)

for( user in ci_contacts.filterableList ){
if( user.info.equals(email, true) ){
return user
}
}

return User(
this,
null,
email.split("@")[0], /* Obtendo somente o nick antes do @ do endereço de email. */
email
)
}
}

 

Depois de identificado que o texto de entrada do usuário é um email, temos ainda de verificar se é o email de um usuário em lista ou de um novo usuário e assim realizar a adição correta ao ChipsInput.

Com isso terminamos a parte de chips de contato de nosso aplicativo de email. Agora temos de trabalhar os algoritmos de verificação de tags em corpo de email.

Fluxo do algoritmo de tag

Para sabermos o que teremos de codificar para a identificação e geração de chip de tag, vamos primeiro ao fluxograma do algoritmo, fluxo de geração de um único chip de tag:

Fluxograma do algoritmo de geração de Chip hashtag

Assim vamos precisar de:

  • Listener de digitação do EditText de mensagem;
  • Algoritmo de verificação de tag no final do texto de mensagem;
  • Algoritmo de obtenção de tag do texto completo;
  • Algoritmo de geração de ChipView da tag encontrada;
  • Algoritmo de geração de Bitmap a partir de um ChipView;
  • Algoritmo de geração de Drawable a partir de um Bitmap;
  • Algoritmo de mudança de conteúdo de Spanned String, de texto para ImageSpan.

Vamos iniciar pelos mais simples e independentes.

Algoritmo de verificação de tag ao final do texto

Na classe Util, adicione o seguinte método em destaque:

abstract class Util {
companion object {
...

fun containsHashTag(texto: String) =
texto
.contains("\\B(\\#[A-zÀ-úA-zÀ-ÿ]+\\b)(?![^A-zÀ-úA-zÀ-ÿ])".toRegex())
}
}

 

O retorno é do tipo Boolean (true / fase).

Algoritmo de obtenção de tag do texto completo

Ainda na classe Util, vamos criar o método que retornará a tag encontrada ao final do texto completo informado pelo usuário até o momento da última digitação dele:

abstract class Util {
companion object {
...

/*
* Definindo o padrão de expressão regular e retornando o
* match dele direto do texto que o usuário informou.
* */
fun getHashTagMatch(text: String) =
Pattern
.compile("\\B(\\#[A-zÀ-úA-zÀ-ÿ]+\\b)").matcher(text)
}
}

 

Como sempre estaremos verificação tags a partir da última digitação do usuário, é certo que o grupo de match retornado conterá ou uma palavra ou nenhuma.

Algoritmo de geração de Bitmap partindo de um ChipView

Ainda na classe Util, adicione o método a seguir em destaque:

abstract class Util {
companion object {
...

fun createBitmapFromView(v: View): Bitmap {
val width: Int
val height: Int

/*
* Em algumas versões do Android o v.measuredHeight não
* será maior do que 0, por isso a necessidade desses
* condicionais e algoritmos específicos.
* */
if(v.measuredHeight <= 0) {
v.measure(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT)
width = v.measuredWidth
height = v.measuredHeight
v.layout(0, 0, width, height)
}
else {
width = v.layoutParams.width
height = v.layoutParams.height
v.layout(v.left, v.top, v.right, v.bottom)
}
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
v.draw( Canvas(bitmap) )

return bitmap
}
}
}

 

Apesar do uso do método createBitmapFromView() somente com ChipView nesse aplicativo de email, ele é genérico e funcionaria com qualquer outra View.

O trabalho com LinearLayout é opcional, você poderia colocar outro ViewGroup que funcionaria sem problemas.

Algoritmo de geração de Drawable e de atualização de Spanned String

Ainda na classe Util, adicione o método retrieveSpannableWithBitmap():

abstract class Util {
companion object {
...

fun retrieveSpannableWithBitmap(
context: Context,
spannable: SpannableStringBuilder,
bitmap: Bitmap,
startPositionTexto: Int,
endPositionTexto: Int ): SpannableStringBuilder {

/* Deletando a parte de texto que contém a tag. */
spannable.delete( startPositionTexto, endPositionTexto )

/*
* Criando um novo Drawable partindo do Bitmap informado
* como parâmetro.
* */
val imgDrawable = BitmapDrawable( context.resources, bitmap )
imgDrawable.setBounds(
0,
0,
imgDrawable.getIntrinsicWidth(),
imgDrawable.getIntrinsicHeight() )

/*
* Colocando a nova ImageSpan ocupando um único caractere
* no conteúdo total da Spanned String.
* */
val imgSpan = ImageSpan(imgDrawable, ImageSpan.ALIGN_BASELINE)
spannable.setSpan(
imgSpan,
startPositionTexto,
startPositionTexto + 1,
Spannable.SPAN_INCLUSIVE_EXCLUSIVE )

return spannable
}
}
}

 

É muito importante que você leia todos os comentários presentes nos algoritmos apresentados, assim entenderá o porque de cada trecho de código.

Como os algoritmos de geração de Drawable e atualização de SpannableStringBuilder são pequenos, foi entendido como eficiente coloca-los todos em um único método.

Mais sobre como trabalhar com Spanned  String no Android, no artigo a seguir: Como Utilizar Spannable no Android Para Customizar Strings.

Listener de entrada de texto no campo de mensagem de email

Na atividade principal adicione os seguintes códigos em destaque:

class MainActivity :
AppCompatActivity(),
ChipsInput.ChipsListener,
TextWatcher {

override fun onCreate(savedInstanceState: Bundle?) {
...

et_message.addTextChangedListener( this )
}
...

/*
* Para que seja possível obter o conteúdo informado pelo usuário
* e se encontrada alguma tag ao final, troca-la por uma imagem,
* um ChipView.
* */
override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {}
override fun afterTextChanged(p0: Editable?) {}
override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {}
}

 

Dos métodos obrigatórios a implementação de TextWatcher, utilizaremos somente o método onTextChanged().

Algoritmo de tag na atividade principal

Aqui vamos juntar todas as "peças" adicionadas anteriormente na classe Util e mais algumas que estarão diretamente vinculadas a atividade principal. Leia todos os comentários e adicione os códigos em destaque:

class MainActivity ... {

/*
* Flag para que seja possível somente invocar o
* algoritmo de extração de tag quando houver uma.
* */
var hasTag: Boolean = false
...

/*
* Para que seja possível obter o conteúdo informado pelo usuário
* e se encontrada alguma hashtag ao final, troca-la por uma
* imagem, ChipView.
* */
override fun onTextChanged(text: CharSequence?, p1: Int, p2: Int, p3: Int) {
if( Util.containsHashTag( text.toString() ) ){
/*
* Enquanto o padrão de tag permanecer sendo
* encontrado no texto informado pelo usuário, mantenha
* entrando aqui para poder gravar na flag que há uma
* tag no texto.
* */
hasTag = true
}
else if( hasTag ){
/*
* A entrada aqui somente ocorre porque não mais está
* sendo encontrado um padrão de tag ao final do
* texto informado pelo usuário, porém a flag hasTag
* aponta que ainda há uma tag não extraída para
* dar lugar a uma imagem.
* */
hasTag = false
changeHashTagToImage( text.toString() )
}

}
...

private fun changeHashTagToImage( text: String ){
/* Obtendo a última tag presente em texto. */
val match = Util.getHashTagMatch(text)

while( match.find() ) {
val hashTag = match.group()
val beginPosition = text.indexOf(hashTag)
val endPosition = text.indexOf(hashTag) + hashTag.length

/* Criando Bitmap de um ChipView. */
val chipView = ChipView(this)
chipView.label = hashTag.replace("#", "") /* O # não ficará no Bitmap gerado de ChipView. */
val imgBitmap = Util.createBitmapFromView( chipView )

/*
* A obtenção da Spanned String do EditText como SpannableStringBuilder
* é necessária para que seja possível remover a tag em texto e
* deixar somente a versão em imagem.
* */
var spannable = et_message.text as SpannableStringBuilder
spannable = Util.retrieveSpannableWithBitmap(
this,
spannable,
imgBitmap,
beginPosition,
endPosition )

et_message.setText( spannable )
et_message.setSelection( spannable.length ) /* Para manter o cursor no final do texto no EditText. */
}
}
}

 

Note que até mesmo uma flag está sendo utilizada para deixar o código mais preciso. Ela é responsável por informar se tem ou não uma tag ao final do texto, isso logo depois de o usuário terminar a digitação que faz com que o condicional if( Util.containsHashTag( text.toString() ) ) seja verdadeiro.

Assim podemos partir para os testes.

Testes e resultados

Antes de executar o projeto vamos colocar um código de debug na MainActivity caso o usuário acione o "Enviar" a direita na barra de topo:

class MainActivity ... {
...

override fun onOptionsItemSelected(item: MenuItem?): Boolean {
/*
* Percorrendo todos os contatos em lista depois
* do acionamento do item "Enviar".
* */
for( i in ci_contacts.selectedChipList ){
Log.i("log", "User:");
Log.i("log", " - Label: ${i.label}");
Log.i("log", " - Info: ${i.info}");
}
Log.i("log", "Email:")
Log.i("log", " - Mensagem: ${et_message.text}");

return super.onOptionsItemSelected(item)
}
...
}

 

Executando o projeto e entrando com os tipos de textos corretos nos campos corretos, temos:

Animação do aplicativo Android FasterEmail

O debug via LogCat gera:

... I/log: User:
... I/log: - Label: thi
... I/log: - Info: thi@hotmail.com
... I/log: User:
... I/log: - Label: Aaron Hope
... I/log: - Info: aaron.hope@gmail.com
... I/log: Email:
... I/log: - Mensagem: Apenas uma de testes.

 

Com isso terminamos a apresentação do componente Chip do Material Design Android e da API MaterialChipsInput.

Note que há duas limitações evidente no projeto de exemplo:

  • O algoritmo de obtenção de tag somente funciona para tags ao final do texto informado pelo usuário, se o user voltar e colocar uma tag antes do final, não funciona;
  • Quando obtendo o texto completo de mensagem de email, em formato de String, os locais das ImageSpan ficam em branco, é preciso alguma maneira de manter o texto da tag ainda presente mesmo com o uso de ImageSpan.

As limitações acima ficam para ti como desafio para expansão de funcionalidade. Dica para a última: você somente não precisa mais remover a tag em texto e colocar a ImageSpan por toda a extensão dela ao invés de ocupar somente um caractere.

Não deixe de se inscrever na 📩 lista de emails do Blog, logo acima ou ao lado, para receber em primeira mão os conteúdos exclusivos sobre o dev Android.

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

Slides

Abaixo os slides com a apresentação completa do componente Chip e da library MaterialChipsInput:

Vídeo

Abaixo o vídeo com a implementação passo a passo do MaterialChipsInput e a apresentação do componente chip:

Para acesso completo ao projeto FasterEmail, entre no GitHub a seguir: https://github.com/viniciusthiengo/faster-email.

Conclusão

Com o componente chip é possível melhorar consideravelmente a experiência do usuário e passar ainda mais profissionalismo seu ao aplicativo construído.

Podemos seguramente aguardar o lançamento de uma API nativa para este componente, tanto para contatos como também para outros contextos.

Mas até que essa API nativa seja desenvolvida, podemos manter o uso, por exemplo, da MaterialChipsInput que dá suporte a partir da API 15 do Android, cobrindo mais de 99% do mercado.

As limitações da API estudada em artigo não removem o bom custo benefício dela, mesmo assim, caso você não necessite de chips em todos os contextos possíveis, tente utilizar alguma API mais específica, que permita o trabalho somente com chips de contato, por exemplo, ou chips para tag.

Não esqueça de se inscrever na 📩 lista de emails e de deixar o seu comentário sobre o que achou do conteúdo.

Abraço.

Fontes

Material Design Guideline - Chips

Documentação oficial MaterialChipsInput API

Como Utilizar Spannable no Android Para Customizar Strings

Converting a view to Bitmap without displaying it in Android? - Resposta de Simon Heinen

Receba em primeira mão, e com prioridade, os conteúdos Android exclusivos do Blog.
Email inválido

Relacionado

Kotlin Android, Entendendo e Primeiro ProjetoKotlin Android, Entendendo e Primeiro ProjetoAndroid
Como Criar Protótipos AndroidComo Criar Protótipos AndroidAndroid
ViewModel Android, Como Utilizar Este Componente de ArquiteturaViewModel Android, Como Utilizar Este Componente de ArquiteturaAndroid
BottomNavigationView Android, Como e Quando UtilizarBottomNavigationView Android, Como e Quando UtilizarAndroid

Compartilhar

Comentários Facebook

Comentários Blog

Para código / script, coloque entre [code] e [/code] para receber marcação especifica.
Forneça seu nome válido.
Forneça seu email válido.
Forneça o comentário.
Enviando, aguarde...