Przejdź do treści
School IT / Przestrzeń do nauki

school-it.pl // android + kotlin // rozdział 4 // lekcja 16

RatingBar — ocena w gwiazdkach

RatingBar to rząd gwiazdek, którymi użytkownik wystawia ocenę — jak w sklepie z aplikacjami czy przy recenzji filmu. Ta sama kontrolka może też tylko pokazywać ocenę, na przykład średnią z wielu głosów. W tej lekcji zbudujesz ankietę oceny szkolnej stołówki: trzy kategorie oceniane gwiazdkami i średnia pokazana w małych gwiazdkach tylko do odczytu.

Kotlin XML RatingBar numStars / stepSize rating isIndicator 1 × 45 min
CEL LEKCJI

Czego się dziś nauczysz

  • Ustawisz liczbę gwiazdek i krok oceny (całe lub połówki).
  • Odczytasz ocenę jako liczbę Float.
  • Zareagujesz na zmianę oceny.
  • Pokażesz ocenę tylko do odczytu w małych gwiazdkach.
  • Policzysz średnią z kilku ocen i zamienisz ją na opis słowny.
TEORIA

Gwiazdki w XML

pięć gwiazdek, ocena co pół gwiazdki
<RatingBar
    android:id="@+id/rbFilm"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:numStars="5"
    android:stepSize="0.5"
    android:rating="3.5" />
AtrybutZnaczenie
android:numStarsLiczba gwiazdek.
android:stepSizeKrok oceny: 1 — całe gwiazdki, 0.5 — połówki.
android:ratingOcena na starcie (0 = brak oceny).
android:isIndicator="true"Tylko do odczytu — palcem nie da się zmienić.
android:progressTintKolor wypełnionych gwiazdek.

Szerokość tylko wrap_content

Przy layout_width="match_parent" RatingBar dorysuje tyle gwiazdek, ile zmieści się na szerokości ekranu — numStars zostanie zignorowane. Szerokość ma być zawsze wrap_content.

TEORIA

Tylko do odczytu

Średnią ocen, ocenę z bazy czy ocenę innego użytkownika pokazujesz w RatingBar z isIndicator="true". Zwykle jest wtedy mniejszy — służy do tego gotowy styl:

małe gwiazdki tylko do odczytu
<RatingBar
    android:id="@+id/rbSrednia"
    style="?android:attr/ratingBarStyleSmall"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:isIndicator="true"
    android:numStars="5"
    android:stepSize="0.1" />
12:30
do oceniania, rating = 3.5
★★★★★
ratingBarStyleSmall, isIndicator, rating = 4.2
★★★★★ 4,2 (127 ocen)
progressTint=”#FFB300″
★★★★★
trzy odmiany RatingBar

krok 0.1 pozwala pokazać średnią dokładnie — gwiazdka wypełnia się częściowo

TEORIA

Ocena w kodzie

odczyt, zmiana i reakcja
val ocena = rbFilm.rating                 // Float, np. 3.5
rbFilm.rating = 0f                        // wyczyść ocenę

rbFilm.setOnRatingBarChangeListener { _, nowaOcena, odUzytkownika ->
    tvInfo.text = "Twoja ocena: $nowaOcena"
}

Ocena jest typu Float (liczba z częścią ułamkową) — nawet przy całych gwiazdkach to 4.0, a nie 4. Listener dostaje trzy wartości: kontrolkę, nową ocenę i informację, czy zmienił ją użytkownik (true), czy kod (false).

Ocena wyświetlona wprost wygląda jak „4.0”. Do napisu lepiej użyć String.format("%.1f", ocena) — da „4,0” z polskim przecinkiem — albo zamienić całe gwiazdki na liczbę całkowitą: ocena.toInt().

PRZYKŁAD

Ocena stołówki

Uczeń ocenia dzisiejszy obiad w trzech kategoriach: smak, wielkość porcji i obsługa (całe gwiazdki). Pod każdą kategorią pojawia się opis słowny. Na dole średnia w małych gwiazdkach tylko do odczytu i liczbowo. Wyślij jest aktywny dopiero po ocenieniu wszystkich trzech kategorii. Przycisk Wyczyść zeruje oceny.

