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.
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.
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:
<!-- 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" />
| Rodzaj | Kiedy | Przykł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.
Postęp z kodu
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).
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:
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.
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.
Przygotowanie projektu
- Nowy projekt Empty Views Activity, nazwa
Pobieranie, Kotlin, API 24. - Podmień
activity_main.xmliMainActivity.kt. Nic nie jest naprawdę pobierane — upływ czasu udajeCountDownTimer.
<?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>
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 jeustawPrzyciski(true). - Minutnik zapisujemy w zmiennej
minutniktypuCountDownTimer?, żeby Anuluj mógł go zatrzymać. Gdy nic się nie pobiera, zmienna ma wartośćnull. onTickdostaje 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.formatznak%%oznacza zwykły znak procentu — samo%jest początkiem miejsca na wartość.%.1fto liczba z jednym miejscem po przecinku. onFinishustawia równe 100% — ostatnionTickmógł wypaść na przykład przy 98%.minutnik?.cancel()zatrzymuje minutnik. Operator?.wywołuje metodę tylko wtedy, gdy zmienna nie jestnull.
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
- Kliknij Pobierz, a w połowie Anuluj. Pasek zatrzyma się, a napis pokaże, przy ilu procentach przerwano. Ponowne Pobierz zaczyna od zera.
- Zmień w obliczeniu procentu kolejność na
uplynelo / czasPobierania * 100. Pasek stanie na zerze aż do końca — dzielenie liczb całkowitych. - Zmień odstęp minutnika z 100 na 1000. Pasek będzie skakał co sekundę o 20%.
Zestawienie elementów
| Element | Typ | Co robi |
|---|---|---|
<ProgressBar> | kontrolka | Bez stylu: kółko oczekiwania. |
style="?android:attr/progressBarStyleHorizontal" | styl | Poziomy pasek postępu. |
android:max, progress, secondaryProgress | atrybuty | Zakres, postęp, drugi poziom. |
android:indeterminate="true" | atrybut | Pasek bez określonego postępu. |
android:visibility="gone" | atrybut | Kontrolka ukryta bez zajmowania miejsca. |
pb.progress, pb.max | Int | Postęp i zakres z kodu. |
view.isVisible | Boolean | Pokazanie / ukrycie. |
object : CountDownTimer(czas, odstep) { … } | obiekt | Minutnik z onTick i onFinish. |
.start(), .cancel() | metody | Uruchomienie i zatrzymanie minutnika. |
zmienna?.metoda() | operator | Wywołanie tylko, gdy nie null. |
%%, %.1f w String.format | wzorzec | Znak %, liczba z jednym miejscem po przecinku. |
5000L | Long | Liczba typu Long (czas w milisekundach). |
Zanim utkniesz
| Zapis / objaw | Problem |
|---|---|
| zamiast paska kręci się kółko | Brak style="?android:attr/progressBarStyleHorizontal". |
| pasek stoi na 0, choć postęp rośnie | a / b * 100 na liczbach całkowitych daje 0. Najpierw mnóż: a * 100 / b. |
Thread.sleep w pętli | Ekran zamarza, potem komunikat „Aplikacja nie odpowiada”. Użyj CountDownTimer. |
| dwa kliknięcia „Pobierz” — pasek skacze | Działają dwa minutniki naraz. Blokuj przycisk na czas pracy. |
String.format("%d%", p) | Awaria UnknownFormatConversionException. Znak procentu we wzorcu: %%. |
| „Anuluj” nie działa | Minutnik nie został zapamiętany w zmiennej — nie ma czego zatrzymać. |
visibility="invisible" zamiast gone | Kontrolka niewidoczna, ale zajmuje miejsce — zostaje dziura w układzie. |
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.
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.
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, potemString.format("%d:%02d", s / 60, s % 60).
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ę tylkoonFinish. Pola blokujesz przezisEnabled = falsejak przyciski.
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 przezisVisible. Krok trzymaj wvar krok = 1, a pasek mamax="4".
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).
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).
Co trzeba zapamiętać
ProgressBarbez stylu to kółko; ze stylemprogressBarStyleHorizontal— pasek.- Pasek:
max= liczba kroków,progress= wykonane. Procent licz jakoa * 100 / b. - Kółko pokazujesz i chowasz przez
isVisible. - Upływ czasu symuluj przez
CountDownTimer, nigdy przezThread.sleep. - W trakcie pracy blokuj przyciski, które mogłyby ją uruchomić drugi raz.
Dokumentacja: ProgressBar, CountDownTimer.