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

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

ProgressBar — pasek postępu

ProgressBar pokazuje, że coś trwa: pasek, który się zapełnia (wiadomo, ile zostało), albo kręcące się kółko (nie wiadomo, ile to potrwa). W odróżnieniu od suwaka użytkownik go nie przesuwa — postęp ustawia kod. W tej lekcji poznasz oba rodzaje i zasymulujesz pobieranie pliku z licznikiem czasu CountDownTimer, który co chwilę przesuwa pasek.

Kotlin XML ProgressBar progressBarStyleHorizontal isVisible CountDownTimer 1 × 45 min
CEL LEKCJI

Czego się dziś nauczysz

  • Dodasz poziomy pasek postępu i kółko oczekiwania.
  • Ustawisz postęp z kodu i obliczysz procent wykonania.
  • Pokażesz i ukryjesz kółko w trakcie pracy.
  • Uruchomisz i zatrzymasz licznik czasu CountDownTimer.
  • Zablokujesz przyciski, które w trakcie pracy nie mają sensu.
TEORIA

Pasek i kółko

Ta sama kontrolka ProgressBar ma dwa wyglądy. Domyślnie to kręcące się kółko. Poziomy pasek wymaga stylu:

dwa rodzaje ProgressBar
<!-- poziomy pasek: postęp 0–100 -->
<ProgressBar
    android:id="@+id/pbPasek"
    style="?android:attr/progressBarStyleHorizontal"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:max="100"
    android:progress="40" />

<!-- kółko: kręci się bez końca -->
<ProgressBar
    android:id="@+id/pbKolko"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" />
12:30
pasek, progress = 40
pasek z secondaryProgress (np. bufor wideo)
kółko (indeterminate)
rodzaje ProgressBar
RodzajKiedyPrzykład
pasek (określony)Wiadomo, ile pracy zostało.pobieranie pliku, kroki formularza, poziom baterii
kółko (nieokreślony)Nie wiadomo, jak długo to potrwa.łączenie z serwerem, logowanie

Poziomy pasek też może być nieokreślony — z atrybutem android:indeterminate="true" przesuwa się po nim pasmo bez końca. Atrybut android:secondaryProgress rysuje drugi, jaśniejszy poziom — w odtwarzaczach wideo pokazuje, ile filmu już się załadowało.

Wskaźniki z biblioteki Material

Biblioteka Material ma nowsze kontrolki LinearProgressIndicator i CircularProgressIndicator — wyglądają nowocześniej, a w kodzie działają podobnie (też progress i max). W kursie zostajemy przy ProgressBar.

TEORIA

Postęp z kodu

postęp: 3 z 8 zadań
val wykonane = 3
val wszystkie = 8

pbPasek.max = wszystkie            // pasek liczy w zadaniach, nie w procentach
pbPasek.progress = wykonane
tvInfo.text = "Wykonano ${wykonane * 100 / wszystkie}%"    // 37%

pbKolko.isVisible = true           // pokaż kółko
pbKolko.isVisible = false          // schowaj kółko

Nie trzeba przeliczać na procenty — wystarczy ustawić max na liczbę wszystkich kroków. Procent potrzebny jest tylko do napisu. Kolejność w obliczeniu jest ważna: wykonane * 100 / wszystkie daje 37, a wykonane / wszystkie * 100 daje 0, bo dzielenie liczb całkowitych 3 / 8 obcina wynik do zera.

Kółko kręci się zawsze, gdy jest widoczne — sterujesz nim tylko przez isVisible (import androidx.core.view.isVisible).

TEORIA

Upływ czasu: CountDownTimer

Do symulowania długiej pracy potrzebny jest kod, który wykona się wiele razy w odstępach czasu. CountDownTimer to minutnik: odlicza podany czas i co ustalony odstęp wywołuje onTick, a na końcu onFinish:

minutnik 5 sekund, krok 0,1 s
val minutnik = object : CountDownTimer(5000, 100) {   // czas całkowity i odstęp w milisekundach

    override fun onTick(pozostalo: Long) {             // co 100 ms
        tvInfo.text = "Zostało ${pozostalo / 1000} s"
    }

    override fun onFinish() {                          // po 5 sekundach
        tvInfo.text = "Koniec!"
    }
}
minutnik.start()          // uruchomienie
minutnik.cancel()         // zatrzymanie przed końcem — onFinish się nie wywoła

Nie zatrzymuj aplikacji pętlą

