Como Utilizar Spannable no Android Para Customizar Strings

Investir em Você é Barra de Ouro a R$ 2,00. Cadastre-se e receba grátis conteúdos Android sem precedentes! Você receberá um email de confirmação. Somente depois de confirma-lo é que eu poderei lhe enviar os conteúdos semanais exclusivos. Os artigos em PDF são entregues somente para os inscritos na lista.

Email inválido.
Blog /Android /Como Utilizar Spannable no Android Para Customizar Strings

Como Utilizar Spannable no Android Para Customizar Strings

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

Tudo bem?

Neste artigo vamos entender o que é e como utilizar Span nas Strings de nossos aplicativos Android.

Estas permitem que possamos colocar estilos em nível de caractere, algo não possível, de forma trivial, quando aplicando estilos diretamente na View em uso, um TextView, por exemplo.

No artigo vamos estar trabalhando dois projetos Android, um para a apresentação geral do assunto, como fizemos no artigo do FlexboxLayout, e outro para mostrar como aplicar Spans em Strings quando em um projeto real, aqui será um projeto de chat:

Telas do aplicativo Android de chat

Todo o conteúdo em texto também está presente no vídeo, em Vídeo com a implementação dos projetos.

Antes de prosseguir, não esqueça de se inscrever 📫na lista de e-mails do Blog para receber todas as atualizações, em primeira mão, sobre o desenvolvimento Android.

Abaixo os tópicos que estaremos discutindo:

Estilo em bloco e estilo em nível de caractere

Caso você seja um desenvolvedor Android e também um desenvolvedor Web, provavelmente já teve a necessidade de estilizar somente um pequeno trecho do conteúdo de um TextView e acabou caindo na solução onde um Html.fromHtml() se saiu como a mais viável, certo?

Isso, pois, para colocarmos um texto em negrito em um TextView, utilizando os atributos de estilo dessa View, temos de: ou isolar o texto que precisa estar em negrito no próprio TextView dele; ou modificar algumas regras de negócio do sistema para que o texto por completo fique em negrito ou nenhuma parte fique em negrito.

Veja o código a seguir:

...
<TextView
android:text="Android:"
android:textStyle="bold"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:text="o Android é um SO ..."
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
...

 

Esse código dentro de um LinearLayout com o atributo android:orientation="horizontal" consegue o estilo desejado: somente o termo "Android" e os ":" seguidos em negrito. Mas nem de perto essa é a solução ideal.

Tenha em mente que a configuração de estilo aplicada diretamente em Views, esse tipo de estratégia de configuração é conhecida como "configuração de estilo em bloco".

Ok, mas eu ainda preciso de detalhes em algumas configurações de estilo, isso indica que somente o Html.fromHtml() é que vai me ajudar? Digo isso, pois sei das limitações desta solução.

Não. O Html.fromHtml() é apenas uma solução possível e não é a mais eficiente, isso, pois o método fromHtml() não interpreta todas as tags. Uma outra opção, aqui para links, é o Linkify, mas somente para links, até mesmo telefônicos.

Mas em termos de performance e também de possibilidade de personalização, quando se tratando de estilo a nível de caractere, o trabalho com os CharSequenceStringSpannable e StringSpannableBuiilder.

Essas se saem como sendo a melhor solução. E, acredite, essas classes não são novas no Android, apesar de pouco discutidas.

Falei muito sobre o TextView como uma View de exemplo para customização a nível de caractere, mas em alguns casos uma SpannableString é a única opção, pois não há TextView envolvido.

Um exemplo? Os itens de menu de um NavigationView. Esses para passarem por customização, somente acessando a String deles e então colocando nelas alguns Spans de estilo.

Estrutura CharSequence

O CharSequence é a Interface ancestral de todas as classes de String / Spanned no Java. Entendendo essa estrutura fica muito mais simples saber qual classe utilizar, para trabalho com texto, em seus algoritmos Android.

A seguir uma parte do diagrama de CharSequence, o necessário para entendermos sobre as entidades de estilo que estaremos utilizando aqui:

  • Diagrama de classe da hierarquia de CharSequence no Java

    CharSequence: Interface Java que representa o contrato de somente leitura de sequência de caracteres;
  • String: representa uma sequência de caracteres onde o que temos na verdade é uma constante, pois qualquer atualização na String gera um novo objeto. Sequências de caracteres literais no Java são sempre String;
  • StringBuilder: diferente de String, com essa entidade é possível trabalhar uma sequência de caracteres e não ter novos objetos sendo criados a cada operação, StringBuilder é também conhecido como: uma String mutável;
  • Spanned: Interface que cria um contrato de possibilidade de anexar, às sequências de caracteres, objetos de marcação;
  • SpannedString: classe concreta que representa sequências de caracteres que são imutáveis, incluindo que as marcações, ou estilos, também não podem ser alterados;
  • Spannable: Interface que cria contrato com classes onde as marcações podem ser anexadas e desanexadas, sendo a classe de texto mutável ou não;
  • SpannableString: classe concreta que permite o trabalho de mudança de marcação, estilo, porém o texto, sequência de caracteres, continua imutável;
  • Editable: Interface que garante a API de trabalho com classes de texto e marcação mutáveis;
  • SpannableStringBuilder: classe concreta que permite que o texto e as marcações de estilo possam ser atualizadas, ou seja, são mutáveis.

Quando você não tem como regra de negócio o trabalho com Strings que tenham estilos aplicados a nível de caractere, nesse caso quase sempre a classe de sequência de caracteres utilizada será a String. Caso contrário há grandes chances de você trabalhar com SpannableString ou SpannableStringBuilder, essa última principalmente em quando com uma View de entrada de dados, como um EditText.

Em nosso primeiro projeto de exemplo vamos prosseguir com o uso de SpannableString. Com o projeto de chat vamos seguir com o SpannableStringBuilder.

Customizando texto com uso de Spannable

A partir daqui vamos seguir com um simples projeto de exemplo onde testaremos algumas possibilidades quando trabalhando com classes Spannable.

Note que apesar de não ter nada confirmado em documentação, digo, na documentação do Android para as classes comentadas até aqui, segundo as fontes de pesquisa que utilizei para a construção deste artigo, as classes Spannable, quando o assunto é "customização de texto", essas são mais eficientes do que a aplicação de estilo em bloco, por atributos de Views de texto.

Vamos ao projeto.

Projeto de exemplo, visão geral

O projeto aqui é bem simples. Caso queira ter acesso a ele por completo, incluindo imagens, entre no seguinte GitHub: https://github.com/viniciusthiengo/spannable-string-test.

De qualquer forma, continue acompanhando a explicação para entender como trabalhar com classes Spannable.

Abra o Android Studio e crie um novo projeto com uma "Empty Activity" e o nome "SpanableString Test".

Como layout principal, temos o /res/layout/activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
tools:context="br.com.thiengo.spannablestringtest.MainActivity">

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">

<TextView
android:id="@+id/tv_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@android:color/white" />

<EditText
android:id="@+id/et_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textMultiLine" />
</LinearLayout>
</ScrollView>

 

Mantive um EditText para que seja possível a visualização da aplicação de estilos em textos dentro dele também.

E assim temos o código inicial da MainActivity:

public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

TextView tv = (TextView) findViewById(R.id.tv_content);
EditText et = (EditText) findViewById(R.id.et_content);
}
}

 

Vamos a aplicação dos estilos. Iremos criar uma String, mas é possível utilizar a mesma já presente em um TextView ou EditText, somente aplique o cast correto (falarei mais sobre isso em seção futura).

Para ter uma lista de todas as classes que podemos utilizar como marcação em textos, entre em: android.text.style.

Método setSpan()

É exatamente com esse método que conseguimos anexar marcações de estilo ao nosso texto. Ele é declarado na Interface Spannable e tem a seguinte configuração:

public void setSpan(Object what, int start, int end, int flags)

 

  • what: objeto de marcação para adicionar algum estilo ou funcionalidade ao texto. Caso seja um objeto já sendo utilizado no conteúdo, ele será movido para a posição atual definida junto aos parâmetros start e end;
  • start: posição do primeiro caractere que deve também estar incluído na aplicação da marcação de what;
  • end: posição depois do último caractere que deve também estar incluído na aplicação da marcação de what. Note que todos os caracteres entre start e end - 1 também recebem a marcação;
  • flags: a flag / constante definida aqui vem da classe Spanned. Essa flag define como será o comportamento do texto que será adicionado posteriormente ao conteúdo que aplicamos a marcação. Por exemplo: colocando a flag Spanned.SPAN_INCLUSIVE_INCLUSIVE, qualquer texto adicionado entre, no início ou ao fim do conteúdo com a marcação persistirá com essa marcação(ões). Se fosse apenas itálico a marcação, essa persistiria no novo texto. Com a constante Spanned.SPAN_INCLUSIVE_EXCLUSIVE, textos novos que iniciam depois do último caractere com marcação, esses não teriam o estilo incluído. Para conhecer todas as flags possíveis, entre em Android Spanned.

Assim vamos a estilização de String em nosso projeto.

Estilo em texto grande

Para a criação de um "large text" nós podemos duplicar o tamanho atual da fonte, fazemos isso com a classe RelativeSizeSpan.

