Aprenda a usar o Spinner (caixa de seleção) no Android (Kotlin)

Spinner

Um dos componentes disponibilizados pelo Android é o Spinner, que é um elemento bem parecido (se não igual) com o SELECT do HTML.

Este conteúdo será divido em três partes, onde iremos conhecer diferentes formas de se declarar um Spinner:

  • Spinner Estático
  • Spinner Dinâmico
  • Spinner Personalizado

Spinner Estático

Como o próprio nome já nos diz, é uma caixa de seleção onde nossos itens já são criados e definidos diretamente pela View.

E isso significa dizer que os itens ali existentes estão sendo declarados diretamente no elemento do XML, e não via código.

Veja abaixo como é feita a declaração de um spinner estático

<Spinner
android:id="@+id/spinner_estatico"
android:entries="@array/opcoes_array"
android:spinnerMode="dropdown"
android:dropDownVerticalOffset="50dp"
android:layout_width="match_parent"
android:layout_height="wrap_content" />

Note que atribuímos um id para ele, para que mais tarde consigamos selecionar o item escolhido pelo usuário.

O atributo spinnerMode, diz que a lista deve se comportar como uma dropdown.

Já o atributo dropDownVerticalOffset define a margem que a lista vai ser criada em relação ao spinner.

Quando este atributo não está definido a lista é aberta em cima do Spinner.

Ali dentro você pode perceber uma nova tag chamada de entries, que nada mais é do que as entradas (itens) que serão mostrados dentro do seu Spinner.

Por padrão, nós definimos essas entradas dentro do arquivo strings.xml da seguinte forma:

<resources>
<string name="app_name">...</string>

<string-array name="opcoes_array">
<item>Micilini Roll</item>
<item>Gabriel Solano</item>
<item>Toneleda Tias</item>
</string-array>

</resources>

Executando o projeto, você verá o Spinner 100% funcional:

Bem legal e muito simples de se implementar, não acha?

Só que... no SELECT do HTML, nós podemos definir um VALUE para cada um dos itens existentes na caixa de seleção.

Isso para sabermos qual dos itens estamos selecionando. Sabendo disso, como atrelar um valor para cada um dos itens do spinner?

O processo não é tão simples como é feito no SELECT do HTML, aqui precisamos primeiro definir um segundo array de valores dentro do arquivo strings.xml, como estamos fazendo abaixo:

<resources>
<string name="app_name">TestesComponentes</string>

<string-array name="opcoes_array">
<item>Micilini Roll</item>
<item>Gabriel Solano</item>
<item>Toneleda Tias</item>
</string-array>

<string-array name="opcoes_valores">
<item>99</item>//Este item é destinado ao 'Micilini Roll'
<item>87</item>//Este item é destinado ao 'Gabriel Solano'
<item>89</item>//Este item é destinado a 'Toneleda Tias'
</string-array>

</resources>

Como veremos mais adiante, por meio de código sabemos qual índice do spinner foi selecionado, e com isso em mãos, podemos selecionar o item referênte no "opcoes_valores".

Veja como isso funciona na prática:

val spinnerEstatico = findViewById<View>(R.id.spinner_estatico) as Spinner//Seleciona o elemento Spinner pelo seu ID

val opcaoSelecionada = spinnerEstatico.selectedItem.toString()//Seleciona a opção que está selecionada no momento e retorna em formato de string

Vale ressaltar que a variável chamada opcaoSelecionada retorna a opção selecionada em formato de string, e não os valores que definimos em "opcoes_valores", ok?

Para selecionar o valor declarado no strings.xml, você pode executar o seguinte código:

val posicaoAtual: Int = spinnerEstatico.getSelectedItemPosition()//Seleciona a posição atual do spinner (Retorna um valor numérico)

val valoresDoSpinner = resources.getStringArray(R.array.opcoes_valores)//Retorna todos os itens existentes no 'opcoes_valoresdo string.xml

val opcaoSelecionadaValor = Integer.valueOf(valoresDoSpinner[posicaoAtual])//Retorna o valor numérico do item selecionado