Kusi zapis for (i in 1..100) { pasek.progress = i; Thread.sleep(50) }. To zamrozi cały ekran na 5 sekund — pasek nie odświeży się ani razu, a po kilku sekundach Android pokaże okno „Aplikacja nie odpowiada”. Ekran rysuje się dopiero, gdy twój kod skończy działanie. CountDownTimer nie blokuje ekranu: twój kod wykonuje się krótko co 100 ms, a pomiędzy nimi Android ma czas na rysowanie.

PRZYKŁAD

Pobieranie pliku

Symulacja pobierania materiałów z lekcji. Pobierz zeruje pasek i uruchamia 5-sekundowy minutnik, który przesuwa pasek i napis z procentami; w tym czasie obok kręci się kółko, a Pobierz jest zablokowany. Anuluj zatrzymuje pobieranie w połowie. Po zakończeniu pojawia się nazwa pliku.

14:00
Materiały z lekcji
lekcja-15.pdf · 2,4 MB
0%
Gotowe do pobrania
Anuluj
Pobierz
przed pobraniem
14:00
Materiały z lekcji
lekcja-15.pdf · 2,4 MB
62% · 1,5 z 2,4 MB
Pobieranie…
Anuluj
Pobierz
w trakcie — kółko się kręci
14:00
Materiały z lekcji
lekcja-15.pdf · 2,4 MB
100% · 2,4 z 2,4 MB
Pobrano: lekcja-15.pdf
Anuluj
Pobierz
po 5 sekundach

Przygotowanie projektu

  1. Nowy projekt Empty Views Activity, nazwa Pobieranie, Kotlin, API 24.
  2. Podmień activity_main.xml i MainActivity.kt. Nic nie jest naprawdę pobierane — upływ czasu udaje CountDownTimer.
res/layout/activity_main.xml — pełny plik
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:layout_margin="16dp"
    android:fitsSystemWindows="true">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Materiały z lekcji"
        android:textSize="24sp"
        android:textStyle="bold" />

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="lekcja-15.pdf · 2,4 MB"
        android:textSize="16sp" />

    <ProgressBar
        android:id="@+id/pbPostep"
        style="?android:attr/progressBarStyleHorizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="24dp"
        android:max="100"
        android:progress="0" />

    <!-- wiersz: napis z procentami i małe kółko -->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center_vertical"
        android:orientation="horizontal">

        <TextView
            android:id="@+id/tvProcent"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="0%"
            android:textSize="16sp" />

        <ProgressBar
            android:id="@+id/pbKolko"
            android:layout_width="32dp"
            android:layout_height="32dp"
            android:visibility="gone" />
    </LinearLayout>

    <TextView
        android:id="@+id/tvStan"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp"
        android:text="Gotowe do pobrania"
        android:textSize="16sp" />

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

        <Button
            android:id="@+id/btnAnuluj"
            style="@style/Widget.Material3.Button.OutlinedButton"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:layout_marginEnd="8dp"
            android:enabled="false"
            android:text="Anuluj" />

        <Button
            android:id="@+id/btnPobierz"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="Pobierz" />
    </LinearLayout>

</LinearLayout>
MainActivity.kt — pełny plik
package pl.schoolit.pobieranie

import android.graphics.Color
import android.os.Bundle
import android.os.CountDownTimer
import android.widget.Button
import android.widget.ProgressBar
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible

class MainActivity : AppCompatActivity() {

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

        val pbPostep = findViewById<ProgressBar>(R.id.pbPostep)
        val pbKolko = findViewById<ProgressBar>(R.id.pbKolko)
        val tvProcent = findViewById<TextView>(R.id.tvProcent)
        val tvStan = findViewById<TextView>(R.id.tvStan)
        val btnPobierz = findViewById<Button>(R.id.btnPobierz)
        val btnAnuluj = findViewById<Button>(R.id.btnAnuluj)

        val czasPobierania = 5000L          // 5 s w milisekundach (L = liczba typu Long)
        val rozmiarMB = 2.4

        var minutnik: CountDownTimer? = null    // null = nic się nie pobiera

        // ustawia przyciski i kółko zależnie od tego, czy trwa pobieranie
        fun ustawPrzyciski(trwa: Boolean) {
            btnPobierz.isEnabled = !trwa
            btnAnuluj.isEnabled = trwa
            pbKolko.isVisible = trwa
        }