Na MainActivity adicione o seguinte código em destaque:

public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

TextView tv = (TextView) findViewById(R.id.tv_content);
EditText et = (EditText) findViewById(R.id.et_content);

SpannableString stringEstilizada = new SpannableString(
"Texto largo\n\n" /* index 0 - 11 */
);

/* COLOCANDO O TEXTO AINDA MAIOR (2X MAIS) */
stringEstilizada.setSpan(
new RelativeSizeSpan(2f),
0,
11,
Spanned.SPAN_INCLUSIVE_INCLUSIVE );

tv.setText( stringEstilizada );
et.setText( stringEstilizada );
}
}

 

Executando o aplicativo, temos:

SpannableString com estilo de texto grande

Note que eu, no EditText, continuei colocando um novo texto, "Teste". Devido ao uso da flag Spanned.SPAN_INCLUSIVE_INCLUSIVE o novo texto continuou com a mesma marcação de "estilo grande".

Note que o valor 2f no construtor de RelativeSizeSpan indicando a proporção a mais que queremos do texto, esse valor foi retirado da comunidade Android, logo vamos assumir que é essa a medição que simula o mesmo estilo quando em HTML, mais precisamente, simula a tag <big>.

Outro ponto importante a se notar é que como não trabalhamos a apresentação do conteúdo no EditText para ser exatamente igual a de um TextView, uma diferença pequena existirá, mas é possível deixar ambas as Views com a mesma saída.

Estilo em negrito

Para um texto em negrito utilizamos a classe StyleSpan junto a constante BOLD de Typeface.

Na MainActivity do projeto, adicione o seguinte trecho de código em destaque:

public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
...

SpannableString stringEstilizada = new SpannableString(
"Texto largo\n\n" /* index 0 - 11 */
+ "Negrito\n\n" /* index 13 - 20 */
);
...

/* COLOCANDO O TETO COMO NEGRITO */
stringEstilizada.setSpan(
new StyleSpan(Typeface.BOLD),
13,
20,
Spanned.SPAN_INCLUSIVE_INCLUSIVE );

...
}
}

 

Agora execute o aplicativo, terá:

SpannableString com estilo em negrito

As outras constantes Typeface são: ITALIC, BOLD_ITALIC e NORMAL.

Estilo lista (bullet)

Aproveitando o último trecho de texto adicionado, "Negrito", vamos colocar também nele um estilo de lista, mais precisamente, um bullet antes do nome, isso utilizando a classe BulletSpan.

Na MainActivity adicione o seguinte código em destaque:

public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
...

SpannableString stringEstilizada = new SpannableString(
"Texto largo\n\n" /* index 0 - 11 */
+ "Negrito\n\n" /* index 13 - 20 */
);
...

/* COLOCANDO UM PONTO / BULLET NA FRENTE DO TEXTO,
* COMO EM LISTAS HTML
* */
stringEstilizada.setSpan(
new BulletSpan(10),
13,
13,
Spanned.SPAN_INCLUSIVE_EXCLUSIVE );

...
}
}

 

Coloquei a flag Spanned.SPAN_INCLUSIVE_EXCLUSIVE para garantir que nenhum texto após o bullet seja travado.

Sim, isso é possível. Caso você utilize a flag Spanned.SPAN_INCLUSIVE_INCLUSIVE, por exemplo, e o próximo texto a adicionar não seja passível em receber a marcação do texto atual, nada será digitado, estará simulando um EditText travado.

Esse travamento é comum de acontecer quando utilizando a constante Spanned.SPAN_INCLUSIVE_INCLUSIVE junto a um ImageSpan que entra no local de um caractere já presente em texto.

O argumento do construtor de BulletSpan é a distância, em pixels, que haverá entre o bullet e o texto.

Executando o aplicativo, temos:

SpannableString com bullet de lista

Note que não há problemas em termos mais de uma marcação para o mesmo trecho.

Note que para adicionar o bullet eu utilizei somente uma posição em setSpan(), a 13ª. Não há problemas em fazer isso quando o estilo é na verdade a adição de um item. O bullet não existia em texto, colocamos ele.

Porém quando o estilo será aplicado a um trecho do texto existente, neste caso devemos fornecer a posição do caractere inicial e a posição do caractere posterior ao último que receberá a marcação.

Algo importante a informar é que o caractere de escape, "/", não entra na contagem de caracteres de String.

Estilo em sublinhado

O estilo sublinhado tem uma classe exclusiva, UnderlineSpan.

Na atividade principal do projeto adicione o seguinte trecho em destaque:

public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
...

SpannableString stringEstilizada = new SpannableString(
"Texto largo\n\n" /* index 0 - 11 */
+ "Negrito\n\n" /* index 13 - 20 */
+ "Sublinhado\n\n" /* index 22 - 32 */
);
...

/* COLOCANDO O TEXTO COMO SUBLINHADO */
stringEstilizada.setSpan(
new UnderlineSpan(),
22,
32,
Spanned.SPAN_INCLUSIVE_INCLUSIVE );

...
}
}

 

Executando o aplicativo, temos:

SpannableString com estilo de texto sublinhado

Estilo em itálico

O estilo em itálico é aplicado como o estilo em negrito, utilizando a classe StyleSpan, porém com a constante Typeface.ITALIC.

Na atividade principal do projeto adicione o seguinte código em destaque:

public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
...

SpannableString stringEstilizada = new SpannableString(
"Texto largo\n\n" /* index 0 - 11 */
+ "Negrito\n\n" /* index 13 - 20 */
+ "Sublinhado\n\n" /* index 22 - 32 */
+ "Itálico\n\n" /* index 34 - 41 */
);
...

/* COLOCANDO O TEXTO COMO ITÁLICO */
stringEstilizada.setSpan(
new StyleSpan(Typeface.ITALIC),
34,
41,
Spanned.SPAN_INCLUSIVE_INCLUSIVE );

...
}
}

 

Executando o aplicativo, temos:

SpannableString com estilo de texto em itálico

Estilo de linha atravessada (strikethrough)

O conhecido estilo "texto removido" de blogs, ou strikethrough, tem também uma classe exclusiva: StrikethroughSpan.

Na MainActivity do projeto, adicione o seguinte código em destaque:

public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
...

SpannableString stringEstilizada = new SpannableString(
"Texto largo\n\n" /* index 0 - 11 */
+ "Negrito\n\n" /* index 13 - 20 */
+ "Sublinhado\n\n" /* index 22 - 32 */
+ "Itálico\n\n" /* index 34 - 41 */
+ "Removido\n\n" /* index 43 - 51 */
);
...

/* COLOCANDO O TEXTO COM UM TRAÇO NO MEIO */
stringEstilizada.setSpan(
new StrikethroughSpan(),
43,
51,
Spanned.SPAN_INCLUSIVE_INCLUSIVE );

...
}
}

 

Executando o aplicativo, temos:

SpannableString com estilo de texto em linha atravessada

Colorido

Para colorir somente o texto, utilizamos a classe ForegroundColorSpan e alguma constante da classe Color.

Na MainActivity do projeto, adicione o código em destaque:

public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
...

SpannableString stringEstilizada = new SpannableString(
"Texto largo\n\n" /* index 0 - 11 */
+ "Negrito\n\n" /* index 13 - 20 */
+ "Sublinhado\n\n" /* index 22 - 32 */
+ "Itálico\n\n" /* index 34 - 41 */
+ "Removido\n\n" /* index 43 - 51 */
+ "Colorido\n\n" /* index 53 - 61 */
);
...

/* DEIXANDO O TEXTO COLORIDO (VERMELHO) */
stringEstilizada.setSpan(
new ForegroundColorSpan( Color.RED ),
53,
61,
Spanned.SPAN_INCLUSIVE_INCLUSIVE );

...
}
}

 

Executando o App, temos:

SpannableString com texto colorido em vermelho

Destacado

Para destacar o texto temos a classe BackgroundColorSpan, que como a classe ForegroundColorSpan, trabalha em conjunto com a classe Color, digo, trabalha com as constantes desta classe.

Na MainActivity do projeto de exemplo acrescente os seguintes códigos em destaque:

public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
...

SpannableString stringEstilizada = new SpannableString(
"Texto largo\n\n" /* index 0 - 11 */
+ "Negrito\n\n" /* index 13 - 20 */
+ "Sublinhado\n\n" /* index 22 - 32 */
+ "Itálico\n\n" /* index 34 - 41 */
+ "Removido\n\n" /* index 43 - 51 */
+ "Colorido\n\n" /* index 53 - 61 */
+ "Destacado\n\n" /* index 63 - 72 */
);
...

/* DESTACANDO O TEXTO EM AZUL */
stringEstilizada.setSpan(
new BackgroundColorSpan(Color.BLUE),
63,
72,
Spanned.SPAN_INCLUSIVE_INCLUSIVE );

...
}
}

 

Executando o aplicativo, temos:

SpannableString com texto destacado em azul

Estilo acima da linha base (superscript)

Com a classe SuperscriptSpan conseguimos colocar o texto, com essa marcação, acima da baseline, porém o tamanho do texto permanece o mesmo, logo, é comum utilizar essa marcação juntamente com a marcação RelativeSizeSpan. Isso caso sua meta seja ter um resultado como o da tag <sup> do HTML.

