Article image
Francisco Rasia
Francisco Rasia11/02/2022 09:15
Share

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.

image

image

🧙‍♀️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:

image

@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" />

image

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:

image

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:

image

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

Share
Comments (0)