Note que no código acima, nos retornamos a posição do item selecionado, recuperamos as opções de "opcoes_valores" e retornamos o valor do item selecionado juntando essas duas variáveis.

Identificando mudanças no Spinner

As vezes queremos identificar quando o usuário selecionou algum item do spinner.

E isso pode ser feito quando implementamos o onItemSelectedListener:

spinnerEstatico?.onItemSelectedListener = object : AdapterView.OnItemSelectedListener{
override fun onNothingSelected(parent: AdapterView<*>?) {
Log.d("onNothingSelected", "Nada Selecionado")
}
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
Log.d("onItemSelected", "Posição: $position, id: $id")
}
}

Note que dentro do método onItemSelected é retornado alguns parâmetros como a posição do elemento selecionado, junto com o id do mesmo.

Já o onNothingSelected é chamado quando o usuário abre e fecha a lista sem selecionar nada.

Retornando uma seleção de forma direta

Também é possível por meio de código, retornar de forma direta o item que está selecionado no Spinner, para isso basta executar:

val idItemSelecionado = spinnerEstatico.selectedItemId

val posicaoItemSelecionado = spinnerEstatico.selectedItemPositivon

Selecionar um item via código

Também é possível selecionar um item de um Spinner via código, para isso basta usar o método setSelection() informando o índice do item que você deseja selecionar:

spinnerEstatico.setSelecion(2)//Aqui estamos escolhendo o item existente no índice 2 do Spinner

Spinner Dinâmico

Como o nome já diz, ele é um spinner dinâmico, e isso significa que podemos controlar os itens via código da maneira como desejarmos.

Para declarar um spinner dinâmico, não precisa definir as entradas, nesse caso, basta apenas que você defina esse elemento da forma como estamos fazendo abaixo:

<Spinner
android:id="@+id/spinner_dinamico"
android:spinnerMode="dropdown"
android:dropDownVerticalOffset="50dp"
android:layout_width="match_parent"
android:layout_height="wrap_content" />

Falando agora sobre a estrutura de um spinner, nós temos o Layout do elemento em si, e os elementos (itens) que existirão dentro do Spinner.

E assim como acontece na RecyclerView, nós temos a camada de Adapter, que é responsável por fazer a conexão do Layout com os elementos.

No caso do Spinner estático, nós não precisamos definir umaa classe do Adapter, pois tudo isso foi gerenciado de forma automática pelo Android.

Já quando trabalhamos com dados dinâmicos, no momento atual, nós somos obrigados a fazer a configuração do Adapter manualmente.

Em primeiro lugar precisamos definir a nossa lista de strings que serão inseridas dentro do nosso Spinner:

val list = listOf<String>("Micilini Roll - Dinâmico", "Gabriel Solano - Dinâmico", "Toneleda Tias - Dinâmico")//Definimos a lista que será atribuida ao spinner_dinamico

Em seguida precisamos definir a classe do Adapter que irá receber os itens que serão inseridos junto com o Layout do Spinner:

val adapter = ArrayAdapter(this, android.R.layout.simple_spinner_dropdown_item, list)

android.R.layout.simple_spinner_dropdown_item: O Android por padrão conta com layouts pré-definidos que facilitam o nosso trabalho.

Você também pode usar o simple_spinner_item, que é uma lista mais enxuta e sem muitos espaços.

Veja que o último parâmetro passado foi o list, que representa a lista que criamos logo acima.

Como estamos usando o Layout do dropdown oferecido pelo sistema, é importante entender que ele espera um listOf.

Agora caso você tivesse criado um Layout personalizado, você poderia passar um ArrayOf, ListOf, MapOf ou outros tipos de parâmetros.

Após a criação do Adapter, chegou o momento de fazer as devidas conexões com nosso spinner_dinamico:

val spinnerDinamico = findViewById<View>(R.id.spinner_dinamico) as Spinner

spinnerDinamico.adapter = adapter

E pronto, ao executar seu aplicativo no emulador, você já será capaz de visualizar o Spinner criado dinamicamente.

Lembrando que podemos fazer o uso dos métodos e estratégias que vimos no Spinner Estático aqui também, ok?

Spinner Dinâmico (Layout Personalizado)