Na atividade principal do projeto adicione o seguinte código em destaque:

public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

TextView tv = (TextView) findViewById(R.id.tv_content);
EditText et = (EditText) findViewById(R.id.et_content);


SpannableString stringEstilizada = new SpannableString(
"Texto largo\n\n" /* index 0 - 11 */
+ "Negrito\n\n" /* index 13 - 20 */
+ "Sublinhado\n\n" /* index 22 - 32 */
+ "Itálico\n\n" /* index 34 - 41 */
+ "Removido\n\n" /* index 43 - 51 */
+ "Colorido\n\n" /* index 53 - 61 */
+ "Destacado\n\n" /* index 63 - 72 */
+ "ECM2\n\n" /* "2" index 77 - 78 */
);
...

/* COLOCANDO O TEXTO ACIMA DA BASELINE */
stringEstilizada.setSpan(
new SuperscriptSpan(),
77,
78,
Spanned.SPAN_INCLUSIVE_INCLUSIVE );

/* DIMINUINDO O TAMANHO DO TEXTO PARA SIMULAR O EFEITO DA TAG HTML <sup> */
stringEstilizada.setSpan(
new RelativeSizeSpan(0.5f),
77,
78,
Spanned.SPAN_INCLUSIVE_INCLUSIVE );

...
}
}

 

Veja que o estilo superscript é aplicado a somente o número "2" do texto "ECM2", pois é ele que queremos colocar a acima da baseline.

Executando o aplicativo, temos:

SpannableString com estilo de texto acima da baseline

Estilo abaixo da linha base (subscript)

Com regra de negócio similar ao da classe SuperscriptSpan, para colocar um texto abaixo da baseline e ainda simular a tag HTML <sub>, precisamos utilizar a classe SubscriptSpan junto a classe RelativeSizeSpan.

Na MainActivity do projeto, adicione os códigos em destaque:

public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
...

SpannableString stringEstilizada = new SpannableString(
"Texto largo\n\n" /* index 0 - 11 */
+ "Negrito\n\n" /* index 13 - 20 */
+ "Sublinhado\n\n" /* index 22 - 32 */
+ "Itálico\n\n" /* index 34 - 41 */
+ "Removido\n\n" /* index 43 - 51 */
+ "Colorido\n\n" /* index 53 - 61 */
+ "Destacado\n\n" /* index 63 - 72 */
+ "ECM2\n\n" /* "2" index 77 - 78 */
+ "ECM2\n\n" /* "2" index 83 - 84 */
);
...

/* COLOCANDO O TEXTO ABAIXO DA BASELINE */
stringEstilizada.setSpan(
new SubscriptSpan(),
83,
84,
Spanned.SPAN_INCLUSIVE_INCLUSIVE );

/* DIMINUINDO O TAMANHO DO TEXTO PARA SIMULAR A TAG HTML <sub> */
stringEstilizada.setSpan(
new RelativeSizeSpan(0.5f),
83,
84,
Spanned.SPAN_INCLUSIVE_INCLUSIVE );

...
}
}

 

Executando o App, temos:

SpannableString com estilo de texto abaixo da baseline

Novamente, somente o "2" é que tem aplicado a ele o estilo de subscript.

Link clicável

Com o uso da classe URLSpan ou a classe ClickableSpan nós conseguimos deixar o texto clicável, digo, esse é 50% do procedimento, os outros 50% é a adição de uma instância de MovementMethod para permitir que um TextView, ou subclasse dessa View, consiga processar o clique.

Instâncias da classe MovementMethod não devem, segundo a documentação, ser trabalhadas diretamente em nossos algoritmos. Essas serão criadas de acordo com o uso dos frameworks e APIs nativas do Android.

Mas aqui, se não ao menos solicitarmos uma instância de LinkMovementMethod, não teremos nenhum comportamento de clique nos trechos de texto com as marcações URLSpan ou ClickableSpan.

Assim, primeiro com a classe URLSpan, altere a MainActivity como o código em destaque a seguir:

public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
...

SpannableString stringEstilizada = new SpannableString(
"Texto largo\n\n" /* index 0 - 11 */
+ "Negrito\n\n" /* index 13 - 20 */
+ "Sublinhado\n\n" /* index 22 - 32 */
+ "Itálico\n\n" /* index 34 - 41 */
+ "Removido\n\n" /* index 43 - 51 */
+ "Colorido\n\n" /* index 53 - 61 */
+ "Destacado\n\n" /* index 63 - 72 */
+ "ECM2\n\n" /* "2" index 77 - 78 */
+ "ECM2\n\n" /* "2" index 83 - 84 */
+ "Url\n\n" /* index 86 - 89 */
);
...

/* PERMITINDO QUE O TEXTO ABRA UMA URL QUANDO NO CLIQUE */
stringEstilizada.setSpan(
new URLSpan("https://www.thiengo.com.br"),
86,
89,
Spanned.SPAN_INCLUSIVE_INCLUSIVE );

/* PARA QUE O TEXTO POSSA SER CLICÁVEL, TEMOS DE CONFIGURAR
* UM LinkMovementMethod
* */
tv.setMovementMethod( LinkMovementMethod.getInstance() );
et.setMovementMethod( LinkMovementMethod.getInstance() );

...
}
}

 

Executando o aplicativo e clicando em "Url" no TextView ou no EditText, temos:

SpannableString com texto em link e página Web aberta

A versão com URLSpan é um pouco limitada, pois somente podemos abrir uma URL, não conseguimos, por exemplo, abrir uma outra Activity. Para isso temos a classe ClickableSpan.

Adicione o seguinte código em destaque a atividade principal do projeto:

public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
...

SpannableString stringEstilizada = new SpannableString(
"Texto largo\n\n" /* index 0 - 11 */
+ "Negrito\n\n" /* index 13 - 20 */
+ "Sublinhado\n\n" /* index 22 - 32 */
+ "Itálico\n\n" /* index 34 - 41 */
+ "Removido\n\n" /* index 43 - 51 */
+ "Colorido\n\n" /* index 53 - 61 */
+ "Destacado\n\n" /* index 63 - 72 */
+ "ECM2\n\n" /* "2" index 77 - 78 */
+ "ECM2\n\n" /* "2" index 83 - 84 */
+ "Url\n\n" /* index 86 - 89 */
+ "Clicável\n\n" /* index 91 - 99 */
);
...

/* PERMITINDO QUE O TEXTO SEJA CLICÁVEL */
ClickableSpan clickableSpan = new ClickableSpan() {
@Override
public void onClick(View widget) {
Toast.makeText(
MainActivity.this,
"Link clicado",
Toast.LENGTH_SHORT ).show();
}
};

stringEstilizada.setSpan(
clickableSpan,
91,
99,
Spanned.SPAN_INCLUSIVE_INCLUSIVE );

/* PARA QUE O TEXTO POSSA SER CLICÁVEL, TEMOS DE CONFIGURAR
* UM LinkMovementMethod
* */
tv.setMovementMethod(LinkMovementMethod.getInstance());
et.setMovementMethod(LinkMovementMethod.getInstance());

...
}
}

 

Executando o aplicativo e clicando em "Clicável", temos:

SpannableString com estilo em link e toast acionado

Fonte customizada

Para colocarmos fonte personalizada em um texto temos que primeiro criar uma classe que herde de TypefaceSpan.

Aqui vamos criar a classe CustomTypefaceSpan. Segue:

public class CustomTypefaceSpan extends TypefaceSpan {

private final Typeface newTypeFace;

public CustomTypefaceSpan(String family, Typeface type ) {
super(family);
newTypeFace = type;
}

@Override
public void updateDrawState( TextPaint paint ) {
paint.setTypeface( newTypeFace );
}

@Override
public void updateMeasureState(TextPaint paint) {
paint.setTypeface( newTypeFace );
}
}

 

Caso queira uma classe mais robusta, digo, que mantenha o estilo negrito e itálico caso eles estejam presentes, trabalhe com a versão a seguir de CustomTypefaceSpan:

public class CustomTypefaceSpan extends TypefaceSpan {

private final Typeface newTypeFace;

public CustomTypefaceSpan(String family, Typeface type ) {
super(family);
newTypeFace = type;
}

@Override
public void updateDrawState( TextPaint paint ) {
applyCustomTypeFace( paint, newTypeFace );
}

@Override
public void updateMeasureState(TextPaint paint) {
applyCustomTypeFace( paint, newTypeFace );
}

private static void applyCustomTypeFace(Paint paint, Typeface typeface) {
int styleAnterior;
Typeface typefaceAnterior = paint.getTypeface();

if( typefaceAnterior == null ) {
styleAnterior = 0;
} else {
styleAnterior = typefaceAnterior.getStyle();
}

/* PARA VERIFICAR A COMPATIBILIDADE DE ESTILOS */
int fake = styleAnterior & ~typeface.getStyle();

/*
* VERIFICA SE A FONTE MAIS ATUAL JÁ ESTÁ DE ACORDO
* COM A ANTERIOR EM TERMOS DE "TEXTO EM NEGRITO",
* CASO NÃO, ATUALIZA.
* */
if ( (fake & Typeface.BOLD) != 0 ) {
paint.setFakeBoldText(true);
}

/*
* VERIFICA SE A FONTE MAIS ATUAL JÁ ESTÁ DE ACORDO
* COM A ANTERIOR EM TERMOS DE "TEXTO EM ITÁLICO",
* CASO NÃO, ATUALIZA.
* */
if ( (fake & Typeface.ITALIC) != 0 ) {
paint.setTextSkewX(-0.25f);
}

/* APLICA A FONTE */
paint.setTypeface( typeface );
}
}

 