12:40
Oceń dzisiejszy obiad
Smak
★★★★★
bardzo dobrze
Wielkość porcji
★★★★★
nie oceniono
Obsługa
★★★★★
rewelacja
Oceń wszystkie kategorie
Wyczyść
Wyślij
jedna kategoria bez oceny — Wyślij nieaktywny
12:41
Oceń dzisiejszy obiad
Smak
★★★★★
bardzo dobrze
Wielkość porcji
★★★★★
słabo
Obsługa
★★★★★
rewelacja
Średnia: ★★★★★ 3,7
Wyczyść
Wyślij
wszystko ocenione — średnia (4 + 2 + 5) / 3

Przygotowanie projektu

  1. Nowy projekt Empty Views Activity, nazwa OcenaStolowki, Kotlin, API 24.
  2. Podmień activity_main.xml i MainActivity.kt.
res/layout/activity_main.xml — pełny plik
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:padding="16dp">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Oceń dzisiejszy obiad"
            android:textSize="24sp"
            android:textStyle="bold" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="16dp"
            android:text="Smak"
            android:textStyle="bold" />
        <RatingBar
            android:id="@+id/rbSmak"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:numStars="5"
            android:stepSize="1" />
        <TextView
            android:id="@+id/tvSmak"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="12dp"
            android:text="Wielkość porcji"
            android:textStyle="bold" />
        <RatingBar
            android:id="@+id/rbPorcja"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:numStars="5"
            android:stepSize="1" />
        <TextView
            android:id="@+id/tvPorcja"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="12dp"
            android:text="Obsługa"
            android:textStyle="bold" />
        <RatingBar
            android:id="@+id/rbObsluga"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:numStars="5"
            android:stepSize="1" />
        <TextView
            android:id="@+id/tvObsluga"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />

        <!-- średnia: małe gwiazdki tylko do odczytu + liczba -->
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="16dp"
            android:gravity="center_vertical"
            android:orientation="horizontal">

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Średnia: "
                android:textSize="18sp" />

            <RatingBar
                android:id="@+id/rbSrednia"
                style="?android:attr/ratingBarStyleSmall"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:isIndicator="true"
                android:numStars="5"
                android:stepSize="0.1" />

            <TextView
                android:id="@+id/tvSrednia"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginStart="8dp"
                android:textSize="18sp"
                android:textStyle="bold" />
        </LinearLayout>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="16dp"
            android:orientation="horizontal">

            <Button
                android:id="@+id/btnWyczysc"
                style="@style/Widget.Material3.Button.OutlinedButton"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:layout_marginEnd="8dp"
                android:text="Wyczyść" />

            <Button
                android:id="@+id/btnWyslij"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:enabled="false"
                android:text="Wyślij" />
        </LinearLayout>

        <TextView
            android:id="@+id/tvPotwierdzenie"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="8dp"
            android:textColor="#2E7D32" />
    </LinearLayout>
</ScrollView>
MainActivity.kt — pełny plik
package pl.schoolit.ocenastolowki