        btnPobierz.setOnClickListener {
            pbPostep.progress = 0
            tvStan.text = "Pobieranie…"
            tvStan.setTextColor(Color.BLACK)
            ustawPrzyciski(true)

            minutnik = object : CountDownTimer(czasPobierania, 100) {

                override fun onTick(pozostalo: Long) {
                    val uplynelo = czasPobierania - pozostalo
                    val procent = (uplynelo * 100 / czasPobierania).toInt()
                    pbPostep.progress = procent
                    val pobrane = rozmiarMB * procent / 100
                    tvProcent.text = String.format("%d%% · %.1f z %.1f MB", procent, pobrane, rozmiarMB)
                }

                override fun onFinish() {
                    pbPostep.progress = 100
                    tvProcent.text = String.format("100%% · %.1f z %.1f MB", rozmiarMB, rozmiarMB)
                    tvStan.text = "Pobrano: lekcja-15.pdf"
                    tvStan.setTextColor(Color.parseColor("#2E7D32"))
                    ustawPrzyciski(false)
                    minutnik = null
                }
            }.start()
        }

        btnAnuluj.setOnClickListener {
            minutnik?.cancel()               // ?. — wywołaj tylko, gdy minutnik nie jest null
            minutnik = null
            tvStan.text = "Anulowano przy ${pbPostep.progress}%"
            tvStan.setTextColor(Color.parseColor("#C62828"))
            ustawPrzyciski(false)
        }
    }
}

Co dzieje się po kolei

  • Kółko ma w XML visibility="gone" — na starcie nie ma go wcale. Pokazuje je ustawPrzyciski(true).
  • Minutnik zapisujemy w zmiennej minutnik typu CountDownTimer?, żeby Anuluj mógł go zatrzymać. Gdy nic się nie pobiera, zmienna ma wartość null.
  • onTick dostaje liczbę milisekund, które zostały. Odejmując ją od czasu całkowitego, dostajemy czas, który upłynął, a z niego procent.
  • W String.format znak %% oznacza zwykły znak procentu — samo % jest początkiem miejsca na wartość. %.1f to liczba z jednym miejscem po przecinku.
  • onFinish ustawia równe 100% — ostatni onTick mógł wypaść na przykład przy 98%.
  • minutnik?.cancel() zatrzymuje minutnik. Operator ?. wywołuje metodę tylko wtedy, gdy zmienna nie jest null.

Minutnik działa dalej po zamknięciu ekranu

Gdy zamkniesz aplikację w trakcie pobierania, minutnik będzie dalej wywoływał onTick. W tym przykładzie nic złego się nie stanie, ale w większych aplikacjach minutnik zatrzymuje się w metodzie onDestroy — poznasz ją w lekcji 36.

Sprawdź trzy rzeczy

  1. Kliknij Pobierz, a w połowie Anuluj. Pasek zatrzyma się, a napis pokaże, przy ilu procentach przerwano. Ponowne Pobierz zaczyna od zera.
  2. Zmień w obliczeniu procentu kolejność na uplynelo / czasPobierania * 100. Pasek stanie na zerze aż do końca — dzielenie liczb całkowitych.
  3. Zmień odstęp minutnika z 100 na 1000. Pasek będzie skakał co sekundę o 20%.
ELEMENTY WBUDOWANE

Zestawienie elementów

ElementTypCo robi
<ProgressBar>kontrolkaBez stylu: kółko oczekiwania.
style="?android:attr/progressBarStyleHorizontal"stylPoziomy pasek postępu.
android:max, progress, secondaryProgressatrybutyZakres, postęp, drugi poziom.
android:indeterminate="true"atrybutPasek bez określonego postępu.
android:visibility="gone"atrybutKontrolka ukryta bez zajmowania miejsca.
pb.progress, pb.maxIntPostęp i zakres z kodu.
view.isVisibleBooleanPokazanie / ukrycie.
object : CountDownTimer(czas, odstep) { … }obiektMinutnik z onTick i onFinish.
.start(), .cancel()metodyUruchomienie i zatrzymanie minutnika.
zmienna?.metoda()operatorWywołanie tylko, gdy nie null.
%%, %.1f w String.formatwzorzecZnak %, liczba z jednym miejscem po przecinku.
5000LLongLiczba typu Long (czas w milisekundach).
CZĘSTE BŁĘDY

Zanim utkniesz