Antes de prosseguir ao código da MainActivity, adicione uma fonte ao /assets folder de seu projeto. Modifique a visualização do projeto para ficar como "Project" ao invés de "Android":

Visualização em Project no Android Studio

Depois expanda /SpannableStringTest, em seguida expanda /app. Agora expanda /src e então expanda /main. Clique com o botão direito do mouse em /main, logo depois vá até "New" e então clique em "Directory". Digite assets e assim clique em "Ok".

Agora escolha um arquivo de fonte latin e no formato .ttf. Aqui vamos utilizar a fonte Pacifico, você pode acessa-la em: https://github.com/viniciusthiengo/spannable-string-test/blob/master/app/src/main/assets/Pacifico.ttf.

Estrutura de um projeto no Android Studio

Voltando a MainActivity, coloque o seguinte novo código:

public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
...

SpannableString stringEstilizada = new SpannableString(
"Texto largo\n\n" /* index 0 - 11 */
+ "Negrito\n\n" /* index 13 - 20 */
+ "Sublinhado\n\n" /* index 22 - 32 */
+ "Itálico\n\n" /* index 34 - 41 */
+ "Removido\n\n" /* index 43 - 51 */
+ "Colorido\n\n" /* index 53 - 61 */
+ "Destacado\n\n" /* index 63 - 72 */
+ "ECM2\n\n" /* "2" index 77 - 78 */
+ "ECM2\n\n" /* "2" index 83 - 84 */
+ "Url\n\n" /* index 86 - 89 */
+ "Clicável\n\n" /* index 91 - 99 */
+ "Fonte customizada\n\n" /* index 101 - 118 */
);
...

/* COLOCANDO UMA FONTE CUSTOMIZADA */
Typeface face = Typeface.createFromAsset( getAssets(), "Pacifico.ttf" );
stringEstilizada.setSpan(
new CustomTypefaceSpan("", face),
101,
118,
Spanned.SPAN_INCLUSIVE_INCLUSIVE );

...
}
}

 

Executando o aplicativo, temos:

SpannableString com fonte customizada

Imagem em texto

A classe ImageSpan, em meu ponto de vista, é o verdadeiro poder do trabalho com Span String no Android. Com ela nós conseguimos adicionar uma imagem como se fosse um caractere.

Na MainActivity, adicione o seguinte código em destaque:

public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
...

SpannableString stringEstilizada = new SpannableString(
"Texto largo\n\n" /* index 0 - 11 */
+ "Negrito\n\n" /* index 13 - 20 */
+ "Sublinhado\n\n" /* index 22 - 32 */
+ "Itálico\n\n" /* index 34 - 41 */
+ "Removido\n\n" /* index 43 - 51 */
+ "Colorido\n\n" /* index 53 - 61 */
+ "Destacado\n\n" /* index 63 - 72 */
+ "ECM2\n\n" /* "2" index 77 - 78 */
+ "ECM2\n\n" /* "2" index 83 - 84 */
+ "Url\n\n" /* index 86 - 89 */
+ "Clicável\n\n" /* index 91 - 99 */
+ "Fonte customizada\n\n" /* index 101 - 118 */
+ "Ícone \n\n" /* index 126 - 127 */
);
...

/* COLOCANDO UM ÍCONE */
Drawable img = getResources().getDrawable( R.drawable.emotion, null );
img.setBounds( 0, 0, img.getIntrinsicWidth(), img.getIntrinsicHeight() );
ImageSpan spanImg = new ImageSpan( img, ImageSpan.ALIGN_BASELINE );
stringEstilizada.setSpan(
spanImg,
126,
127,
Spannable.SPAN_INCLUSIVE_EXCLUSIVE );

...
}
}

 

A outra opção ante ao uso de ImageSpan.ALIGN_BASELINE é a constante ImageSpan.ALIGN_BOTTOM. Ao menos em meus testes eu não notei diferença na alternância de uso dessas constantes.

Como com BulletSpan, utilizei a flag Spannable.SPAN_INCLUSIVE_EXCLUSIVE, para que o texto posterior a image seja aceito, pois a marcação não estará sendo aplicada a ele. Mas para garantir que nem mesmo o texto adicionado antes da imagem seja travado, você pode seguramente utilizar a constante Spannable.SPAN_EXCLUSIVE_EXCLUSIVE.

As linhas de getDrawable() e setBounds() são boilerplate code, somente no local de getIntrinsicWidth()getIntrinsicHeight() é que você pode fornecer seus próprios tamanhos.

Executando o aplicativo, temos:

SpannableString com texto e imagem

Uma outra maneira de adicionar imagem a texto utilizando o ImageSpan, uma maneira até mais simples, é como segue:

public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
...

SpannableString stringEstilizada = new SpannableString(
"Texto largo\n\n" /* index 0 - 11 */
+ "Negrito\n\n" /* index 13 - 20 */
+ "Sublinhado\n\n" /* index 22 - 32 */
+ "Itálico\n\n" /* index 34 - 41 */
+ "Removido\n\n" /* index 43 - 51 */
+ "Colorido\n\n" /* index 53 - 61 */
+ "Destacado\n\n" /* index 63 - 72 */
+ "ECM2\n\n" /* "2" index 77 - 78 */
+ "ECM2\n\n" /* "2" index 83 - 84 */
+ "Url\n\n" /* index 86 - 89 */
+ "Clicável\n\n" /* index 91 - 99 */
+ "Fonte customizada\n\n" /* index 101 - 118 */
+ "Ícone \n\n" /* index 126 - 127 */
+ "Emotion \n\n" /* index 137 - 138 */
);
...

/* COLOCANDO UM ÍCONE, MAS COM UM BITMAP */
Bitmap emotion = BitmapFactory.decodeResource( getResources(), R.drawable.emotion );
spanImg = new ImageSpan( this, emotion, ImageSpan.ALIGN_BASELINE );
stringEstilizada.setSpan(
spanImg,
137,
138,
Spannable.SPAN_INCLUSIVE_EXCLUSIVE );

...
}
}

 

Essa versão é a que utilizaremos no projeto de chat, pois ela é mais simples por permitir o trabalho direto com Bitmap. Note que a imagem que utilizei aqui, emoticon, tem o tamanho de 14dp, tamanho padrão de fonte no Android, 14sp.

Executando o projeto, temos:

SpannableString com texto e imagem, Bitmap

Considerações importantes

Caso você esteja obtendo a String de um TextView, por exemplo, pode ser que você tenha de forçar o TextView a trabalhar com um Spannable, isso, pois o TextView somente trabalha com um tipo desse caso necessário, a princípio ele utiliza somente uma simples String.

Veja o código a seguir:

...
Spannable spannable = (Spannable) textView.getText();
...

 

Pode ser que você tenha um ClassCastException, pois "java.lang.String cannot be cast to android.text.Spannable".

Para evitar isso, uma possível solução é forçando o TextView a trabalhar com um Spannable. No XML dele acrescente o bufferType:

...
<TextView
android:id="@+id/tv_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:bufferType="spannable"
android:textColor="@android:color/white" />
...

 

Uma outra opção é via Java code, mas essa se encaixa melhor quando estamos colocando um novo Spannable no TextView. Segue:

...
tv.setText( spannable, TextView.BufferType.SPANNABLE );
...

 

Note que as marcações Span não foram feitas para serem persistidas, ou seja, caso você salve o CharSequence, com marcações, em algumas das persistências possíveis tanto no Android quanto fora dele, nesse caso as marcações serão perdidas, isso se você for obrigado a salvar como String, caso consiga salvar como Object, as marcações persistem.

Por que como String as marcações serão perdidas?

Porque a invocação implícita ou explícita de toString() remove todas as marcações. Lembre que String somente representa um sequência de caracteres não mutáveis, não há suporte para marcações.

Na MainActivity de seu projeto de exemplo, adicione o seguinte código ao final do método onCreate():

...
/* QUANDO O TEXTO É PERSISTIDO COMO STRING, TODO O
* ESTILO É PERDIDO
* */
SpannableStringBuilder ss = new SpannableStringBuilder( stringEstilizada );
SharedPreferences sp = getSharedPreferences("pref", MODE_PRIVATE);
sp.edit().putString( "span", ss.toString() ).apply();
et.setText( sp.getString("span", "") );
...

 

Executando o projeto e visualizando o conteúdo do EditText, temos:

SpannableString sem efeito devido a conteúdo persistido como String

Outra particularidade é quando trabalhando com EditText, ou alguma subclasse dessa View, e então uma imagem é adicionada ao texto, digo, um ImageSpan. Caso o EditText comporte múltiplas linhas, é possível que essa imagem seja perdida assim que houver a quebra de linha.

Uma "solução parcial" para esse bug é a adição de um "\r" (retorno de carro) exatamente na posição onde o ImageSpan será colocado. Veja o código a seguir:

...
SpannableStringBuilder ssb = new SpannableStringBuilder("Imagem ");
Bitmap emotion = BitmapFactory.decodeResource( getResources(), R.drawable.emotion );
spanImg = new ImageSpan( this, emotion, ImageSpan.ALIGN_BASELINE );
ssb.append("\r"); /* POSIÇÃO ONDE SERÁ ADICIONADA A IMAGEM */
ssb.setSpan(
spanImg,
ssb.length() - 1,
ssb.length(),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE );
...

 

Assim, esse bug de quebra de linha é parcialmente resolvido.

Com isso podemos ir ao projeto de chat, onde precisaremos utilizar strings Spannable em contexto real.

Projeto de exemplo, Android

Nosso projeto Android que simula um contexto real é uma página de chat onde estará havendo uma conversa. Já temos o trivial sendo realizado, enviar mensagens para a lista. Porém apesar de termos a tela de emoticons, eles ainda não estão com a funcionalidade de serem incluídos em texto.

Nossa meta aqui é permitir que o usuário escolha o emoticon e este apareça no EditText de mensagem sendo montada e também no TextView que deve recebe-la. O usuário deve conseguir utilizar shortcuts para alguns emoticons e também deve conseguir coloca-los em qualquer parte do texto.

Antes de prosseguir, saiba que para ter acesso completo ao projeto, incluindo imagens, você pode estar entrando no GitHub dele em: https://github.com/viniciusthiengo/chat-page-span. Mesmo assim não deixe de seguir com o artigo para as explicações.

Os emoticons foram baixados do site: http://www.flaticon.com/packs/emoticon-set. Todos gratuitos e sem necessidade de cadastro.

Para colocar os emoticons nos tamanhos necessários, utilizei o Android Assets Studio em: https://romannurik.github.io/AndroidAssetStudio/index.html. Mais precisamente, a opção "Generic icon generator".

Assim... vamos iniciar apresentando todo o código que já temos antes da aplicação da funcionalidade de colocar emoticons em texto, para depois irmos a esse algoritmo final de emoticons.

Abra o Android Studio, crie um novo projeto, um com uma "Empty Activity" e com o nome "Chat Page". Ao final dessa primeira parte de implementação teremos o seguinte aplicativo:

Aplicativo Android de chat

E em estrutura de projeto teremos:

Estrutura do projeto Android de chat

Configurações Gradle

Vamos começar com o Gradle Project Level, ou build.gradle (Project: ChatPage):

buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.3.1'
}
}

allprojects {
repositories {
jcenter()
}
}

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

 

Como deve ter notado, ele não passou por nenhuma configuração extra. Note que tanto para o Gradle Project Level quanto para o Gradle App Level, caso você esteja com uma versão mais atual, persista utilizando ela, pois o projeto de exemplo e o assunto comentado em artigo não vão ser influenciados pelo uso de uma versão mais atual de Gradle.

Abaixo o código do Gradle App Level, ou build.gradle (Module: app):

apply plugin: 'com.android.application'

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

dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
compile 'com.android.support:appcompat-v7:25.3.1'
testCompile 'junit:junit:4.12'

/* RECYCLERVIEW */
compile 'com.android.support:design:25.3.1'

/* MATERIAL DIALOG */
compile 'me.drakeet.materialdialog:library:1.3.1'

/* FLEXBOXLAYOUT */
compile 'com.google.android:flexbox:0.2.6'
}

 

No App Level adicionamos algumas libraries para que seja possível simular o domínio do problema em trabalho, pois nenhuma das libraries adicionadas têm influência no uso dos Spannable.

Configurações AndroidManifest

No AndroidManifest.xml apenas adicionamos uma trava na orientação de tela para ficar somente em portrait e também um tema para a atividade principal. O tema estaremos apresentando na seção de estilo.

Segue código do manifest:

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

<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=".MainActivity"
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

Caso tenha seguido as instruções iniciais para este projeto, criar uma "Empty Activity", verá que aqui, nos arquivos de estilo, adicionamos uma série de novos códigos.

Vamos iniciar pelo arquivo de cores, /res/values/colors.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#9C27B0</color>
<color name="colorPrimaryDark">#7B1FA2</color>
<color name="colorPrimaryTransparent">#447B1FA2</color>
<color name="colorPrimaryLight">#ba8dce</color>
<color name="colorAccent">@color/colorPrimaryDark</color>
<color name="colorMessageBackground">#e2ffc8</color>
</resources>

 

Agora o arquivo de String, o mais simples, segue /res/values/strings.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Chat Page</string>
</resources>

 

E por fim o arquivo de definição de estilo em tema, segue /res/values/styles.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="AppTheme" parent="Theme.AppCompat">
<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" />
</resources>

Classe adaptadora

Para trabalho com as mensagens do chat, vamos utilizar um RecyclerView com orientação vertical e invertida, de baixo para cima.

Para isso vamos ter de trabalhar com um adapter. Segue o XML de layout de itens do RecyclerView, /res/layout/item_message.xml:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingRight="60dp"
tools:context="br.com.thiengo.chatpage.MainActivity">

<TextView
android:id="@+id/tv_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:layout_margin="8dp"
android:background="@drawable/rounded_corner"
android:elevation="4dp"
android:gravity="left"
android:padding="12dp"
android:textColor="@android:color/black"
android:bufferType="spannable" />
</RelativeLayout>

 

A seguir o diagrama do layout anterior:

Diagrama do layout item_message.xml

Assim o código Java da classe MessagesAdapter:

public class MessagesAdapter extends RecyclerView.Adapter<MessagesAdapter.ViewHolder> {
private Context context;
private LinkedList<SpannableStringBuilder> messages;

MessagesAdapter(Context context, LinkedList<SpannableStringBuilder> messages){
this.context = context;
this.messages = messages;
}

@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater
.from( context )
.inflate(R.layout.item_message, parent, false);

return new ViewHolder( view );
}

@Override
public void onBindViewHolder(ViewHolder holder, int position) {
holder.tvMessage.setText( messages.get( position ) );
}

@Override
public int getItemCount() {
return messages.size();
}

class ViewHolder extends RecyclerView.ViewHolder {
TextView tvMessage;

private ViewHolder(View view) {
super(view);
tvMessage = (TextView) view.findViewById(R.id.tv_message);
}
}
}

 

Código simples, apenas os scripts obrigatórios para se trabalhar com um RecyclerView adapter, scripts de atribuição de valor, sem lógica condicional.

Atividade principal

Para a MainActivity, vamos iniciar apresentando os códigos XML.

Segue XML /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="br.com.thiengo.chatpage.MainActivity"
tools:showIn="@layout/activity_main">

<android.support.v7.widget.RecyclerView
android:id="@+id/rv_chat"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_above="@+id/vv_line"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true" />

<View
android:id="@+id/vv_line"
android:layout_width="match_parent"
android:layout_height="0.8dp"
android:layout_above="@+id/ll_container"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:background="@color/colorPrimaryDark" />

<LinearLayout
android:id="@+id/ll_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:background="@color/colorPrimaryTransparent"
android:orientation="horizontal"
android:padding="8dp">

<ImageButton
android:layout_width="36dp"
android:layout_height="36dp"
android:background="@null"
android:contentDescription="Escolher emoticon"
android:onClick="chooseEmoticon"
android:scaleType="center"
android:src="@drawable/ic_emoticon" />

<EditText
android:id="@+id/et_message"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:layout_marginStart="8dp"
android:layout_weight="1"
android:background="@drawable/et_background"
android:inputType="textMultiLine"
android:paddingBottom="6dp"
android:paddingEnd="8dp"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:paddingStart="8dp"
android:paddingTop="6dp"
android:textColor="@android:color/black"
android:bufferType="spannable" />

<ImageButton
android:layout_width="36dp"
android:layout_height="36dp"
android:background="@null"
android:contentDescription="Enviar mensagem"
android:onClick="newMessage"
android:scaleType="center"
android:src="@drawable/ic_send" />
</LinearLayout>
</RelativeLayout>

 

Note que no EditText, para evitar qualquer tipo de problema em versões do Android onde não houve testes, nessa View já adicionei o atributo android:bufferType="spannable".

A seguir o diagrama do layout anterior:

Diagrama do layout content_main.xml

Agora o arquivo XML que referencia o 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"
tools:context="br.com.thiengo.chatpage.MainActivity">

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

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

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

<include layout="@layout/content_main" />

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

 

Assim o diagrama de activity_maim.xml:

Diagrama do layout activity_maim.xml

Podemos prosseguir com o código Java inicial da MainActivity:

public class MainActivity extends AppCompatActivity {

private LinkedList<SpannableStringBuilder> messages;
private RecyclerView rvMessages;
private MessagesAdapter adapter;
private EditText etMessage;
private MaterialDialog materialDialog;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
toolbar.setSubtitle("Thiengo [Calopsita]... em conversa");

etMessage = (EditText) findViewById(R.id.et_message);

messages = new LinkedList<>();

initRecycler();
initDialog();
}