import android.os.Bundle
import android.widget.Button
import android.widget.RatingBar
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {

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

        val rbSmak = findViewById<RatingBar>(R.id.rbSmak)
        val rbPorcja = findViewById<RatingBar>(R.id.rbPorcja)
        val rbObsluga = findViewById<RatingBar>(R.id.rbObsluga)
        val rbSrednia = findViewById<RatingBar>(R.id.rbSrednia)
        val tvSmak = findViewById<TextView>(R.id.tvSmak)
        val tvPorcja = findViewById<TextView>(R.id.tvPorcja)
        val tvObsluga = findViewById<TextView>(R.id.tvObsluga)
        val tvSrednia = findViewById<TextView>(R.id.tvSrednia)
        val tvPotwierdzenie = findViewById<TextView>(R.id.tvPotwierdzenie)
        val btnWyczysc = findViewById<Button>(R.id.btnWyczysc)
        val btnWyslij = findViewById<Button>(R.id.btnWyslij)

        // ocena 0–5 → opis; indeks tablicy = liczba gwiazdek
        val opisy = arrayOf("nie oceniono", "bardzo słabo", "słabo", "przeciętnie", "bardzo dobrze", "rewelacja")

        fun przelicz() {
            tvPotwierdzenie.text = ""
            tvSmak.text = opisy[rbSmak.rating.toInt()]
            tvPorcja.text = opisy[rbPorcja.rating.toInt()]
            tvObsluga.text = opisy[rbObsluga.rating.toInt()]

            val wszystkieOcenione = rbSmak.rating > 0 && rbPorcja.rating > 0 && rbObsluga.rating > 0
            btnWyslij.isEnabled = wszystkieOcenione

            if (wszystkieOcenione) {
                val srednia = (rbSmak.rating + rbPorcja.rating + rbObsluga.rating) / 3
                rbSrednia.rating = srednia
                tvSrednia.text = String.format("%.1f", srednia)
            } else {
                rbSrednia.rating = 0f
                tvSrednia.text = "oceń wszystko"
            }
        }

        przelicz()

        rbSmak.setOnRatingBarChangeListener { _, _, _ -> przelicz() }
        rbPorcja.setOnRatingBarChangeListener { _, _, _ -> przelicz() }
        rbObsluga.setOnRatingBarChangeListener { _, _, _ -> przelicz() }

        btnWyczysc.setOnClickListener {
            rbSmak.rating = 0f
            rbPorcja.rating = 0f
            rbObsluga.rating = 0f
        }

        btnWyslij.setOnClickListener {
            tvPotwierdzenie.text = "Dziękujemy! Twoja średnia: ${tvSrednia.text}"
        }
    }
}

Co dzieje się po kolei

  • Trzy oceniające RatingBar mają krok 1 — tylko całe gwiazdki. Na starcie rating wynosi 0, czyli „nie oceniono”.
  • rating.toInt() zamienia ocenę 4.0 na liczbę 4 — indeks w tablicy opisy. Pozycja 0 to „nie oceniono”, dlatego tablica ma 6 elementów.
  • przelicz() sprawdza, czy wszystkie oceny są większe od 0. Tylko wtedy liczy średnią i włącza Wyślij.
  • Średnia (np. 3,67) trafia do małego RatingBar z krokiem 0.1 — gwiazdka wypełnia się częściowo.
  • Wyczyść ustawia oceny na 0f. Listener reaguje także na zmiany z kodu, więc opisy i średnia odświeżają się same.

Sprawdź trzy rzeczy

  1. Przesuń palcem po gwiazdkach zamiast w nie stukać. Ocena zmienia się w trakcie przesuwania.
  2. Dotknij małych gwiazdek średniej — nic się nie dzieje, bo mają isIndicator="true".
  3. Zmień w rbSmak szerokość na match_parent. Gwiazdek będzie więcej niż pięć.
ELEMENTY WBUDOWANE

Zestawienie elementów

ElementTypCo robi
<RatingBar>kontrolkaOcena w gwiazdkach.
android:numStars, stepSize, ratingatrybutyLiczba gwiazdek, krok, ocena startowa.
android:isIndicator="true"atrybutTylko do odczytu.
style="?android:attr/ratingBarStyleSmall"stylMałe gwiazdki.
rb.ratingFloatOdczyt i ustawienie oceny.
rb.setOnRatingBarChangeListener { _, ocena, odUzytkownika -> }metodaKod po zmianie oceny.
liczba.toInt()IntLiczba całkowita (obcina część ułamkową).
0fFloatZero typu Float.
CZĘSTE BŁĘDY

Zanim utkniesz

Zapis / objawProblem
więcej gwiazdek niż numStarsSzerokość match_parent lub waga. Ustaw wrap_content.
rb.rating = 3Błąd kompilacji: oczekiwany Float. Pisz 3f.
„Twoja ocena: 4.0” zamiast „4”Ocena to Float. Użyj toInt() albo String.format.
średnia pokazuje się w całych gwiazdkachWskaźnik ma domyślny krok. Ustaw stepSize="0.1".
użytkownik może zmienić średniąBrak isIndicator="true".
opisy[rb.rating.toInt()] z tablicą 5-elementowąPrzy 5 gwiazdkach awaria ArrayIndexOutOfBoundsException — potrzebne miejsce także na 0.
ZADANIA