Zapis / objawProblem
zamiast paska kręci się kółkoBrak style="?android:attr/progressBarStyleHorizontal".
pasek stoi na 0, choć postęp rośniea / b * 100 na liczbach całkowitych daje 0. Najpierw mnóż: a * 100 / b.
Thread.sleep w pętliEkran zamarza, potem komunikat „Aplikacja nie odpowiada”. Użyj CountDownTimer.
dwa kliknięcia „Pobierz” — pasek skaczeDziałają dwa minutniki naraz. Blokuj przycisk na czas pracy.
String.format("%d%", p)Awaria UnknownFormatConversionException. Znak procentu we wzorcu: %%.
„Anuluj” nie działaMinutnik nie został zapamiętany w zmiennej — nie ma czego zatrzymać.
visibility="invisible" zamiast goneKontrolka niewidoczna, ale zajmuje miejsce — zostaje dziura w układzie.
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 1Szklanki wody★☆☆

Cel: 8 szklanek wody dziennie. Pasek z max="8", przyciski +1 szklanka i Cofnij, napis „5 / 8 (62%)”. Po osiągnięciu celu napis „Cel osiągnięty!” i zablokowany przycisk dodawania.

pokaż wskazówkę
  • Bez minutnika — postęp zmienia przycisk. Jedna funkcja pokaz() jak w lekcji 05.
ZAD 2Minutnik do jajek★☆☆

Trzy przyciski: Na miękko (4 min), Średnio (6 min), Na twardo (9 min). Pasek pokazuje, ile czasu upłynęło, napis — ile zostało w formacie „3:25”. Do testów skróć czasy 10-krotnie.

pokaż wskazówkę
  • Minuty i sekundy: val s = pozostalo / 1000, potem String.format("%d:%02d", s / 60, s % 60).
ZAD 3Logowanie z kółkiem★★☆

Pola Login i Hasło oraz przycisk Zaloguj. Po kliknięciu przez 2 sekundy kręci się kółko, pola i przycisk są zablokowane, a potem pojawia się „Witaj, admin!” (login „admin”, hasło „1234”) albo „Błędne dane”.

pokaż wskazówkę
  • Minutnik z pustym onTick — liczy się tylko onFinish. Pola blokujesz przez isEnabled = false jak przyciski.
ZAD 4Formularz w krokach★★☆

Formularz rejestracji w czterech krokach, w których widać tylko jedną grupę pól naraz (imię i nazwisko / e-mail / hasło / zgody). Na górze pasek „Krok 2 z 4”, na dole przyciski Wstecz i Dalej (w ostatnim kroku: Zakończ). Dalej działa tylko po wypełnieniu pól bieżącego kroku.

pokaż wskazówkę
  • Każdy krok to LinearLayout; pokazuj jeden przez isVisible. Krok trzymaj w var krok = 1, a pasek ma max="4".
ZAD 5Instalacja z etapami★★☆

Symulacja instalacji gry trwającej 10 s. Pasek pokazuje postęp, a napis zmienia się w zależności od etapu: „Pobieranie…” (0–49%), „Rozpakowywanie…” (50–89%), „Konfiguracja…” (90–99%), „Gotowe!”. Pasek ma też secondaryProgress wyprzedzający główny postęp o 15 punktów (ale nie więcej niż 100).

pokaż wskazówkę
  • Etap wybierz instrukcją when (procent) { in 0..49 -> … }. Drugi poziom: pb.secondaryProgress = minOf(procent + 15, 100).
ZAD 6Szybkie klikanie★★★

Gra: kliknij przycisk jak najwięcej razy w 10 sekund. Pasek czasu maleje od 100 do 0, napis pokazuje liczbę kliknięć i aktualne tempo („6,2 kliknięć/s”). Po czasie przycisk się blokuje, pojawia się wynik i rekord (najlepszy wynik od uruchomienia aplikacji). Od nowa zaczyna kolejną rundę.

pokaż wskazówkę
  • Pasek malejący: progress = (pozostalo * 100 / czas).toInt(). Tempo: kliknięcia podzielone przez sekundy, które upłynęły (uważaj na dzielenie przez zero w pierwszej chwili).
PODSUMOWANIE

Co trzeba zapamiętać

  • ProgressBar bez stylu to kółko; ze stylem progressBarStyleHorizontal — pasek.
  • Pasek: max = liczba kroków, progress = wykonane. Procent licz jako a * 100 / b.
  • Kółko pokazujesz i chowasz przez isVisible.
  • Upływ czasu symuluj przez CountDownTimer, nigdy przez Thread.sleep.
  • W trakcie pracy blokuj przyciski, które mogłyby ją uruchomić drugi raz.

Dokumentacja: ProgressBar, CountDownTimer.

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