private void initRecycler(){
rvMessages = (RecyclerView) findViewById(R.id.rv_chat);
rvMessages.setHasFixedSize(true);

LinearLayoutManager layoutManager = new LinearLayoutManager(this);
layoutManager.setStackFromEnd( true );
rvMessages.setLayoutManager( layoutManager );

adapter = new MessagesAdapter( this, messages );
rvMessages.setAdapter( adapter );
}

private void initDialog(){
LayoutInflater inflater = LayoutInflater.from(this);
FlexboxLayout fl = (FlexboxLayout) inflater.inflate(R.layout.emoticons, null);
materialDialog = new MaterialDialog(this);

materialDialog.setView(fl);
materialDialog.setCanceledOnTouchOutside(true);
}

public void newMessage( View view ){
/* PADRÃO CLÁUSULA DE GUARDA PARA EVITAR MENSAGENS
* VAZIAS
* */
if( etMessage.getText().length() == 0 ){
return;
}

SpannableStringBuilder message = (SpannableStringBuilder) etMessage.getText();

etMessage.setText("");
messages.add( message );
adapter.notifyDataSetChanged();
rvMessages.getLayoutManager().scrollToPosition( messages.size() - 1 );
}

public void chooseEmoticon( View view ){
materialDialog.show();
}

public void chosenEmoticon(View view){
/* TODO */
}
}

 

Note que já estamos trabalhando com o tipo SpannableStringBuilder, tanto na lista de mensagens como na recuperação de dados do EditText.

Os métodos newMessage() e chooseEmoticon() você já deve ter percebido que estão sendo referenciados pelos ImageButton de nosso layout content_main.xml.

O que você não deve estar entendendo é o método chosenEmoticon(). Este está sendo referenciado no layout carregado em nosso MaterialDialog, o layout /res/layout/emoticons.xml:

<?xml version="1.0" encoding="utf-8"?>
<com.google.android.flexbox.FlexboxLayout 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="wrap_content"
android:layout_height="wrap_content"
android:paddingLeft="20dp"
android:paddingRight="20dp"
android:paddingTop="20dp"
app:flexWrap="wrap"
app:justifyContent="space_between"
tools:context="br.com.thiengo.chatpage.MainActivity">

<ImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:contentDescription="Emoticon Alien"
android:onClick="chosenEmoticon"
android:scaleType="center"
android:src="@drawable/ic_emoticon_alien" />

<ImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:contentDescription="Emoticon Confuso"
android:onClick="chosenEmoticon"
android:scaleType="center"
android:src="@drawable/ic_emoticon_confused" />

<ImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:contentDescription="Emoticon Feliz"
android:onClick="chosenEmoticon"
android:scaleType="center"
android:src="@drawable/ic_emoticon_happy" />

<ImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:contentDescription="Emoticon do Harry Potter"
android:onClick="chosenEmoticon"
android:scaleType="center"
android:src="@drawable/ic_emoticon_harry_potter" />

<ImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:contentDescription="Emoticon Apaixonado"
android:onClick="chosenEmoticon"
android:scaleType="center"
android:src="@drawable/ic_emoticon_in_love"
app:layout_wrapBefore="true" />

<ImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:contentDescription="Emoticon Sorrindo"
android:onClick="chosenEmoticon"
android:scaleType="center"
android:src="@drawable/ic_emoticon_laughing" />

<ImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:contentDescription="Emoticon Pessoas"
android:onClick="chosenEmoticon"
android:scaleType="center"
android:src="@drawable/ic_emoticon_people" />

<ImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:contentDescription="Emoticon Aliviado"
android:onClick="chosenEmoticon"
android:scaleType="center"
android:src="@drawable/ic_emoticon_relieved" />

<ImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:contentDescription="Emoticon Rico"
android:onClick="chosenEmoticon"
android:scaleType="center"
android:src="@drawable/ic_emoticon_rich"
app:layout_wrapBefore="true" />

<ImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:contentDescription="Emoticon Triste"
android:onClick="chosenEmoticon"
android:scaleType="center"
android:src="@drawable/ic_emoticon_sad" />

<ImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:contentDescription="Emoticon Muito Triste"
android:onClick="chosenEmoticon"
android:scaleType="center"
android:src="@drawable/ic_emoticon_sad_1" />

<ImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:contentDescription="Emoticon Extramente Triste"
android:onClick="chosenEmoticon"
android:scaleType="center"
android:src="@drawable/ic_emoticon_sad_2" />

<ImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:contentDescription="Emoticon Doente"
android:onClick="chosenEmoticon"
android:scaleType="center"
android:src="@drawable/ic_emoticon_sick"
app:layout_wrapBefore="true" />

<ImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:contentDescription="Emoticon Rindo"
android:onClick="chosenEmoticon"
android:scaleType="center"
android:src="@drawable/ic_emoticon_smile" />

<ImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:contentDescription="Emoticon Pensando"
android:onClick="chosenEmoticon"
android:scaleType="center"
android:src="@drawable/ic_emoticon_thinking" />

<ImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:contentDescription="Emoticon Dando Uma Piscadela"
android:onClick="chosenEmoticon"
android:scaleType="center"
android:src="@drawable/ic_emoticon_wink" />
</com.google.android.flexbox.FlexboxLayout>

 

Aqui estamos fazendo o uso do FlexboxLayout e também forçando um total de quatro ImageView por linha nas quatro linhas, 16 emoticons, isso devido ao uso do atributo app:layout_wrapBefore="true".

A seguir o diagrama do layout anterior:

Diagrama do layout emoticons.xml

Executando o aplicativo e clicando no emoticon ao lado do EditText, temos:

Dialog, dos emotions, aberto

Assim podemos prosseguir com os algoritmos que vão permitir adicionar os emoticons as mensagens.

Implementação do código que permite customização de String a nível de caractere

Desde a parte inicial do projeto de chat já estamos trabalhando com o tipo SpannableStringBuilder, logo, podemos ir direto aos algoritmos que permitem a inclusão dos emoticons as mensagens.

Somente fique ciente que para os scripts a partir desta seção funcionarem, deveríamos trabalhar com SpannableStringBuilder ao invés de SpannableString, isso para não termos problemas com, por exemplo, o posicionamento do cursor no EditText, pois antes da mensagem ser enviada, temos de apresentar ela corretamente no EditText, incluindo o posicionamento dos emoticons.

Lembrando de nossas metas:

  • Permitir que os emoticons sejam colocados em texto, tanto no TextView de item do RecyclerView quanto no EditText de criação de mensagem. Que eles possam ser colocados em qualquer posição da mensagem;
  • No EditText teremos de trabalhar também com padrão em texto, ou seja, dependendo do conjunto de caracteres digitados um emoticon deve ser colocado no local.

Permitindo que o emoticon seja colocado em qualquer parte da mensagem

Nosso primeiro passo aqui é criar um método que vai permitir que possamos pegar uma parte específica da mensagem no EditText.

Isso, pois seguindo nossa lógica de negócio, um emoticon poderá ser adicionado em qualquer lugar da mensagem, exatamente onde se encontra o cursor. Assim vamos precisar da metade antes do cursor e da metade depois do cursor.

Dessa forma será possível adicionar o emoticon ao final da primeira metade e depois juntarmos as duas metades como conteúdo do EditText.

Na MainActivity, adicione o seguinte novo método:

...
private SpannableStringBuilder getSubSetMessage( int start, int end ){
SpannableStringBuilder m = (SpannableStringBuilder) etMessage.getText();
m = (SpannableStringBuilder) m.subSequence( start, end );
return m;
}
...

 

O método getSubSetMessage() não indica nada de "trabalho somente com metade do conteúdo original", mas ele será utilizado somente para isso e, acredite, mesmo com o cursor estando na posição zero ou no final da mensagem, essa estratégia de quebrar a mensagem em duas funciona sem problemas.

Agora vamos as atualizações do método chosenEmoticon(). Primeiro devemos acessar as duas metades da mensagem. Segue:

...
public void chosenEmoticon(View view){
SpannableStringBuilder message = (SpannableStringBuilder) etMessage.getText();

/* OBTENDO O TEXTO ATUAL EM DUAS PARTES, A PRIMEIRA
* É ANTES DO CURSOR, A SEGUNDA É DEPOIS DELE
* (INCLUINDO A POSIÇÃO DELE), ASSIM SERÁ POSSÍVEL COLOCAR O
* EMOTICON EM QUALQUER PARTE DA STRING E NÃO SOMENTE
* NO FINAL
* */
SpannableStringBuilder m1 = getSubSetMessage( 0, etMessage.getSelectionStart() );
SpannableStringBuilder m2 = getSubSetMessage( etMessage.getSelectionStart(), message.length() );
}
...

 

Já adicionamos também a variável message com referência ao objeto SpannableStringBuilder do EditText, pois estaremos colocando a mensagem final, com o emoticon, nesse mesmo objeto, sem precisar criar um novo.

O método getSelectionStart() de EditText é uma maneira de obtermos a posição atual do cursor.

Note que o método subSequence() de getSubSetMessage() trabalha da seguinte forma: retorna a sub-sequência de caracteres do conteúdo atual, porém retorna de start até end - 1. Logo, o nosso trabalho acima com 0etMessage.getSelectionStart()message.length() está correto.