Supondo que agora você queira criar um layout personalizado para o seu Spinner, e que ele seja criado de forma dinâmica seguindo o padrão de Layout como é mostrada na figura abaixo:

Como fazemos isso?

Bem, no caso de um Spinner que contem um TextView e uma ImageView, precisamos fazer alguns passos a mais para que isso seja possível.

Antes de mais nada, você pode acessar o repositório que contém os arquivos desse spinner customizado, basta clicar aqui.

Criando os Layouts

Dentro do arquivo main_activity.xml que existe dentro da pasta res > layout.

Você vai precisar definir um novo elemento do tipo <Spinner>:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:gravity="center"
tools:context=".MainActivity">

<Spinner
android:id="@+id/spinner"
android:spinnerMode="dropdown"
android:dropDownVerticalOffset="50dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />

</LinearLayout>

Em seguida, crie dentro da pasta res > layout, um novo arquivo do tipo XML chamado de spinner_custom_layout.xml com os seguintes elementos:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">

<ImageView
android:id="@+id/imageView"
android:layout_width="50dp"
android:layout_height="50dp"
android:padding="5dp"
android:src="@mipmap/ic_launcher" />

<TextView
android:id="@+id/textView"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:paddingLeft="10dp"
android:paddingRight="10dp"
android:layout_gravity="center"
android:text="Custom Text"
android:textColor="#000" />

</LinearLayout>

Esse arquivo que você acabou de criar, futuramente ele será inflado diversas vezes dentro do elemento <Spinner>, de modo que conseguiremos alterar o texto do TextView e a imagem do ImageView.

Criando a nossa classe de Adapter

Anteriormente ficamos sabendo que a classe do Adapter, é responsável por ligar nosso Spinner com nossos itens, e que ela é relativamente fácil de ser implementada.

Só que no nosso caso, nós estamos trabalhando com um Spinner personalizado, que por sua vez, pede uma implementação personalizada também 😂

E isso quer dizer que vamos escrever mais códigos 🧐

Pois bem, na pasta principal do seu projeto - por quesitos de organização -, você vai precisar criar uma nova Package chamada spinner, onde dentro dela, você vai criar uma nova classe chamada CustomAdapter.kt:

Observe como essa está estruturara:

package com.example.testescomponentes.spinner

import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.BaseAdapter
import android.widget.ImageView
import android.widget.TextView
import com.example.testescomponentes.R

//Quando temos um Spinner customizado, devemos criar uma instância customizada de Adapter que herda de BaseAdapter
class CustomAdapter(internal var context: Context, internal var imagensFrutas: IntArray, internal var nomeFrutas: Array<String>) : BaseAdapter() {//Aqui estamos recebendo como parâmetro o contexto, junto com as imagens e o nome das frutas

internal var inflter: LayoutInflater//Cria o atributo que será necessário para Inflar o arquivo 'spinner_custom_layout.xmldentro do elemento Spinner existente na 'activity_main.xml'

init {
//Bloco de código que é executado quando a classe 'CustomAdapteré instanciada
inflter = LayoutInflater.from(context)//A primeira coisa que a gente faz é inflar o Layout com o contexto de MainActivity
}

//Retorna o número total de itens que devem ser carregados no Spinner
override fun getCount(): Int {
return imagensFrutas.size//Poderiamos usar tanto nomesFrutas.size quanto imagensFrutas.size que não alteraria em nada pois ambos possuem o mesmo tamanho
}

//Método que retorna o nome da fruta. Este método foi feito para ser usado fora da classe.
override fun getItem(i: Int): Any? {
return nomeFrutas[i]
}

//Método usado para retornar o id do item selecionado.
override fun getItemId(i: Int): Long {
return i.toLong()
}

//Para que usar os métodos getItem e getItemId se já não conseguimos fazer isso por meio do onItemSelected existente na MainActivity.kt?

/*
Resposta de um usuário do Stackoverflow: A finalidade ou uso do método depende principalmente do desenvolvedor e não está vinculado a um aplicativo baseado em banco de dados. use-o a seu favor para criar código claro/legível/reutilizável.
Ou seja, esses dois métodos atuam como funcionalidades opcionais que podemos usar caso queiramos uma outra maneira de selecionar um item ou um id.
*/

override fun getView(i: Int, view: View?, viewGroup: ViewGroup): View {

//É importante ressaltar que a função getView atua neste momento como uma especie de LOOP, e o número de laços será definido pelo retorno do método getCount.
//E é dessa forma que ele vai inflando e preenchendo os elementos do 'spinner_custom_layout.xmlcom seus respectivos valores.

val view = inflter.inflate(R.layout.spinner_custom_layout,null)//Primeiro ele infla o layout do arquivo 'spinner_custom_layout.xml'.

val icon = view.findViewById<View>(R.id.imageView) as ImageView?//Em seguida acessamos o elemento do tipo imagem.

val names = view.findViewById<View>(R.id.textView) as TextView?//Em seguida acessamos o elemento do tipo texto.

icon!!.setImageResource(imagensFrutas[i])//Aqui estamos setando a imagem existente no arquivo inflado com aquela que se encontra no nosso array de imagens.

names!!.text = nomeFrutas[i]//Aqui estamos setando o texto do arquivo inflado com aquela que se encontra no nosso array de nomes.

//Para saber mais como funciona as duplas exclamações acesse: https://micilini.com/conteudos/android/nullsafety-e-excecoes

return view//Retorna a View criada
}

}

