Wybór jednokrotny- kontrolka Spinner
Kontrolka Spinner jest przeznaczona do szybkiego wyboru jednej wartości z zestawu. Przez zestaw rozumie się listę wartości (opcji) zapisanych w zbiorze, który może być pobrany z tablicy jednowymiarowej, kolekcji (listy), pliku zasobów XML itp.
Kontrolka Spinner w stanie domyślnym wyświetla jedną wybraną wartość na wzór kontrolki ComboBox kompilatora Visual Studio. Naciśnięcie kontrolki Spinner rozwija listę dostępnych wartości możliwych do wyboru przez użytkownika. W standardowym układzie widok rozwijalnej listy jest bardzo prosty. Patrz poniżej
Istnieje możliwość tworzenia niestandardowych widoków listy kontrolki Spinner.
W przypadku niestandardowej listy widoku wymagane jest utworzenie layout?u elementu listy oraz przygotowanie adaptera, który będzie obsługiwał nowy widok.
Układ głównego widoku aplikacji
Utworzymy projekt aplikacji, która wykorzystując kontrolkę Spinner pozwoli wykonać jedno z wybranych działań matematycznych. Poniżej propozycja układu widoku aplikacji. Układ zawiera trzy kontenery układu LinearLayout. Jeden z nich pracuje w trybie pionowym (vertical), a dwa w trybie poziomym (horizontal). Pozostałe kontrolki dobierz na podstawie poniższej ilustracji.
Zawartość pliku XML głównego układu widoku aplikacji
Wskazówka:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
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:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:orientation="vertical"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="60dp"
android:orientation="horizontal">
<TextView
android:id="@+id/textView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Wybierz działanie:"
android:textSize="24sp" />
<Spinner
android:id="@+id/spinner"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:layout_weight="1" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<EditText
android:id="@+id/editTextNumber1"
android:layout_width="59dp"
android:layout_height="48dp"
android:layout_marginRight="10dp"
android:layout_weight="1"
android:ems="10"
android:hint="Wprowadź liczbę"
android:inputType="numberDecimal" />
<EditText
android:id="@+id/editTextNumber2"
android:layout_width="79dp"
android:layout_height="48dp"
android:layout_marginRight="10dp"
android:layout_weight="1"
android:ems="10"
android:hint="Wprowadź liczbę"
android:inputType="numberDecimal" />
<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="Wynik"
android:textSize="20sp" />
</LinearLayout>
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="Oblicz" />
<Button
android:id="@+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="Ładuj niestandardowe" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
Wypełnienie kontrolki Spinner
Kontrolkę Spinner wypełnimy danymi zapisanymi w pliku zasobu res/ valuses/ strings.xml. Dane są zorganizowane w tablicy łańcuchów znaków.
Wskazówka:
<resources>
<string name="app_name">Wybór jednej wartości Spinner</string>
<string-array name="dzialania">
<item name="dodawanie">+ [dodawanie]</item>
<item name="odejmowanie">- [odejmowanie]</item>
<item name="mnozenie">* [mnożenie]</item>
<item name="dzielenie">/ [dzielenie]</item>
</string-array>
</resources>
Wczytanie powyższej tablicy opcji wymaga przygotowani prostego adaptera obsługi danych kontrolki Spinner. Wczytana tablica opcji utworzy zbiór elementów kontrolki, dla których standardowo można przypisać układ pracy kontrolki jako widok
Wskazówka:
android.R.layout.simple_spinner_item,
android.R.layout.simple_spinner_dropdown_item,
Kod funkcji wypełniającej zawartość kontrolki Spinner.
Wskazówka:
fun WypelnijSpinnera(){
val tabDzialan=resources.getStringArray(R.array.dzialania)
val adapter=ArrayAdapter<String>(
this,
//android.R.layout.simple_spinner_item,
android.R.layout.simple_spinner_dropdown_item,
tabDzialan
)
val spinner=findViewById<Spinner>(R.id.spinner)
spinner.adapter=adapter
//ustaw aktywne działanie na dzielenie
spinner.setSelection(3)
}
UWAGA. Podstawowe wypełnienie opcjami kontrolki Spinner można uzyskać szybciej i łatwiej wykorzystując metodę entries klasy android, wtóra jest wywoływana w pliku zasobów XML budowy widoku układu.
Wskazówka:
android:entries="@array/dzialania"
Utworzoną funkcję wywołamy w metodzie OnCreate()
Wskazówka:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
WypelnijSpinnera()
}
Skompiluj program i sprawdź efekt działania.
Prawidłowo działająca aplikacja pozwala rozwinąć zawartość kontrolki Spinner, ale wybór opcji nic nie potrafi nic wykonać.
Obsługa wyboru opcji kontrolki Spinner
Elementy opcji kontrolki są zorganizowane w postaci listy o indeksach liczonych od zera. Odczyt wybranej opcji można zapamiętać poprzez zapamiętanie aktywnego indeksu, na podstawie którego podejmie się dalsze działania.
Do kodu programu wprowadzimy zmienną o nazwie idWyboru
Wskazówka:
class MainActivity : AppCompatActivity() {
var idWyboru=-1;
override fun onCreate(savedInstanceState: Bundle?) {
Poprawna obsługa kontrolki Spinner wymaga obsłużenia zdarzenia onItemSelected oraz onNothingSelected, które są metodami słuchacza kliknięć onItemSelectedListener.
Po zainicjowaniu słuchacza kliknięć tworzenie kodu można sobie ułatwić, poprzez skorzystanie z podpowiedzi środowiska Android Studio. Wybór sekwencji klawiszy Alt+ Enter otworzy moduł podpowiedzi, z których skorzystamy przy implementacji funkcji obsługi kontrolki Spinner.
Kod funkcji obsługi opcji
Wskazówka:
fun ObslugaSpinera(){
val spinner=findViewById<Spinner>(R.id.spinner)
spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected (parent: AdapterView <*>, view: View ?, position: Int , id: Long ) {
idWyboru=position
}
override fun onNothingSelected (parent: AdapterView <*>) {
// Nic nie rób
}
}
}
Funkcję zapamiętującą wybór opcji wywołamy w metodzie onCreate(), w której jednocześnie zainicjujemy funkcje wykonującą wybrane działanie matematyczne po kliknięciu w kontrolkę Button. Argumentem funkcji obliczającej jest indeks wybranej opcji kontrolki Spinner.
Wskazówka:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
WypelnijSpinnera()
ObslugaSpinera()
val bt1=findViewById<Button>(R.id.button)
bt1.setOnClickListener {
Oblicz(idWyboru)
}
}
W tworzonej funkcji obliczającej zastosujemy proste zabezpieczenie dla przypadku niepodania danych, tak aby ustrzec się błędów konwersji danych. Przykładowy kod funkcji obliczającej
Wskazówka:
fun Oblicz(id: Int){
val aTxt=findViewById<EditText>(R.id.editTextNumber1)
val bTxt=findViewById<EditText>(R.id.editTextNumber2)
if(aTxt.text.toString()==""||bTxt.text.toString()=="")return
val a=aTxt.text.toString().toDouble()
val b=bTxt.text.toString().toDouble()
val wynik=findViewById<TextView>(R.id.textView2)
when(id){
0->wynik.setText((a+b).toString())
1->wynik.setText((a-b).toString())
2->wynik.setText((a*b).toString())
//przykład formatowania wyniku do trzech miejsc po przecinku
3->wynik.setText(String.format("%.3f",a/b))
}
}
Skompiluj program i sprawdź działanie.
Niestandardowy Spinner
Utworzymy Spinner o niestandardowym wyglądzie, którego elementy będą składać się z ikony działania oraz tekstu z nazwą działania matematycznego. W dowolnym programie graficznym przygotuj cztery ikony działań matematycznych i umieść je w folderze res/ drawable.
W folderze res/ layout utworzymy plik XML widoku elementu niestandardowego Spinner?a. Plik nazwiemy moj_spinner_layout.xml
Wskazówka:
<?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">
<ImageView
android:id="@+id/imgDzialanie"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_margin="4dp"
tools:srcCompat="@tools:sample/avatars" />
<TextView
android:id="@+id/txtDzialanie"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="4dp"
android:text="TextView"
android:textSize="16sp" />
</LinearLayout>
Do pliku res/ strings.xml dopiszemy tablice zasobów tworzonych elementów. Zauważ że odpowiednie tablice zawierają nazwę działania i odwołanie do ikony działania. Patrz kod poniżej
Wskazówka:
<resources>
<string name="app_name">Wybór jednej wartości Spinner</string>
<string-array name="dzialania">
<item name="dodawanie">+ [dodawanie]</item>
<item name="odejmowanie">- [odejmowanie]</item>
<item name="mnozenie">* [mnożenie]</item>
<item name="dzielenie">/ [dzielenie]</item>
</string-array>
<array name="TablicaDzialan">
<item>@array/plus</item>
<item>@array/minus</item>
<item>@array/razy</item>
<item>@array/dziel</item>
</array>
<array name="plus">
<item name="nazwa">Dodawanie</item>
<item name="ikona">@drawable/plus</item>
</array>
<array name="minus">
<item name="nazwa">Odejmowanie</item>
<item name="ikona">@drawable/minus</item>
</array>
<array name="razy">
<item name="nazwa">Mnożenie</item>
<item name="ikona">@drawable/razy</item>
</array>
<array name="dziel">
<item name="nazwa">Dzielenie</item>
<item name="ikona">@drawable/dziel</item>
</array>
</resources>
Tak utworzony zasób zostanie wczytany przez adapter, który musimy utworzyć jako osobną kalsę.
Adapter niestandardowego Spinner?a
Do projektu dodajemy nową klasę o nazwie MojAdapter.kt
Adapter niestandardowego widoku Spinner?a bazuje na klasach BaseAdapter(),SpinnerAdapter.
Ale zanim utworzymy adapter dopiszemy do zawartości pliku prostą klasę pośrednicząca z dwoma parametrami. Patrz poniżej.
Wskazówka:
class PrzekazywaneDane(val nazwaDzalania:String, val ikonaDzialania:Int)
Tę klasę zapiszemy na końcu utworzonego pliku.
Nagłówek klasy adaptera zapisz jak poniżej
Wskazówka:
class MojAdapterSpinnera(val context: Context, val zasobWidoku: Int,
val itemy: List<PrzekazywaneDane>) :
BaseAdapter(),SpinnerAdapter {
Na tę chwilę kompilator powinien zgłaszać błąd z propozycją jego naprawy. Brakuje implementacji wymaganych metod tworzonego adaptera. Implementację ułatwia system podpowiedzi Android Studio, z których skorzystamy po wyborze kombinacji klawiszy Alt + Enter
Zaimplementowane metody wypełnij kodem jak poniżej.
Wskazówka:
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.SpinnerAdapter
import android.widget.TextView
class MojAdapterSpinnera(val context: Context, val zasobWidoku: Int,
val itemy: List<PrzekazywaneDane>) :
BaseAdapter(),SpinnerAdapter {
//zwróć rozmiar listy elementów (itemów)
override fun getCount(): Int = itemy.size
//zwroc dane elementu z indeksu pozycji listy
override fun getItem(position: Int): Any = itemy[position]
//zwroć identyfikator elementu z listy
override fun getItemId(position: Int): Long = position.toLong()
//obsłuz ładowany układ widoku itemu
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val layout: LayoutInflater = LayoutInflater.from(context)
val view: View = layout.inflate(zasobWidoku, null)
val img: ImageView = view.findViewById(R.id.imgDzialanie)
val txtDzialanie: TextView = view.findViewById(R.id.txtDzialanie)
val dane: PrzekazywaneDane = itemy[position]
img.setImageDrawable(context.resources.getDrawable(dane.ikonaDzialania))
txtDzialanie.text = dane.nazwaDzalania
return view
}
}
//klasa posredniczaca w przekazywaniu danych do itemów adaptera
class PrzekazywaneDane(val nazwaDzalania:String, val ikonaDzialania:Int)
Metoda wypełnienia niestandardowego Spinner?a
Teraz możemy wypełnić elementami tworzony niestandardowy Spinner. W głównym pliku MainActivity.kt zapiszemy funkcje ładującą elementy. Funkcja ma za zadanie:
- odczytać tablicę danych z zasobów pliku XML,
- utworzyć listę działań,
- dodać do listy kolejne elementy tablicy działań,
- przekazać listę do niestandardowego adaptera
Kod funkcji
Wskazówka:
//ukryj komunikaty pozornego błędu indeksu
@SuppressLint("ResourceType")
fun LadujNiestandardowe(){
val tabDzialan=resources.obtainTypedArray(R.array.TablicaDzialan)
var listaDzialan= mutableListOf<PrzekazywaneDane>()
for(i in 0..tabDzialan.length()-1){
val tabBufor=resources.obtainTypedArray(tabDzialan.getResourceId(i,0))
listaDzialan.add(
PrzekazywaneDane(
tabBufor.getString(0).toString(),
tabBufor.getResourceId(1,R.drawable.plus)
)
)
}
val spinner=findViewById<Spinner>(R.id.spinner)
spinner.adapter=MojAdapterSpinnera(this,R.layout.moj_spinner_layout,listaDzialan)
}
Utworzoną funkcję wywołamy w zdarzeniu kliknięcia drugiej kontrolki Button
Wskazówka:
val bt2=findViewById<Button>(R.id.button2)
bt2.setOnClickListener {
LadujNiestandardowe()
}
Skompiluj program i sprawdź efekt działania.
Prawidłowo działająca aplikacja podmienia elementy Spiner?a. Aplikacja działa i wykonuje wcześniej zdefiniowane działania matematyczne.
Obsługa niestandardowego Spinner?a
Do aplikacji możemy dopisać funkcję, która pozwoli pokazać zalążek metod obsługi niestandardowego układu kontrolki Spinner. Funkcja pokazuje jak odczytać pozostałe właściwości elementów kontrolki poprzez odwołanie się do klasy pośredniczącej przy przekazywaniu danych do kontrolki Spinner. Poniżej proste wysłanie tych danych do okna komunikatu.
Wskazówka:
fun ObslugaSpineraNiestandardowaLista(context:Context){
val spinner=findViewById<Spinner>(R.id.spinner)
spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected (parent: AdapterView <*>,
view: View ?, position: Int , id: Long ) {
idWyboru=position
val wybranyItem=parent.getItemAtPosition(position)as PrzekazywaneDane
val oknoKomunikatu=AlertDialog.Builder(context)
oknoKomunikatu.setTitle("Wybrano działanie")
oknoKomunikatu.setMessage(wybranyItem.nazwaDzalania)
oknoKomunikatu.setIcon(wybranyItem.ikonaDzialania)
oknoKomunikatu.setPositiveButton("Zamknij",null)//{okno,_->okno.dismiss()}
oknoKomunikatu.create()
oknoKomunikatu.show()
}
override fun onNothingSelected (parent: AdapterView <*>) {
// Nic nie rób
}
}
}
Wywołanie tej funkcji dopiszemy do zdarzenia kliknięcia drugiej kontrolki Button
Wskazówka:
val bt2=findViewById<Button>(R.id.button2)
bt2.setOnClickListener {
LadujNiestandardowe()
ObslugaSpineraNiestandardowaLista(this)
}
Skompiluj program i sprawdź działanie aplikacji.
Pełny kod MainActivity.kt
Wskazówka:
import android.annotation.SuppressLint
import android.content.Context
import android.os.Bundle
import android.view.View
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.Button
import android.widget.EditText
import android.widget.Spinner
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
var idWyboru=-1;
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
WypelnijSpinnera()
ObslugaSpinera()
val bt1=findViewById<Button>(R.id.button)
bt1.setOnClickListener {
Oblicz(idWyboru)
}
val bt2=findViewById<Button>(R.id.button2)
bt2.setOnClickListener {
LadujNiestandardowe()
ObslugaSpineraNiestandardowaLista(this)
}
}
fun WypelnijSpinnera(){
val tabDzialan=resources.getStringArray(R.array.dzialania)
val adapter=ArrayAdapter<String>(
this,
//android.R.layout.simple_spinner_item,
android.R.layout.simple_spinner_dropdown_item,
tabDzialan
)
val spinner=findViewById<Spinner>(R.id.spinner)
spinner.adapter=adapter
//ustaw aktywne działanie na dzielenie
spinner.setSelection(3)
}
fun ObslugaSpinera(){
val spinner=findViewById<Spinner>(R.id.spinner)
spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected (parent: AdapterView <*>,
view: View ?, position: Int , id: Long ) {
idWyboru=position
}
override fun onNothingSelected (parent: AdapterView <*>) {
// Nic nie rób
}
}
}
fun Oblicz(id: Int){
val aTxt=findViewById<EditText>(R.id.editTextNumber1)
val bTxt=findViewById<EditText>(R.id.editTextNumber2)
if(aTxt.text.toString()==""||bTxt.text.toString()=="")return
val a=aTxt.text.toString().toDouble()
val b=bTxt.text.toString().toDouble()
val wynik=findViewById<TextView>(R.id.textView2)
when(id){
0->wynik.setText((a+b).toString())
1->wynik.setText((a-b).toString())
2->wynik.setText((a*b).toString())
//przykład formatowania wyniku do trzech miejsc po przecinku
3->wynik.setText(String.format("%.3f",a/b))
}
}
//ukry komunikaty pozornego błędu indeksu
@SuppressLint("ResourceType")
fun LadujNiestandardowe(){
val tabDzialan=resources.obtainTypedArray(R.array.TablicaDzialan)
var listaDzialan= mutableListOf<PrzekazywaneDane>()
for(i in 0..tabDzialan.length()-1){
val tabBufor=resources.obtainTypedArray(tabDzialan.getResourceId(i,0))
listaDzialan.add(
PrzekazywaneDane(
tabBufor.getString(0).toString(),
tabBufor.getResourceId(1,R.drawable.plus)
)
)
}
val spinner=findViewById<Spinner>(R.id.spinner)
spinner.adapter=MojAdapterSpinnera(this,R.layout.moj_spinner_layout,listaDzialan)
}
fun ObslugaSpineraNiestandardowaLista(context:Context){
val spinner=findViewById<Spinner>(R.id.spinner)
spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected (parent: AdapterView <*>,
view: View ?, position: Int , id: Long ) {
idWyboru=position
val wybranyItem=parent.getItemAtPosition(position)as PrzekazywaneDane
val oknoKomunikatu=AlertDialog.Builder(context)
oknoKomunikatu.setTitle("Wybrano działanie")
oknoKomunikatu.setMessage(wybranyItem.nazwaDzalania)
oknoKomunikatu.setIcon(wybranyItem.ikonaDzialania)
oknoKomunikatu.setPositiveButton("Zamknij",null)//{okno,_->okno.dismiss()}
oknoKomunikatu.create()
oknoKomunikatu.show()
}
override fun onNothingSelected (parent: AdapterView <*>) {
// Nic nie rób
}
}
}
}
Pełny kod MojAdapter.kt
Wskazówka:
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.SpinnerAdapter
import android.widget.TextView
class MojAdapterSpinnera(val context: Context, val zasobWidoku: Int,
val itemy: List<PrzekazywaneDane>) :
BaseAdapter(),SpinnerAdapter {
//zwróć rozmiar listy elementów (itemów)
override fun getCount(): Int = itemy.size
//zwroc dane elementu z indeksu pozycji listy
override fun getItem(position: Int): Any = itemy[position]
//zwroć identyfikator elementu z listy
override fun getItemId(position: Int): Long = position.toLong()
//obsłuz ładowany układ widoku itemu
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val layout: LayoutInflater = LayoutInflater.from(context)
val view: View = layout.inflate(zasobWidoku, null)
val img: ImageView = view.findViewById(R.id.imgDzialanie)
val txtDzialanie: TextView = view.findViewById(R.id.txtDzialanie)
val dane: PrzekazywaneDane = itemy[position]
img.setImageDrawable(context.resources.getDrawable(dane.ikonaDzialania))
txtDzialanie.text = dane.nazwaDzalania
return view
}
}
//klas posredniczaca w przekazywaniu danych do itemów adaptera
class PrzekazywaneDane(val nazwaDzalania:String, val ikonaDzialania:Int)