Agora precisamos acessar o Bitmap do ImageView clicado e então atualizar o tamanho dele para não extrapolar o tamanho da fonte do texto em mensagem. Lembrando que o método chosenEmoticon() é referenciado somente por ImageView, logo, a View que é parâmetro de entrada na verdade é um ImageView.

Segue novos trechos em chosenEmoticon():

...
public void chosenEmoticon(View view){
SpannableStringBuilder message = (SpannableStringBuilder) etMessage.getText();

/* OBTENDO O TEXTO ATUAL EM DUAS PARTES, A PRIMEIRA
* É ANTES DO CURSOR, A SEGUNDA É DEPOIS DELE
* (INCLUINDO A POSIÇÃO DELE), ASSIM SERÁ POSSÍVEL COLOCAR O
* EMOTICON EM QUALQUER PARTE DA STRING E NÃO SOMENTE
* NO FINAL
* */
SpannableStringBuilder m1 = getSubSetMessage( 0, etMessage.getSelectionStart() );
SpannableStringBuilder m2 = getSubSetMessage( etMessage.getSelectionStart(), message.length() );

ImageView iv = (ImageView) view;
Bitmap bitmap = ((BitmapDrawable) iv.getDrawable()).getBitmap();

/* DIMINUINDO O TAMANHO DO ÍCONE PARA COLOCA-LO NA
* MENSAGEM
* */
bitmap = Bitmap.createScaledBitmap(
bitmap,
getDpToPx( 34 ),
getDpToPx( 34 ),
false );
}
...

 

A imagem original de emoticon tem o tamanho de 48dp, no texto precisamos dela em 34dp. Para isso utilizamos também o método getDpToPx(). Adicione-o também a MainActivity:

...
public static int getDpToPx(int pixels ){
return (int) (pixels * Resources.getSystem().getDisplayMetrics().density);
}
...

 

O que nos resta agora é criar um ImageSpan com o novo Bitmap, adiciona-lo ao fim da primeira metade da mensagem, que já temos em m1, e por fim adicionar as duas metades a variável message.

Segue código de chosenEmoticon() atualizado:

...
public void chosenEmoticon(View view){
SpannableStringBuilder message = (SpannableStringBuilder) etMessage.getText();

/* OBTENDO O TEXTO ATUAL EM DUAS PARTES, A PRIMEIRA
* É ANTES DO CURSOR, A SEGUNDA É DEPOIS DELE
* (INCLUINDO A POSIÇÃO DELE), ASSIM SERÁ POSSÍVEL COLOCAR O
* EMOTICON EM QUALQUER PARTE DA STRING E NÃO SOMENTE
* NO FINAL
* */
SpannableStringBuilder m1 = getSubSetMessage( 0, etMessage.getSelectionStart() );
SpannableStringBuilder m2 = getSubSetMessage( etMessage.getSelectionStart(), message.length() );

ImageView iv = (ImageView) view;
Bitmap bitmap = ((BitmapDrawable) iv.getDrawable()).getBitmap();

/* DIMINUINDO O TAMANHO DO ÍCONE PARA COLOCA-LO NA
* MENSAGEM
* */
bitmap = Bitmap.createScaledBitmap(
bitmap,
getDpToPx( 34 ),
getDpToPx( 34 ),
false );

ImageSpan is = new ImageSpan( this, bitmap, ImageSpan.ALIGN_BASELINE );

/* EXATAMENTE NO FINAL DA PRIMEIRA METADE DO TEXTO É
* QUE VAMOS ADICIONAR O EMOTICON, DEPOIS DE
* ADICIONARMOS UM RETORNO DE CARRO, O QUE VAI SER
* SUBSTITUÍDO PELO EMOTICON
* */
m1.append("\r");
m1.setSpan( is, m1.length() - 1, m1.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE );

message.clear();
message.append( m1 );
message.append( m2 );
}
...

 

Veja que adicionei um retorno de carro com o script m1.append("\r"), isso, pois o emoticon entrará no lugar dele e também porque essa é uma solução parcial para o problema de "remoção de ImageSpan em quebra de linhas no EditText".

Já lhe adianto que há uma falha em nosso algoritmo, digo, está faltando algo. Caso o usuário insira o emoticon fora do fim da mensagem, devido a nós estarmos utilizando o mesmo objeto SpannableStringBuilder e junto a isso a invocação do método clear(), com esse esquema o cursor do EditText não estará no local correto.

Assim vamos adicionar um script a chosenEmoticon() para corrigir esse problema:

...
public void chosenEmoticon(View view){
SpannableStringBuilder message = (SpannableStringBuilder) etMessage.getText();

/* OBTENDO O TEXTO ATUAL EM DUAS PARTES, A PRIMEIRA
* É ANTES DO CURSOR, A SEGUNDA É DEPOIS DELE
* (INCLUINDO A POSIÇÃO DELE), ASSIM SERÁ POSSÍVEL COLOCAR O
* EMOTICON EM QUALQUER PARTE DA STRING E NÃO SOMENTE
* NO FINAL
* */
SpannableStringBuilder m1 = getSubSetMessage( 0, etMessage.getSelectionStart() );
SpannableStringBuilder m2 = getSubSetMessage( etMessage.getSelectionStart(), message.length() );

ImageView iv = (ImageView) view;
Bitmap bitmap = ((BitmapDrawable) iv.getDrawable()).getBitmap();

/* DIMINUINDO O TAMANHO DO ÍCONE PARA COLOCA-LO NA
* MENSAGEM
* */
bitmap = Bitmap.createScaledBitmap(
bitmap,
getDpToPx( 34 ),
getDpToPx( 34 ),
false );

ImageSpan is = new ImageSpan( this, bitmap, ImageSpan.ALIGN_BASELINE );

/* EXATAMENTE NO FINAL DA PRIMEIRA METADE DO TEXTO É
* QUE VAMOS ADICIONAR O EMOTICON, DEPOIS DE
* ADICIONARMOS UM ESPAÇÕES EM BRANCO, O QUE VAI SER
* SUBSTITUÍDO PELO EMOTICON
* */
m1.append("\r");
m1.setSpan( is, m1.length() - 1, m1.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE );

message.clear();
message.append( m1 );
message.append( m2 );

/* PARA COLOCAR O CURSOR EXATAMENTE ONDE ESTAVA
* ANTES DA ADIÇÃO DO ÍCONE
* */
etMessage.setSelection( m1.length() );

materialDialog.dismiss();
}
...

 

Veja que já adicionei o dimiss() do materialDialog, isso para podermos fechar o dialog assim que o usuário escolher um emoticon.

Para colocar o cursor logo depois do emoticon adicionado, nós utilizamos o tamanho total de m1, pois essa metade é que tem a posição de onde estava o cursor e, obviamente, o emoticon.

Executando o aplicativo, adicionando uma mensagem, voltando ao meio dela e colocando um emoticon, temos:

Aplicativo Android de chat em funcionamento com emotions

Clicando em enviar, temos:

Aplicativo Android de chat em funcionamento com emotions

Assim podemos ir a parte final para atender ao nosso domínio do problema por completo. Próxima meta: inserir emoticons de acordo com o padrão em texto.

Trabalhando com o reconhecimento de padrão em texto para a colocação de emoticon

Para trabalhar com reconhecimento de padrões em texto nós temos de colocar um listener de mudança de conteúdo no EditText.

Para isso, na MainActivity, adicione os seguintes trechos de código em destaque:

public class MainActivity extends AppCompatActivity implements TextWatcher {
...

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
toolbar.setSubtitle("Thiengo [Calopsita]... em conversa");

etMessage = (EditText) findViewById(R.id.et_message);
etMessage.addTextChangedListener(this);

messages = new LinkedList<>();

initRecycler();
initDialog();
}
...

@Override
public void onTextChanged(CharSequence charSequence, int start, int before, int count) {}
@Override
public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) {}
@Override
public void afterTextChanged(Editable editable) {}
}

 

Com a interface TextWatcher, somente trabalharemos o método onTextChanged().

