Criar Views Customizadas e Interativas no Android com Kotlin
- #Kotlin
- #Android
Entre os componentes padrão do Android e o Bootstrap temos uma variedade enorme de botões, campos de texto, sliders e outros elementos para construir a interface gráfica dos nossos apps. Mas, de vez em quando, podemos nos deparar com uma situação em que queremos ter um componente completamente personalizado.
Há duas maneiras de criar Views customizadas: estender uma das classes concretas, como Button ou TextView (e já herdar vários dos atributos dessas classes), ou estender diretamente de View, quando queremos criar um componente completamente novo.
Nesse tutorial vamos criar um controlador clicável que representa um controle de intensidade de iluminação. Ele começa em 0% e sobe a intensidade em 25 pontos percentuais a cada clique. Um clique quando está em 100% faz o controle voltar para zero.
🧙♀️Passo a passo
1: Criar um projeto novo no Android Studio
Vamos chamar de LightsController. Adicione uma Activity vazia.
2: Criar um layout de mockup
Adicionar um campo de texto e uma ImageView que vai servir de placeholder para o controle; fazer isso no arquivo activity_main.xml. Coloque uma cor qualquer na propriedade background da ImageView para que ela fique visível na tela.
<TextView
android:id="@+id/textView"
style="?attr/textAppearanceHeadline4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/lights"
android:layout_margin="@dimen/margin"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.25" />
<ImageView
android:id="@+id/lightControllerView"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_margin="@dimen/margin"
android:background="@color/purple_200"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView" />
3: Criar uma classe LightControllerView que estende View
Essa classe representa a view personalizada. Usar a função "Add AndroidView constructors with @JvmOverloads" para que o Android Studio ajude a declarar o construtor da classe:
@JvmOverloads indica ao compilador que devem ser geradas sobrecargas do método que substituem os parâemetros default.
class LightControllerView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
//..Implemente a view aqui...
}
4: Declare as variáveis e constantes necessárias para criar a View
Faça-o como propriedades da classe LightControllerView. Crie duas constantes para fazer o offset dos textos, uma variável pointPosition
que vai ajudar a calcular as coordenadas dos elementos visuais e um objeto Paint
que vai desenhar os objetos na Canvas
.
class LightControllerView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private var barWidth = 0.0f
private var barHeight = 0.0f
private val LABEL_X_OFFSET = 20
private val PADDING_OFFSET = 27
private val pointPosition = PointF(0.0f, 0.0f)
private var paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.FILL_AND_STROKE
textAlign = Paint.Align.LEFT
textSize = 55.0F
typeface = Typeface.create("", Typeface.BOLD)
}
}
5: Criar uma enum ControllerSetting
Essa enum vai ajudar a organizar os valores possíveis do controlador. Queremos ir de 0 a 100. A enum tem um único atributo label
que vai ajudar a imprimir os valores na tela. Pode ser criada como uma classe interna de LightControllerView
enum class ControllerSetting(val label: Int) {
OFF(label = 0),
TWENTY_FIVE(label = 25),
FIFTY(label = 50),
SEVENTY_FIVE(label = 75),
FULL(label = 100);
}
Também vamos aproveitar e adicionar uma propriedade controllerSetting
à classe LightControllerView
que é inicializado como OFF
. Esse é o valor inicial padrão da Visualização.
6: Sobrescrever a função onSizeChanged()
Essa função é chamada sempre que a View
muda de tamanho (inclusive quando ela é inflada inicialmente), então é uma boa ideia recalcular os tamanhos dos elementos aqui, antes de entrar no método onDraw()
. Queremos que a barra tenha metade da largura da View e ocupe toda sua altura. Depois vamos usar os offsets para fazer um ajuste dessas dimensões e acomodar o texto.
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
barWidth = (w / 2).toFloat()
barHeight = h.toFloat()
}
7: Sobrescrever a função onDraw()
Vamos sobrescrever a função onDraw() para desenhar uma barra vertical dentro da View.
/**
* Esse método desenha os elementos visuais sobre a Canvas.
*/
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
/**
* Define uma cor para o objeto Paint
*/
paint.color = Color.GRAY
/**
* Desenha um retângulo sobre a Canvas descontando o
* PADDING_OFFSET no topo e na base da View
*/
canvas?.drawRect(
pointPosition.x + barWidth / 2,
pointPosition.y + PADDING_OFFSET,
(pointPosition.x + barWidth * 1.5).toFloat(),
pointPosition.y + barHeight - PADDING_OFFSET,
paint
)
}
8: Incorporar a View customizada no layout XML
Trocar a referência à ImageView
por uma referência ao objeto do tipo LightControllerView
. Deixar o atributo background com uma cor contrastante.
<br.com.chicorialabs.lightscontroller.LightControllerView
android:id="@+id/lightControllerView"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_margin="@dimen/margin"
android:background="@color/purple_200"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView" />
9: Adicionar os rótulos de texto
Antes de adicionar os rótulos precisamos descobrir suas coordenadas. Vamos usar uma função de extensão para calcular X e Y para cada rótulo de texto a partir do valor de pointPosition, da altura da View e da posição do rótulo na enum. Ufa!
fun PointF.computeXYforSettingsText(pos: SliderSetting, barWidth: Float, height: Float) {
x = (1.5 * barWidth + LABEL_X_OFFSET).toFloat()
val barHeight = height - 2 * PADDING_OFFSET
y = when(pos.ordinal) {
0 -> barHeight
1 -> barHeight * 3 / 4
2 -> barHeight / 2
3 -> barHeight / 4
4 -> 0.0f
else -> { 0.0f}
} + (1.5 * PADDING_OFFSET).toFloat()
}
Agora podemos desenhar os rótulos de texto usando a função drawText()
do Canvas
. Vamos adicionar algumas linhas ao método onDraw()
:
/**
* Instrui o objeto paint a pintar usando preto
*/
paint.color = Color.BLACK
/**
* Percorre os valores de ControllerSetting e desenha um label para
* cada item da enum.
*/
ControllerSetting.values().forEach {
pointPosition.computeXYforSettingsText(it, this.barWidth, this.barHeight)
val label = it.label.toString()
canvas?.drawText(label, pointPosition.x, pointPosition.y, paint)
}
Testar o app e ver o resultado:
10: Adicionar um retângulo de indicador
Vamos desenhar um novo retângulo, sobreposto ao primeiro, que vai mostrar o valor selecionado. A ideia é ir preenchendo a barra vertical, de baixo para cima, com incrementos de 25 unidades. Vamos começar criando uma nova função de extensão que retorna um RectF
a partir das coordenadas de pointPosition
:
private fun PointF.createIndicatorRectF(pos: SliderSetting, width: Float, height: Float) : RectF {
val left = x + width / 2
val right = (x + width * 1.5).toFloat()
val bottom = height - PADDING_OFFSET
val barHeight = height - 2 * PADDING_OFFSET
val top = when(pos.ordinal) {
0 -> bottom
1 -> bottom - barHeight / 4
2 -> bottom - barHeight / 2
3 -> bottom - barHeight * 3/ 4
4 -> 0.0f + PADDING_OFFSET
else -> { 0.0f}
}
return RectF(left, top, right, bottom)
}
Vamos adicionar mais algumas linhas ao método onDraw()
, agora para desenhar o indicador em uma cor contrastante:
/**
* Desenha o retângulo do indicador; isso precisa acontecer antes
* do desenho dos rótulos por causa do valor de pointPosition!
*/
val indicator = pointPosition.createIndicatorRectF(controllerSetting,
barWidth,
barHeight)
paint.color = Color.MAGENTA
canvas?.drawRect(indicator, paint)
Vamos modificar o valor inicial de controllerSetting
para que o retângulo fique visível:
private var controllerSetting = ControllerSetting.TWENTY_FIVE
E podemos rodar e ver o resultado:
Passamos da metade do caminho! Já temos os principais componentes da View, agora vamos torná-la interativa e responsiva aos cliques.
11: Adicionar um método next() na enum
O comportamento que queremos é:
- Incrementar o valor cada vez que a View é clicada até chegar em 100
- Se estiver em 100, voltar para zero.
Vamos implementar um método next()
na enum de ControllerSettings
usando uma cláusula when
para ciclar pelos valores:
fun next() : ControllerSetting {
return when (this) {
OFF -> TWENTY_FIVE
TWENTY_FIVE -> FIFTY
FIFTY -> SEVENTY_FIVE
SEVENTY_FIVE -> FULL
FULL -> OFF
}
}
12: Sobrescrever o método performClick()
Esse método é invocado toda vez que o ClickListener
é acionado; colocamos aqui os comportamentos visuais e deixamos o onClickListener
livre para lidar com os comportamento da aplicação.
override fun performClick(): Boolean {
if (super.performClick()) return true
/**
* Chama o método next() da enum para atualizar o valor de controllerSetting
*/
controllerSetting = controllerSetting.next()
/**
* Invalida a View para forçar um redesenho
*/
invalidate()
return true
}
13: Adicionar um bloco init { } para tornar a View clicável
Tudo pronto para testar? Ainda não! Falta configurar a View como clicável.
init {
isClickable = true
}
Antes de testar: resetar o valor inicial de controllerSetting
e eliminar a cor de fundo na view no XML.
🧙♂️Conclusão
Nesse tutorial nós vimos como criar uma View customizada e interativa usando os métodos de desenho do Canvas. Também abordamos um truque muito útil para ter diferentes estados e percorrê-los usando uma enum
.
Mas nossa View ainda não está pronta. Queremos que a barra de indicador mude de cor a cada clique e, mais importante: queremos que esse componente da UI seja acessível para pessoas que usam a função de leitor de tela do Android. Vamos fazer isso no próximo artigo.
📃Para saber mais
Apresentação do projeto:
https://docs.google.com/presentation/d/11sTDWyK7RuddWMHyFQ6DEyKvQtxNj0qA9YjtyZPKFIE/edit?usp=sharing
Página de documentação Android: https://developer.android.com/reference/android/view/View
📸
Photo by Med Badr Chemmaoui on Unsplash