Annotation Span Para Estilização de Texto no Android
(3860) (2)
CategoriasAndroid, Design, Protótipo
AutorVinícius Thiengo
Vídeo aulas186
Tempo15 horas
ExercíciosSim
CertificadoSim
CategoriaDesenvolvimento Web
Autor(es)Robert C. Martin
EditoraAlta Books
Edição1ª
Ano2023
Páginas416
Tudo bem?
Neste artigo vamos, passo a passo, estudar a Annotation Span, API utilizada principalmente em aplicativos que atendem a mais de um idioma e têm estilos complexos em Strings estáticas.
Como projeto de exemplo teremos um aplicativo medidor de luz, luxímetro, útil principalmente para engenheiros civis e arquitetos, para poder melhor ajustar a luz do ambiente. Construiremos a tela de explicação dos principais tipos de lâmpadas no mercado:
Antes de prosseguir, não deixe de se inscrever 📩 na lista de emails do Blog para ter acesso aos conteúdos exclusivos sobre desenvolvimento Android.
A seguir os pontos abordados:
- Por que utilizar Annotation se podemos utilizar tags HTML?;
- Tags HTML suportadas em código estático:
- Annotation Span:
- Projeto Android:
- Aplicando estilo complexo em código de internacionalização:
- Slides;
- Vídeos;
- Conclusão;
- Fontes.
Por que utilizar Annotation se podemos utilizar tags HTML?
Excelente pergunta. Realmente podemos utilizar HTML, mas há limitações, pois não são todas as tags HTML que são interpretadas quando as utilizando em arquivos de String, arquivos XML que serão necessários principalmente na internacionalização do aplicativo Android.
As Annotation Span entram em cena quando uma estilização em texto estático é necessária e as tags HTML suportadas não dão conta de fornecer o estilo apropriado.
Mas eu poderia trabalhar diretamente com SpannableString, certo?
Sim, você poderia. O problema, mesmo quando não há necessidade de mais de um arquivo de Strings, é que será necessário o conhecimento dos posicionamentos dos trechos de textos que precisam ser estilizados, algo dispensável quando utilizando Annotation Span, pois os posicionamentos são obtidos em tempo de execução por métodos fornecidos pela API.
Essa necessidade de conhecimento dos posicionamentos se torna algo crítico quando mais de um arquivo de Strings passa a ser necessário ou quando o único arquivo de Strings precisa passar por atualização de texto, as posições utilizadas em código como "hard-coded" deverão ser atualizadas, algo que atrapalha a evolução de qualquer projeto de software.
Tags HTML suportadas em código estático
Para que você não queira sempre utilizar a Annotation Span, até porque o código dela e menos simples do que quando utilizando tags HTML, a seguir deixo todas as tags HTML de estilo suportadas em arquivos Android de Strings:
- <b> para negrito;
- <i> para itálico;
- <big> para um texto 25% maior do que o tamanho aplicado ao conjunto;
- <small> para um texto 20% menor do que o tamanho aplicado ao conjunto;
- <font> para um texto com cor distinta (em hexadecimal e utilizando o atributo color) ou com uma família de fontes distinta (utilizando o atributo face com algum dos possíveis valores: serif, sans_serif e monospace);
- <tt> para um texto em monospace;
- <strike> para um texto com uma linha no meio;
- <u> para um texto sublinhado;
- <sup> para um texto em linha superior;
- <sub> para um texto em linha inferior.
Se você for estudar também os links de fontes que coloquei neste artigo, notará que em um deles, mais precisamente o artigo da Florina Muntenescu, do Google Android Developers, há ainda mais tags HTML de estilo que são suportadas, digo, que é informado que são suportadas.
Mas quando testando em código, para tags de estilo, somente as que citei anteriormente é que funcionam.
Tags de estilo em execução
A seguir um código XML, strings.xml, com algumas configurações de tag HTML de estilo em uso:
<resources>
<string name="app_name">Annotation Span</string>
<string name="title_html_texts">
Textos com formatação em HTML
</string>
<string name="html_bold">
Negrito com a tag <b><![CDATA[<b>]]></b>.
</string>
<string name="html_italic">
Itálico com a tag <i><![CDATA[<i>]]></i>.
</string>
<string name="html_big">
Texto grande com a tag <big><![CDATA[<big>]]></big>.
</string>
<string name="html_small">
Texto pequeno com a tag <small><![CDATA[<small>]]></small>.
</string>
<string name="html_font">
<font face="monospace" color="#FF0000">Texto com família de fonte monospace e cor vermelha por meio da tag
<![CDATA[<font face=\"monospace\" color=\"#FF0000\">]]></font>.
</string>
<string name="html_tt">
Monospace com a tag <tt><![CDATA[<tt>]]></tt>.
</string>
<string name="html_strikethrough">
Linha central (de corte) com a tag <strike><![CDATA[<strike>]]></strike>.
</string>
<string name="html_underline">
Sublinhado com a tag <u><![CDATA[<u>]]></u>.
</string>
<string name="html_superscript">
Texto em linha superior com a tag <sup><![CDATA[<sup>]]></sup>.
</string>
<string name="html_subscript">
Texto em linha inferior com a tag <sub><![CDATA[<sub>]]></sub>.
</string>
</resources>
Note que <![CDATA[...]]> é utilizado para escapar os símbolos de tag <>.
Executando o aplicativo Android com o código anterior, aplicando cada String a um TextView, temos:
Annotation Span
Uma tag Annotation é formada por: <annotation key="value">. É isso mesmo, o conhecido key="value".
Você pode utilizar o rótulo que quiser para a key (chave), mas seguindo as restrições de nome que temos para variáveis em código dinâmico: nada de espaços em branco ou acentuação.
Para value (valor), qualquer valor pode ser utilizado, mas esse sempre será tratado em código como String.
Arquivos com annotation
A seguir temos um arquivo de Strings, /res/values/strings.xml, que representa o idioma padrão do aparelho de testes, aqui um device em português do Brasil:
<resources>
<string name="app_name">Annotation Span</string>
<string name="title_texts">
Textos com formatação em HTML e em Annotation Span
</string>
<string name="html_and_annotation_to_complex_style">
<font color="#FF0000"><annotation fontFamily="cinzel">Olá</annotation></font>
<annotation fontFamily="fredoka_one">Mundo!</annotation>
</string>
</resources>
Então o arquivo de Strings, /res/values-en/strings.xml, que representa o idioma inglês para aparelhos tendo está língua como padrão:
<resources>
<string name="app_name">Annotation Span</string>
<string name="title_texts">
Texts with HTML and Annotation Span formatting
</string>
<string name="html_and_annotation_to_complex_style">
<font color="#FF0000"><annotation fontFamily="cinzel">Hello</annotation></font>
<annotation fontFamily="fredoka_one">World!</annotation>
</string>
</resources>
Note que não há problemas em utilizar ao mesmo tempo tags HTML de estilo e tags <annotation>.
A definição key="value" para o exemplo é: fontFamily="nome da família de fonte".
Como estaremos utilizando uma família de fonte não padrão, a melhor escolha foi o uso de Annotation Span (a tag <annotation> é uma Span - abreviação de Spanned).
Em resumo: uma Span permite a aplicação de estilos em String.
Aplicando estilos de acordo com as annotation tags
Agora é preciso, em código dinâmico, identificar as annotations e então aplicar os estilos corretos.
É importante assumir que temos duas fontes extras em /res/font. São elas:
- cinzel.ttf;
- e fredoka_one.ttf.
Para saber mais sobre fonts in XML no Android, acesse Fontes em XML, Android O. Configuração e Uso.
Assim o código que aplica os estilos, famílias de fontes (leia todos os comentários):
...
override fun onCreate( savedInstanceState: Bundle? ) {
super.onCreate( savedInstanceState )
setContentView( R.layout.activity_main )
workInText()
}
fun workInText(){
/*
* Aplicando o casting de CharSequence para SpannedString
* para que seja possível acessar as Spans presentes no
* texto.
*
* É preciso utilizar getText(), que retorna um CharSequence.
* getString() retorna uma String e Strings não contém Spans.
* */
val text = getText(R.string.html_and_annotation_to_complex_style) as SpannedString
/*
* Obtendo todas as Annotation Span do texto.
* */
val annotations = text.getSpans(
0, /* Posição inicial do texto. */
text.length, /* Posição final do texto. */
Annotation::class.java /* Classe Span (ParcelableSpan) para recolher objetos do tipo. */
)
/*
* Criando uma cópia do texto, com SpannableString, para
* que seja possível adicionar ou remover Span.
* */
val spannableString = SpannableString( text )
/*
* Iterando sobre todas as annotations.
* */
for( annotation in annotations ){
/*
* Verificando se a Span (annotation) atual é a de
* chave "fontFamily".
* */
if( annotation.key.equals("fontFamily") ){
val fontFamily = annotation.value
/*
* Bloco condicional para verificar o valor em
* value e assim aplicar a família de fonte
* correta ao texto.
* */
val typeface = if( fontFamily.equals("cinzel") ){
ResourcesCompat.getFont( this, R.font.cinzel )
}
else{
ResourcesCompat.getFont( this, R.font.fredoka_one )
}
/*
* Colocando a nova Span de estilo no local
* correto do texto obtido do arquivo de Strings.
* */
spannableString.setSpan(
CustomTypefaceSpan( typeFace!! ),
text.getSpanStart( annotation ),
text.getSpanEnd( annotation ),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
}
/*
* Colocando o texto estilizado no TextView.
* */
tv_hello_world.text = spannableString
}
...
Note como os text.getSpanStart( annotation ) e text.getSpanEnd( annotation ) permitem que nós desenvolvedores não tenhamos de gravar em código os posicionamentos dos textos que terão de ser estilizados. Este é um grande ganho em relação a utilizar diretamente, e somente, objetos Spanned em código dinâmico.
Você deve estar se perguntando sobre a classe CustomTypefaceSpan, certo?
Essa é uma classe hackcode criada para ser possível a definição de família de fonte em uma String Spanned, segue o boilerplate code dela:
class CustomTypefaceSpan(typeFace: Typeface) : TypefaceSpan("") {
val newTypeFace = typeFace
override fun updateDrawState(paint: TextPaint) {
applyCustomTypeFace(paint, newTypeFace)
}
override fun updateMeasureState(paint: TextPaint) {
applyCustomTypeFace(paint, newTypeFace)
}
private fun applyCustomTypeFace(paint: Paint, typeface: Typeface) {
val styleAnterior: Int
val typefaceAnterior = paint.getTypeface()
if (typefaceAnterior == null) {
styleAnterior = 0
}
else {
styleAnterior = typefaceAnterior.getStyle()
}
/* Para verificar a compatibilidade de estilos. */
val fake = styleAnterior and typeface.style.inv()
/*
* 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 and 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 and Typeface.ITALIC != 0) {
paint.setTextSkewX(-0.25f)
}
/* Aplica a fonte. */
paint.setTypeface(typeface)
}
}
Está classe também esta presente no artigo de fonts in XML indicado anteriormente.
Executando o projeto, primeiro com o aparelho no idioma configurado em português e depois modificando o idioma para inglês, temos:
Conteúdos importantes
Para tirar o máximo do conteúdo sobre Annotation Span, fortemente recomendo que ao final do estudo deste artigo você estude também os artigos dos links a seguir, todos sobre famílias de fontes e estilização de Strings no Android:
- Como Utilizar Spannable no Android Para Customizar Strings;
- Fontes em XML, Android O. Configuração e Uso;
- Definindo Fontes em Trechos Não Triviais do Android.
Ponto negativo
- Tags <annotation> têm a limitação de aceitarem somente um par key="value". Poderia aceitar mais, evitando o uso de mais de uma <annotation> para um mesmo trecho de texto estático, por exemplo.
Ponto positivo
- Ganho considerável, em código dinâmico, pela não necessidade de conhecimento prévio das posições dos trechos de textos que devem ser estilizados.
Considerações finais
Quando há a necessidade de trabalho com Spanned String em texto estático, mesmo que ainda não havendo necessidade de mais de um idioma em projeto Android, o uso de tags <annotation> trará maior eficiência principalmente na atualização de código, de textos em strings.xml.
Isso, pois o conhecimento prévio do posicionamento dos textos que têm de receber estilos complexos não será necessário, as APIs de Annotation permitem o fácil acesso às posições.
Projeto Android
Como projeto de exemplo teremos uma parte de um aplicativo de medição de intensidade de luz, luxímetro. Vamos trabalhar a tela de lâmpadas do app, tela que contém algumas breves explicações das principais lâmpadas em mercado.
Vamos prosseguir com o exemplo em duas partes:
- Primeiro a parte onde as tags <annotation> ainda não estarão sendo utilizadas e consequentemente não haverá estilos nos textos estáticos;
- Na segunda parte vamos aplicar estilos, cor e família de fonte, para parte dos textos, isso utilizando as tags <annotation> junto à tag HTML <font>.
Para acessar o projeto completo, entre no GitHub dele em: https://github.com/viniciusthiengo/luxmetro-max-kotlin-android.
Recomendo que você siga todo o artigo, incluindo as duas partes do projeto, pois características extras no dev Android serão também abordadas.
Protótipo estático
A seguir as telas do protótipo estático da primeira parte do projeto:
Tela de entrada | Listagem de lâmpadas - sem estilo em texto |
Iniciando o projeto
Em seu Android Studio inicie um novo projeto Kotlin:
- Nome da aplicação: Luxímetro Max;
- API mínima: 16 (Android Jelly Bean);
- Atividade inicial: Basic Activity;
- Nome da atividade inicial: LampsActivity;
- Para todos os outros campos, mantenha os valores já definidos por padrão.
Ao final da primeira parte teremos a seguinte arquitetura no Android Studio IDE:
Configurações Gradle
A seguir as configurações do Gradle Nível de Projeto, ou build.gradle (Project: LuxmetroMax):
buildscript {
ext.kotlin_version = '1.3.11'
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.2.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 do Gradle Nível de Aplicativo, ou build.gradle (Module: app):
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
android {
compileSdkVersion 28
defaultConfig {
applicationId "thiengo.com.br.luxmetromax"
minSdkVersion 16
targetSdkVersion 28
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'com.android.support:appcompat-v7:28.0.0'
implementation 'com.android.support:design:28.0.0'
}
Configurações AndroidManifest
A seguir as configurações do AndroidManifest.xml:
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="thiengo.com.br.luxmetromax">
<application
android:hardwareAccelerated="true"
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=".LampsActivity"
android:label="@string/app_name"
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
Configurações de estilo
Agora os arquivos de estilo, iniciando com o arquivo de cores, /res/values/colors.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#424242</color>
<color name="colorPrimaryDark">#1B1B1B</color>
<color name="colorAccent">#C7A500</color>
<color name="colorLampBackground">#22FFD600</color>
<color name="colorLightWhite">#33FFFFFF</color>
<color name="colorDivider">#11FFFFFF</color>
</resources>
Assim o arquivo de dimensões, /res/values/dimens.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="lamp_margin">12dp</dimen>
</resources>
Como o 12dp é utilizado com frequência no layout de item, /res/layout/lamp.xml, foi uma melhor escolha mante-lo em um arquivo único de dimensões, assim a possível atualização ocorrerá somente em dimens.xml.
Agora o arquivo de Strings para o idioma português Brasil, /res/values/strings.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Luxímetro Max</string>
<string name="lights_activity_title">Tipos de lâmpadas</string>
<string name="lamp_led">
Díodos Emissores de Luz (LED):
são consideradas as lâmpadas
mais modernas – produto de última tecnologia. Convertem
energia elétrica diretamente em energia luminosa, através de
pequenos chips.
</string>
<string name="lamp_incandescent">
Lâmpadas Incandescentes:
são as lâmpadas mais antigas, que
todos nós já tivemos ou ainda temos em nossas casas. Por
terem baixa eficiência, estão sendo substituídas pelas
lâmpadas fluorescentes.
</string>
<string name="lamp_halogen">
Lâmpadas Halógenas:
também são consideradas lâmpadas
incandescentes (uma corrente elétrica percorre um filamento
liberando calor e luz), mas por possuirem halogênio (geralmente
bromo ou iodo) em sua constituição, são chamadas de lâmpadas
halógenas.
</string>
<string name="lamp_fluorescent">
Lâmpadas Fluorescentes:
são as mais conhecidas e indicadas para
o uso residencial e comercial, pois apresentam alta eficiência
e baixo consumo de energia.
</string>
<string name="lamp_hid">
Lâmpadas de Descarga (HID):
uma descarga (de alta pressão)
elétrica entre os eletrodos leva os componentes internos do
tubo de descarga a produzirem luz.
</string>
</resources>
Então o arquivo de Strings para o idioma inglês, também atendido pelo aplicativo de exemplo. Segue /res/layout/values-en/strings.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Luximeter Max</string>
<string name="lights_activity_title">Types of lamps</string>
<string name="lamp_led">
Light Emitting Diodes (LED):
they are considered the most modern lamps - product of
latest technology. They convert electricity directly
into light energy through small chips.
</string>
<string name="lamp_incandescent">
Incandescent Lamps:
they are the oldest lamps we have ever had or still have
in our homes. Because they have low efficiency, they are
being replaced by fluorescent lamps.
</string>
<string name="lamp_halogen">
Halogen Lamps:
they are also considered incandescent lamps (an electric
current runs through a filament releasing heat and light),
but because they have halogen (usually bromine or iodine)
in their constitution, they are called halogen lamps.
</string>
<string name="lamp_fluorescent">
Fluorescent Lamps:
they are the most known and indicated for residential and
commercial use, because they have high efficiency and low
energy consumption.
</string>
<string name="lamp_hid">
Discharge Lamps (HID):
an electric discharge (high pressure) between the electrodes
leads the internal components of the discharge tube to
produce light.
</string>
</resources>
Por fim o arquivo de definição de tema no app, /res/values/styles.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--
Estilo padrão, aplicado em todo o projeto.
-->
<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>
<!--
Para que a barra de topo padrão não seja utilizada e
assim somente o AppBarLayout junto ao Toolbar possam ser
usados.
-->
<style name="AppTheme.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
</style>
<!--
Para o correto enquadramento do AppBarLayout.
-->
<style
name="AppTheme.AppBarOverlay"
parent="ThemeOverlay.AppCompat.Dark.ActionBar"/>
</resources>
Classe de domínio
Vamos trabalhar com apenas uma classe de domínio, responsável por conter os dados das lâmpadas. Em /domain crie a classe Kotlin Lamp como a seguir:
class Lamp(
val imageRes: Int,
val description: String )
Classe container
Apesar de ser a classe que vai conter os dados de lâmpadas do projeto, dessa vez não temos uma classe mock data, de dados simulados.
Neste projeto de luxímetro, se ele fosse a produção, essa classe permaneceria a mesma, pois esses dados de lâmpadas são estáticos e não seriam obtidos de alguma base de dados remota.
No pacote /data crie a classe Lamps como a seguir:
class Lamps {
companion object {
fun getLamps( context: Context )
= listOf(
Lamp(
R.drawable.led,
context.getString(R.string.lamp_led)
),
Lamp(
R.drawable.incandescente,
context.getString(R.string.lamp_incandescent)
),
Lamp(
R.drawable.halogena,
context.getString(R.string.lamp_halogen)
),
Lamp(
R.drawable.fluorescente,
context.getString(R.string.lamp_fluorescent)
),
Lamp(
R.drawable.hid,
context.getString(R.string.lamp_hid)
)
)
}
}
Todas as imagens do projeto também estão disponíveis no GitHub dele.
Classe adaptadora de itens lâmpada
Poderíamos ter criado um layout sem uso de um framework de lista, mas assim teríamos mais trabalho para a colocação das Views de cada item de lâmpada, dessa forma o uso de um RecyclerView se saiu como uma melhor opção.
Para a classe adaptadora, LampsAdapter, vamos iniciar com o layout, /res/layout/lamp.xml:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/lamp_margin"
android:paddingBottom="@dimen/lamp_margin">
<FrameLayout
android:id="@+id/fl_lamp"
android:layout_alignParentTop="true"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:padding="4dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/lamp_container">
<ImageView
android:id="@+id/iv_lamp"
android:padding="9dp"
android:layout_width="92dp"
android:layout_height="92dp"
android:scaleType="fitCenter"
android:background="@drawable/lamp_background"/>
</FrameLayout>
<View
android:id="@+id/v_vertical_line"
android:layout_width="4dp"
android:layout_height="40dp"
android:background="@color/colorAccent"
android:layout_alignParentTop="true"
android:layout_alignParentBottom="true"
android:layout_toRightOf="@+id/fl_lamp"
android:layout_toEndOf="@+id/fl_lamp"
android:layout_marginLeft="@dimen/lamp_margin"
android:layout_marginStart="@dimen/lamp_margin"/>
<View
android:id="@+id/v_horizontal_line"
android:layout_width="10dp"
android:layout_height="4dp"
android:background="@color/colorAccent"
android:layout_alignParentBottom="true"
android:layout_toRightOf="@+id/v_vertical_line"
android:layout_toEndOf="@+id/v_vertical_line"
android:layout_marginRight="1dp"
android:layout_marginEnd="1dp"/>
<TextView
android:id="@+id/tv_description"
android:layout_toRightOf="@+id/v_horizontal_line"
android:layout_toEndOf="@+id/v_horizontal_line"
android:layout_alignParentTop="true"
android:textColor="@android:color/white"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="6dp"
android:bufferType="spannable" />
</RelativeLayout>
Abaixo o diagrama do layout anterior:
Você deve ter notado o uso de dois backgrounds personalizados, certo? Estes arquivos drawable de background nos permitem seguir com facilidade o que foi definido em protótipo estático.
A seguir o arquivo que permite um background amarelo e com transparência no ImageView de item, /res/drawable/lamp_background.xml:
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<!--
Definindo a cor de background do shape.
-->
<solid android:color="@color/colorLampBackground" />
<!--
Definindo o nível de curvatura de borda do shape.
-->
<corners android:radius="2dp" />
</shape>
Então o arquivo que permite as bordas brancas no FrameLayout container do ImageView de item, /res/drawable/lamp_container.xml:
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<!--
Definindo a cor de background do shape.
-->
<solid android:color="@android:color/transparent" />
<!--
Definindo a espessura e cor da borda do shape.
-->
<stroke
android:color="@color/colorLightWhite"
android:width="0.8dp" />
<!--
Definindo o nível de curvatura de borda do shape.
-->
<corners android:radius="2dp" />
</shape>
Com os drawables anteriores nós conseguimos o seguinte efeito na imagem de item:
Agora o código dinâmico da classe LampsAdapter:
class LampsAdapter( val lamps: List<Lamp> ):
RecyclerView.Adapter<LampsAdapter.ViewHolder>() {
override fun onCreateViewHolder(
parent: ViewGroup, type: Int
): ViewHolder {
val layout = LayoutInflater
.from( parent.context )
.inflate(
R.layout.lamp,
parent,
false
)
return ViewHolder( layout )
}
override fun onBindViewHolder(
holder: ViewHolder,
position: Int
) {
holder.setModel( lamps[ position ] )
}
override fun getItemCount() = lamps.size
inner class ViewHolder( itemView: View):
RecyclerView.ViewHolder( itemView ){
val ivLamp: ImageView
val tvDescription: TextView
init{
ivLamp = itemView.findViewById( R.id.iv_lamp )
tvDescription = itemView.findViewById( R.id.tv_description )
}
fun setModel( lamp: Lamp ){
ivLamp.setImageResource( lamp.imageRes )
tvDescription.text = lamp.description
}
}
}
Atividade de apresentação de lâmpadas
Para a atividade principal de projeto, vamos iniciar com o layout, /res/layout/activity_lamps.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="@color/colorPrimary"
tools:context=".LampsActivity">
<android.support.design.widget.AppBarLayout
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:theme="@style/AppTheme.AppBarOverlay">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:title="@string/lights_activity_title"/>
</android.support.design.widget.AppBarLayout>
<android.support.v7.widget.RecyclerView
app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:id="@+id/rv_lamps"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/lamp_margin"
android:paddingRight="@dimen/lamp_margin"/>
</android.support.design.widget.CoordinatorLayout>
A seguir o diagrama do layout anterior:
Agora o código Kotlin de LampsActivity:
class LampsActivity : AppCompatActivity() {
override fun onCreate( savedInstanceState: Bundle? ) {
super.onCreate( savedInstanceState )
setContentView( R.layout.activity_lamps )
setSupportActionBar( toolbar )
/*
* Somente para a apresentação da seta de "back screen"
* na barra de topo.
* */
supportActionBar?.setDisplayHomeAsUpEnabled( true )
supportActionBar?.setDisplayShowHomeEnabled( true )
initLampList()
}
private fun initLampList(){
rv_lamps.setHasFixedSize( false )
val layoutManager = LinearLayoutManager( this )
rv_lamps.layoutManager = layoutManager
val divider = DividerItemDecoration(
this,
layoutManager.orientation
)
divider.setDrawable(
ContextCompat.getDrawable(
this,
R.drawable.divider_layout
)!!
)
rv_lamps.addItemDecoration( divider )
val adapter = LampsAdapter( Lamps.getLamps( this ) )
rv_lamps.adapter = adapter
}
}
Para o divisor de itens do RecyclerView criamos um novo drawable para mudar a cor e espessura da linha. Segue /res/drawable/divider_layout.xml:
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<!--
Definindo a espessura da linha.
-->
<size android:height="1dp" />
<!--
Definindo a cor da linha.
-->
<solid android:color="@color/colorDivider" />
</shape>
Aplicando estilo complexo em código de internacionalização
Nossa meta a partir deste ponto é:
- Colocar a accent color do projeto Android como a cor dos nomes de lâmpadas;
- Colocar a fonte Open Sans Condensed como família de fonte dos nomes de lâmpadas.
O protótipo estático a seguir deixa mais claro as necessidades de atualização:
Listagem de lâmpadas - com estilo em texto |
A tag HTML <font> certamente será utilizada para a atualização de cor de texto. Para a família de fonte personalizada teremos de usar tags <annotation>.
Tags de estilo e Span nos arquivos de String
Nosso primeiro passo é atualizar os dois arquivos de String.
Primeiro o arquivo padrão, para o idioma português Brasil, /res/values/strings.xml:
<?xml version="1.0" encoding="utf-8"?>
<!--
Com o DOCTYPE abaixo é possível referenciar strings
dentro do arquivo de strings.
-->
<!DOCTYPE resources [
<!ENTITY fontColor "#C7A500">
]>
<resources>
<string name="app_name">Luxímetro Max</string>
<string name="lights_activity_title">Tipos de lâmpadas</string>
<!--
Se nenhum tag Span, como <annotation>, for utilizada
dentro de <string> então getText() retorna uma String
ao invés de uma CharSequence. O método getString()
sempre retorna uma String, sem tags Span.
-->
<string name="lamp_led">
<font color="&fontColor;">
<annotation fontFamily="open_sans_condensed_bold">
Díodos Emissores de Luz (LED):
</annotation>
</font>
são consideradas as lâmpadas
mais modernas – produto de última tecnologia. Convertem
energia elétrica diretamente em energia luminosa, através de
pequenos chips.
</string>
<string name="lamp_incandescent">
<font color="&fontColor;">
<annotation fontFamily="open_sans_condensed_bold">
Lâmpadas Incandescentes:
</annotation>
</font>
são as lâmpadas mais antigas, que
todos nós já tivemos ou ainda temos em nossas casas. Por
terem baixa eficiência, estão sendo substituídas pelas
lâmpadas fluorescentes.
</string>
<string name="lamp_halogen">
<font color="&fontColor;">
<annotation fontFamily="open_sans_condensed_bold">
Lâmpadas Halógenas:
</annotation>
</font>
também são consideradas lâmpadas
incandescentes (uma corrente elétrica percorre um filamento
liberando calor e luz), mas por possuirem halogênio (geralmente
bromo ou iodo) em sua constituição, são chamadas de lâmpadas
halógenas.
</string>
<string name="lamp_fluorescent">
<font color="&fontColor;">
<annotation fontFamily="open_sans_condensed_bold">
Lâmpadas Fluorescentes:
</annotation>
</font>
são as mais conhecidas e indicadas para
o uso residencial e comercial, pois apresentam alta eficiência
e baixo consumo de energia.
</string>
<string name="lamp_hid">
<font color="&fontColor;">
<annotation fontFamily="open_sans_condensed_bold">
Lâmpadas de Descarga (HID):
</annotation>
</font>
uma descarga (de alta pressão)
elétrica entre os eletrodos leva os componentes internos do
tubo de descarga a produzirem luz.
</string>
</resources>
Note o uso de DOCTYPE para evitar a repetição do código hexadecimal de cor, assim é possível atualizar a cor em somente um ponto em cada arquivo de String.
Agora a mesma atualização no arquivo do idioma inglês, /res/values-en/strings.xml:
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE resources [
<!ENTITY fontColor "#C7A500">
]>
<resources>
<string name="app_name">Luximeter Max</string>
<string name="lights_activity_title">Types of lamps</string>
<string name="lamp_led">
<font color="&fontColor;">
<annotation fontFamily="open_sans_condensed_bold">
Light Emitting Diodes (LED):
</annotation>
</font>
they are considered the most modern lamps - product of
latest technology. They convert electricity directly
into light energy through small chips.
</string>
<string name="lamp_incandescent">
<font color="&fontColor;">
<annotation fontFamily="open_sans_condensed_bold">
Incandescent Lamps:
</annotation>
</font>
they are the oldest lamps we have ever had or still have
in our homes. Because they have low efficiency, they are
being replaced by fluorescent lamps.
</string>
<string name="lamp_halogen">
<font color="&fontColor;">
<annotation fontFamily="open_sans_condensed_bold">
Halogen Lamps:
</annotation>
</font>
they are also considered incandescent lamps (an electric
current runs through a filament releasing heat and light),
but because they have halogen (usually bromine or iodine)
in their constitution, they are called halogen lamps.
</string>
<string name="lamp_fluorescent">
<font color="&fontColor;">
<annotation fontFamily="open_sans_condensed_bold">
Fluorescent Lamps:
</annotation>
</font>
they are the most known and indicated for residential and
commercial use, because they have high efficiency and low
energy consumption.
</string>
<string name="lamp_hid">
<font color="&fontColor;">
<annotation fontFamily="open_sans_condensed_bold">
Discharge Lamps (HID):
</annotation>
</font>
an electric discharge (high pressure) between the electrodes
leads the internal components of the discharge tube to
produce light.
</string>
</resources>
Trocando getString() por getText()
Na classe de dados Lamps estamos acessando as Strings por meio do método getString(). Este método retorna uma String sem nenhuma formatação, pois a classe String não suporta formatação.
Temos de modificar o código para passar a invocar getText() que retorna uma SpannedString como CharSequence, tipo que suporta formatação. Segue atualização de Lamps:
class Lamps {
companion object {
fun getLamps( context: Context )
= listOf(
Lamp(
R.drawable.led,
context.getText(R.string.lamp_led)
),
Lamp(
R.drawable.incandescente,
context.getText(R.string.lamp_incandescent)
),
Lamp(
R.drawable.halogena,
context.getText(R.string.lamp_halogen)
),
Lamp(
R.drawable.fluorescente,
context.getText(R.string.lamp_fluorescent)
),
Lamp(
R.drawable.hid,
context.getText(R.string.lamp_hid)
)
)
}
}
Classe para família de fonte personalizada
No projeto crie o pacote /util e coloque como única classe a CustomTypefaceSpan, responsável por permitir que uma família de fonte personalizada seja colocada a um texto como Spanned de estilo:
class CustomTypefaceSpan(typeFace: Typeface) : TypefaceSpan("") {
val newTypeFace = typeFace
override fun updateDrawState(paint: TextPaint) {
applyCustomTypeFace(paint, newTypeFace)
}
override fun updateMeasureState(paint: TextPaint) {
applyCustomTypeFace(paint, newTypeFace)
}
private fun applyCustomTypeFace(paint: Paint, typeface: Typeface) {
val styleAnterior: Int
val typefaceAnterior = paint.getTypeface()
if (typefaceAnterior == null) {
styleAnterior = 0
}
else {
styleAnterior = typefaceAnterior.getStyle()
}
val fake = styleAnterior and typeface.style.inv()
if (fake and Typeface.BOLD != 0) {
paint.setFakeBoldText(true)
}
if (fake and Typeface.ITALIC != 0) {
paint.setTextSkewX(-0.25f)
}
paint.setTypeface(typeface)
}
}
Código Span na classe de domínio
Ainda temos de atualizar a classe Lamp, para receber o tipo de valor correto e também trabalhar a cache da String estilizada, pois caso contrário iremos sobrecarregar a memória reservada ao aplicativo quando nas invocações de onBindViewHolder().
Em Lamp atualize (ou adicione) os códigos em destaque:
/*
* O tipo de "description" foi alterado de String para
* CharSequence, pois String não retém formatação, Span.
* */
class Lamp(
val imageRes: Int,
val description: CharSequence ) {
/*
* Propriedade utilizada para conter a description já com
* a tag Span configurada corretamente e assim evitar
* re-chamada de algoritmo de colocação de Span de família
* de fonte. spannableString trabalha como cache de conteúdo.
* */
private var spannableString = SpannableString("")
fun getDescriptionStyled( context: Context ) : SpannableString{
/*
* Padrão Cláusula de Guarda para evitar que o algoritmo
* de colocação de Span de família de fonte não seja
* chamado novamente quando não mais for necessário
* (somente uma execução é o suficiente).
* */
if( spannableString.length > 0 ){
return spannableString
}
/*
* Aplicando o casting de CharSequence para SpannedString
* para que seja possível acessar as Spans presentes no
* texto.
*
* O trim() está sendo utilizado para remover os espaços
* em branco nos limites do texto, devido às formatações
* em strings.xml.
* */
val spannedDesc = (description.trim()) as SpannedString
/*
* Obtendo todas as Annotation Span do texto.
* */
val annotations = spannedDesc.getSpans(
0,
spannedDesc.length,
Annotation::class.java
)
/*
* Criando uma cópia do texto, com SpannableString, para
* que seja possível adicionar ou remover Span.
* */
spannableString = SpannableString( spannedDesc )
for( annotation in annotations ){
/*
* Annotation trabalha no modelo <key, value>, onde o
* rótulo do atributo utilizado na tag <annotation> é
* a key e o valor do atributo é o value.
*
* No condicional abaixo estamos verificando se é a
* chave "fontFamily" no atual annotation do loop.
* Podemos utilizar qualquer chave de nossa criação.
* */
if( annotation.key.equals("fontFamily") ){
/*
* Como estamos trabalhando somente com uma
* família de fonte via Span, não há necessidade
* de acessar o valor em annotation.value para
* ainda mais comparações em blocos condicionais.
* */
val typeface = ResourcesCompat.getFont( context, R.font.open_sans_condensed_bold )
spannableString.setSpan(
CustomTypefaceSpan( typeface!! ),
spannedDesc.getSpanStart( annotation ),
spannedDesc.getSpanEnd( annotation ),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
}
return spannableString
}
}
Note que se a String em strings.xml não tiver alguma tag de estilo, o casting (description.trim()) as SpannedString falha, pois não é uma SpannedString e sim uma String retornada como CharSequence.
O trim() está sendo utilizado para remover os espaços em branco nos limites dos nomes de cada lâmpada, espaços que existem devido às quebras de linhas nos arquivos strings.xml.
spannableString está sendo utilizada como propriedade cache, para evitar a repetição de chamadas ao algoritmo completo de getDescriptionStyled(). Note também a importância do uso do padrão Cláusula de Guarda para que spannableString funcione como propriedade cache.
Trocando description por getDescriptionStyled()
Na classe adaptadora, LampsAdapter, atualize o código de acesso a descrição, como a seguir:
...
fun setModel( lamp: Lamp ){
ivLamp.setImageResource( lamp.imageRes )
tvDescription.text = lamp.getDescriptionStyled( itemView.context )
}
...
Testes e resultados
Abra o Android Studio, vá em "Build", então em "Rebuid project". Ao final do rebuild execute o aplicativo em seu aparelho ou emulador Android de testes. Assim teremos:
Com isso finalizamos o estudo da Android Annotation Span para aplicação de estilos complexos em textos estáticos.
Não deixe de se inscrever na 📩 lista de emails do Blog para receber em primeira mão conteúdos Android exclusivos.
Se inscreva também no canal do Blog em: YouTube Thiengo.
Slides
Abaixo os slides sobre como utilizar a Annotation Span no Android:
Vídeos
A seguir os vídeos mostrando passo a passo a atualização do aplicativo Android Luxímetro Max:
Para acessar o projeto de exemplo entre no GitHub dele em: https://github.com/viniciusthiengo/luxmetro-max-kotlin-android.
Conclusão
Em caso de internacionalização de aplicativo, contendo textos estáticos com estilos não atendidos pelas tags HTML suportadas, tags <annotation> junto a códigos dinâmico Spanned são a opção para manter o layout como definido em protótipo.
Mesmo em caso de não internacionalização, porém com uso de texto estático com estilo complexo, o uso de Annotation tende a ser a melhor opção ao invés de somente Spanned em código dinâmico, isso, pois Annotation dispensa a necessidade de conhecimento de posicionamento de texto em estilização.
Com isso finalizamos o conteúdo. Caso você tenha dúvidas ou sugestões sobre formatação de texto no Android, deixe logo abaixo nos comentários.
Curtiu o conteúdo? Não esqueça de compartilha-lo. E, por fim, não deixe de se inscrever na 📩 lista de emails.
Abraço.
Fontes
Styling internationalized text in Android
Language and locale resolution overview
Get font resource from TypedArray before API 26 - Resposta de Tom
Display Back Arrow on Toolbar - Resposta de MrEngineer13 e de Brais Gabin
Set drawable for DividerItemDecoration
Reference one string from another string in strings.xml? - Resposta de Beeing Jk e de Joseph Garrone
Iluminação - Tipos de lâmpadas
Tipos de Lâmpadas - Saiba escolher a lâmpada ideal: quais os tipos, sua eficiência e onde são usadas
Comentários Facebook