No caso do código acima, tomei muito cuidado para detalhar o que cada comando faz e executa.

Sendo assim, pare um instante e tente entender o que cada parte daquela classe está fazendo, ok?

Conectando tudo na MainActivity

Indo agora para nosso arquivo MainActivity.kt, chegou o momento de conectarmos tudo isso dentro do nosso método onCreate, vamos nessa?

package com.example.testescomponentes

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity

import android.view.View
import android.widget.AdapterView
import android.widget.Spinner
import android.widget.Toast
import com.example.testescomponentes.spinner.CustomAdapter

class MainActivity : AppCompatActivity() {

internal var nomeFrutas = arrayOf("Uvas", "Manga", "Abacaxi", "Morango")
internal var imagensFrutas = intArrayOf(R.drawable.uva, R.drawable.manga, R.drawable.abacaxi, R.drawable.morango)


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

criaSpinnerPersonalizado()//Método que chama os comandos capazes de personalizar o Spinner com Layout de Imagens + Texto
}

fun criaSpinnerPersonalizado(){

//Seleciona a instancia de spinner existente dentro da nossa view (activity_main.xml)
val spinner = findViewById<View>(R.id.spinner) as Spinner

//Adiciona o método capaz de analisar todas as modificações que o usuário faz no Spinner
spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {

override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) {
//Quando o usuário seleciona uma determinada opção, é mostrado uma mensagem alegando a opção que ele escolheu
Toast.makeText(
this@MainActivity,
"Você selecionou a posição: " + position + ", cujo valor é: " + nomeFrutas[position],
Toast.LENGTH_SHORT
).show()
}

override fun onNothingSelected(parent: AdapterView<*>) {
//Quando o usuário não seleciona nenhuma opção, é mostrada uma outra mensagem
Toast.makeText(
this@MainActivity,
"Você não selecionou nenhuma opção",
Toast.LENGTH_SHORT
).show()
}

}

//Cria uma instância de CustomAdapter, enviando o contexto da aplicação principal juntamente com os atributos que armazenam os textos e o caminho das imagens
val customAdapter = CustomAdapter(applicationContext, imagensFrutas, nomeFrutas)

spinner.adapter = customAdapter//Faz a cola do Adapter com o Spinner setado na nossa view
}

}

Dentro do método onCreate eu chamei o método criaSpinnerPersonalizado(), que contém todos os códigos necessários para chamar a classe CustomAdapter enviando os atributos que contém a imagem das frutas e o nome das mesmas.

Para que de lá, a classe CustomCreate possa inflar aquele layout personalizado de modo a criar o nosso Spinner.

Conclusão

Neste conteúdo você aprendeu a criar um Spinner de três formas diferentes, um Spinner Estático, outro dinâmico e outro dinâmico com layout personalizado.

Até o próximo conteúdo.