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

Spinner Kotlin Android Studio

Istnieje możliwość tworzenia niestandardowych widoków listy kontrolki Spinner.

niestandardowy Spinner Kotlin Android Studio

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.

layout Spinner Kotlin Android Studio

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.

kontrolka Spinner Kotlin Android Studio

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.

obsługa Spinner Kotlin Android Studio

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.

obsługa Spinner Kotlin Android Studio

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.

niestandardowy Spinner Kotlin Android Studio

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

niestandardowy Spinner Kotlin Android Studio

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

adapter niestandardowy Spinner Kotlin Android Studio

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:

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.

adapter niestandardowy Spinner Kotlin Android Studio

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.

adapter niestandardowy Spinner Kotlin Android Studio

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)
Układ okresowy- kod qr
Układ okresowy

Układ okresowy pierwiastków- darmowa aplikacja na Androida

Pobierz ze sklepu Google Play
Alkomat- wirtualny test kod qr
Alkomat- wirtualny test

Alkomat- darmowa aplikacja na Androida

Pobierz ze sklepu Google Play
Taklarz- olinowanie stałe kod qr
Olinowanie stałe- kalkulator średnic

Olinowanie stałe- darmowa aplikacja na Androida

Pobierz ze sklepu Google Play
przepis na gogfry

Przepis na gofry

zobacz
przepis na bitą śmietanę

Przepis na bitą śmietanę

zobacz