Aqui vamos verificar a ocorrência de dois padrões: :) que indica um emoticon sorridente; :( que indica um emoticon triste. Você, posteriormente, poderá adicionar ainda mais padrões.

No método onTextChanged() temos de iniciar verificando se há caracteres no EditText. Logo depois acessar a posição do último caractere antes do cursor e assim verificar se os caracteres dos padrões em analise, se eles estão presentes:

...
public void onTextChanged(CharSequence charSequence, int start, int before, int count) {
if( charSequence.length() > 1 ){
int lastPos = etMessage.getSelectionStart() - 1;

/* PADRÃO CLÁUSULA DE GUARDA PARA NÃO TER PROCESAMENTO
* QUANDO AS CONDIÇÕES MÍNIMAS NÃO FOREM ATENDIDAS
* */
if( lastPos < 0
|| charSequence.charAt(lastPos - 1) != ':'
|| (charSequence.charAt(lastPos) != ')'
&& charSequence.charAt(lastPos) != '(') ){
return;
}
}
}
...

 

Caso não conheça o padrão Cláusula de Guarda, ao final deste artigo entre no seguinte: Padrão de Projeto: Cláusula de Guarda.

Agora temos de escolher o emoticon correto de acordo com o padrão encontrado na mensagem. Adicione em onTextChanged() o código em destaque:

public void onTextChanged(CharSequence charSequence, int start, int before, int count) {
if( charSequence.length() > 1 ){
int lastPos = etMessage.getSelectionStart() - 1;
int icon;

/* PADRÃO CLÁUSULA DE GUARDA PARA NÃO TER PROCESAMENTO
* QUANDO AS CONDIÇÕES MÍNIMAS NÃO FOREM ATENDIDAS
* */
if( lastPos < 0
|| charSequence.charAt(lastPos - 1) != ':'
|| (charSequence.charAt(lastPos) != ')'
&& charSequence.charAt(lastPos) != '(') ){
return;
}

if( charSequence.charAt(lastPos) == ')' ){
icon = R.drawable.ic_emoticon_laughing;
}
else {
icon = R.drawable.ic_emoticon_sad;
}
}
}

 

Agora vamos ter de remover o padrão encontrado em mensagem, remove-lo do texto, pois um emoticon entrará no lugar dele. Para isso utilizaremos a mesma estratégia de "quebrar a mensagem em duas metades e depois uni-las novamente".

Adicione o código em destaque:

public void onTextChanged(CharSequence charSequence, int start, int before, int count) {
if( charSequence.length() > 1 ){
int lastPos = etMessage.getSelectionStart() - 1;
int icon;

/* PADRÃO CLÁUSULA DE GUARDA PARA NÃO TER PROCESAMENTO
* QUANDO AS CONDIÇÕES MÍNIMAS NÃO FOREM ATENDIDAS
* */
if( lastPos < 0
|| charSequence.charAt(lastPos - 1) != ':'
|| (charSequence.charAt(lastPos) != ')'
&& charSequence.charAt(lastPos) != '(') ){
return;
}

if( charSequence.charAt(lastPos) == ')' ){
icon = R.drawable.ic_emoticon_laughing;
}
else {
icon = R.drawable.ic_emoticon_sad;
}

SpannableStringBuilder message = (SpannableStringBuilder) etMessage.getText();

/* REMOVENDO O PADRÃO ":)" OU ":(" DA STRING
* PARA COLOCAR O EMOTICON
* */
SpannableStringBuilder m1 = getSubSetMessage( 0, lastPos - 1 );
SpannableStringBuilder m2 = getSubSetMessage( lastPos + 1, message.length() );

message.clear();
message.append( m1 );
message.append( m2 );

/* PARA COLOCAR O CURSOR EXATAMENTE ONDE ESTAVA
* ANTES DA ADIÇÃO DO ÍCONE
* */
etMessage.setSelection( m1.length() );
}
}

 

Agora está faltando o uso do ícone definido em icon. Para isso vamos reaproveitar o método chosenEmoticon() trabalhado na seção anterior.

O que temos de fazer é criar um ImageView, colocar o valor de icon nele e então invocar chosenEmoticon() com esse ImageView como argumento. Tendo em mente que no código anterior já removemos o padrão em texto e também já colocamos o cursor no local correto.

Adicione ao método onTextChanged() as linhas em destaque:

@Override
public void onTextChanged(CharSequence charSequence, int start, int before, int count) {
if( charSequence.length() > 1 ){
ImageView iv = new ImageView(this);
int lastPos = etMessage.getSelectionStart() - 1;
int icon;

/* PADRÃO CLÁUSULA DE GUARDA PARA NÃO TER PROCESAMENTO
* QUANDO AS CONDIÇÕES MÍNIMAS NÃO FOREM ATENDIDAS
* */
if( lastPos < 0
|| charSequence.charAt(lastPos - 1) != ':'
|| (charSequence.charAt(lastPos) != ')'
&& charSequence.charAt(lastPos) != '(') ){
return;
}

if( charSequence.charAt(lastPos) == ')' ){
icon = R.drawable.ic_emoticon_laughing;
}
else {
icon = R.drawable.ic_emoticon_sad;
}

SpannableStringBuilder message = (SpannableStringBuilder) etMessage.getText();

/* REMOVENDO O PADRÃO ":)" OU ":(" DA STRING
* PARA COLOCAR O EMOTICON
* */
SpannableStringBuilder m1 = getSubSetMessage( 0, lastPos - 1 );
SpannableStringBuilder m2 = getSubSetMessage( lastPos + 1, message.length() );

message.clear();
message.append( m1 );
message.append( m2 );

/* PARA COLOCAR O CURSOR EXATAMENTE ONDE ESTAVA
* ANTES DA ADIÇÃO DO ÍCONE
* */
etMessage.setSelection( m1.length() );

iv.setImageResource( icon );
chosenEmoticon( iv );
}
}

 

Antes de prosseguir, saiba que se materialDialog.dismiss() for invocado sem ele ter sido apresentado, show(), uma Exception será gerada. Para isso, adicione à MainActivity os seguintes códigos em destaque:

public class MainActivity extends AppCompatActivity implements TextWatcher {
...
private boolean isDialogActivated = false;
...

public void chooseEmoticon( View view ){
materialDialog.show();
isDialogActivated = true;
}

public void chosenEmoticon(View view){
...

/* DEVIDO A ADIÇÃO DO ÍCONE TAMBÉM VIA PADRÃO EM TEXTO,
* ONDE NÃO HÁ ABERTURA DE DIALOG, DEVEMOS UTILIZAR UM
* FLAG PARA SABER SE O DIALOG ESTÁ ABERTA PARA ENTÃO
* INVOCAR O dismiss(), CASO CONTRÁRIO HAVERÁ UMA EXCEPTION
* */
if( isDialogActivated ){
materialDialog.dismiss();
isDialogActivated = false;
}
}
...
}

 

Seguramente podemos executar o aplicativo e entrar com uma mensagem de teste que vai incluir algum dos padrões de emoticon em texto:

Aplicativo Android de chat em funcionamento com emotions

Enviando a mensagem temos:

Aplicativo Android de chat em funcionamento com emotions

Com isso finalizamos a apresentação do trabalho de Span em String em um contexto real. Assim você já sabe como trabalhar com as APIs de Span oferecidas pelo Android.

Não esqueça de comentar o que achou e de se inscrever na 📫 lista de e-mails do Blog para receber os conteúdos em primeira mão.

Se inscreva também no canal no YouTube em: Canal Thiengo [Calopsita].

Vídeo com a implementação dos projetos

A seguir o vídeo com a implementação dos projetos deste artigo:

Para acesso aos conteúdos completos dos projetos, entre nos seguintes GitHub:

Conclusão

A customização de texto em nível de bloco pode ser útil se esse for o objetivo com o texto atual, mas caso apenas alguns trechos sejam necessários com um estilo distinto, até mesmo a aplicação de conteúdos extra texto (bullets e imagens), trabalhar com objetos Span pode ser a melhor solução no desenvolvimento Android.

Em alguns casos somente conseguiremos colocar um estilo distinto em textos se trabalharmos com String Spannable. Um exemplo simples é a necessidade de customização dos textos de itens de um NavigationView, não há TextView ou EditText ali, terá de ser direto na String.

Não se preocupe quanto a API mínima do Android, das que estão ainda em mercado, String Spannable funciona para todas.

Logo, ante o uso imediato de soluções como Html.fromHtml(), veja se uma Spannable não se encaixa como uma melhor solução, até porque o método fromHtml() está depreciado, ao menos o mais comumente utilizado, com apenas uma String HTML como argumento.

Fique atento quanto as limitações, principalmente se você trabalha em seu aplicativo a persistência de String. Isso, pois a marcação / estilo é perdido.

Não esqueça de comentar logo abaixo o que achou do conteúdo de Span, suas dúvidas e sugestões. Não se esqueça também de se inscrever na 📩 mailing list do Blog logo abaixo (ou ao lado) para receber os conteúdos em primeira mão.

Abraço.

Fontes

Android SpannableString Example

Android Spanned, SpannedString, Spannable, SpannableString and CharSequence - CommonsWare

Android Spanned, SpannedString, Spannable, SpannableString and CharSequence - Suragch

Introduction to Spans

Documentação Android - CharSequence

Documentação Android - String

Documentação Android - StringBuilder

Documentação Android - Spanned

Documentação Android - Spannable

Documentação Android - SpannableString

Documentação Android - Editable

Documentação Android - SpannableStringBuilder

Documentação Android: TextWatcher

EditText + ImageSpan disappearing when carry over to next line

Investir em Você é Barra de Ouro a R$ 2,00. Cadastre-se e receba grátis conteúdos Android sem precedentes!
Email inválido

Relacionado

MVP AndroidMVP AndroidAndroid
Como Construir Aplicativos Android Com HTML e JSOUPComo Construir Aplicativos Android Com HTML e JSOUPAndroid
Construindo a Política de Privacidade de Seu Aplicativo Android [Agora Obrigatório]Construindo a Política de Privacidade de Seu Aplicativo Android [Agora Obrigatório]Android
FlexboxLayout Para Um Design Previsível No AndroidFlexboxLayout Para Um Design Previsível No AndroidAndroid

Compartilhar

Comentários Facebook

Comentários Blog (1)

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...
alissonluann (6) (0)
11/04/2017
iae thiendo tudo beleza?
cara seria massa você fazer um tutorial de como criar um chat com web service ou de alguma forma que você saiba
Responder