Zadania

Każde zadanie wykonaj w nowym projekcie Empty Views Activity. Cały kod pisz w onCreate. Wskazówkę otwórz dopiero wtedy, gdy utkniesz.

ZAD 1Ocena filmu★☆☆

Tytuł filmu, RatingBar z połówkami gwiazdek i napis „Twoja ocena: 3,5 / 5”, zmieniający się przy każdej zmianie. Ocena 5 dodatkowo pokazuje „Arcydzieło!”.

pokaż wskazówkę
  • stepSize="0.5", napis: String.format("Twoja ocena: %.1f / 5", ocena).
ZAD 2Ocena szkolna★☆☆

RatingBar z sześcioma gwiazdkami jako ocena szkolna 1–6. Pod spodem słownie („dopuszczający”, „dostateczny”…) i kolor napisu zależny od oceny. Ocena 0 jest niedozwolona — po wyczyszczeniu gwiazdek ustaw 1.

pokaż wskazówkę
  • W listenerze: if (ocena < 1f) rb.rating = 1f. Ta zmiana wywoła listener jeszcze raz — tym razem z oceną 1.
ZAD 3Średnia z wielu głosów★★☆

Ocena aplikacji: duże gwiazdki do głosowania i przycisk Oddaj głos. Każdy głos dolicza się do średniej pokazanej w małych gwiazdkach i liczbowo („4,3 · 12 głosów”). Po oddaniu głosu duże gwiazdki się zerują.

pokaż wskazówkę
  • Zapamiętaj sumę ocen (var suma = 0f) i liczbę głosów. Średnia: suma / glosy.
ZAD 4Poziom trudności★★☆

Ekran ustawień gry: RatingBar z trzema gwiazdkami (całe) jako poziom trudności. Zależnie od poziomu zmienia się opis („łatwy: 5 żyć, wolni przeciwnicy”…) i kolor gwiazdek (zielony, pomarańczowy, czerwony).

pokaż wskazówkę
  • Kolor z kodu: rb.progressTintList = ColorStateList.valueOf(Color.parseColor("#…")) (import android.content.res.ColorStateList).
ZAD 5Ranking trzech produktów★★☆

Trzy słuchawki z nazwą i ceną, każde z własnymi gwiazdkami. Pod spodem automatyczny ranking: „1. Model B (4,5) 2. Model A (3,0) 3. Model C (2,5)”, aktualizowany po każdej zmianie oceny. Produkty bez oceny nie biorą udziału w rankingu.

pokaż wskazówkę
  • Zbuduj listę par mutableListOf(Pair("Model A", rbA.rating), …), odfiltruj zera i posortuj: sortedByDescending { it.second }.
ZAD 6Recenzja z warunkami★★★

Formularz recenzji: gwiazdki (połówki), pole „Tytuł” i pole „Treść” (wielowierszowe). Reguły: ocena co najmniej 0,5; przy ocenie ≤ 2 treść musi mieć co najmniej 30 znaków (pod polem licznik „12 / 30”); tytuł 3–40 znaków. Opublikuj jest aktywny tylko przy spełnionych regułach, a pod formularzem pojawia się podgląd recenzji z gwiazdkami tylko do odczytu.

pokaż wskazówkę
  • Na zmianę tekstu w polu reaguje et.addTextChangedListener { … } (import androidx.core.widget.addTextChangedListener). Wywołuj z niego tę samą funkcję sprawdzającą co z gwiazdek.
PODSUMOWANIE

Co trzeba zapamiętać

  • RatingBar: numStars — ile gwiazdek, stepSize — krok, szerokość zawsze wrap_content.
  • Ocena to Float: odczyt rb.rating, zapis rb.rating = 3f.
  • Zmianę obsługuje setOnRatingBarChangeListener — także zmianę z kodu.
  • Ocenę tylko do pokazania wyświetlaj z isIndicator="true" i stylem ratingBarStyleSmall.
  • Średnią pokazuj z krokiem 0.1, żeby gwiazdka wypełniała się częściowo.

Dokumentacja: RatingBar.

Postęp zapisuje się w tej przeglądarce.