003 ‐ Hod kostkou - zizanek/-github-tz-pma-2026-priprava GitHub Wiki
Na stejné aplikaci si ukážeme dva způsoby tvorby uživatelského rozhraní v Androidu:
- XML + Kotlin – rozhraní definujeme v XML a ovládáme ho z Kotlinu.
- Jetpack Compose – rozhraní i jeho chování zapisujeme v Kotlinu.
Vytvořte aplikaci, která obsahuje:
- nadpis „Hoď kostkou“,
- velký symbol kostky,
- tlačítko „Hodit“.
Po stisknutí tlačítka se symbol kostky desetkrát změní s prodlevou 250 milisekund. Potom se zobrazí výsledný hod.
Během animace bude tlačítko zakázané, aby uživatel nemohl spustit několik hodů současně.
Názvy proměnných, funkcí a ID píšeme anglicky. Texty pro uživatele a komentáře ponecháváme česky.
Použijeme textové znaky Unicode:
| Hodnota | Znak | Unicode |
|---|---|---|
| 1 | ⚀ | U+2680 |
| 2 | ⚁ | U+2681 |
| 3 | ⚂ | U+2682 |
| 4 | ⚃ | U+2683 |
| 5 | ⚄ | U+2684 |
| 6 | ⚅ | U+2685 |
Nejde o obrázky. Znaky lze přímo zkopírovat do kódu. Jejich vzhled závisí na použitém písmu.
val diceSymbols = listOf("⚀", "⚁", "⚂", "⚃", "⚄", "⚅")-
valdeklaruje proměnnou, které už nemůžeme přiřadit jinou hodnotu. -
diceSymbolsje název proměnné. -
listOf(...)vytvoří seznam s rozhraním pro čtení. - Jednotlivé položky jsou textové řetězce typu
String.
Položky seznamu se indexují od nuly:
diceSymbols[0] // ⚀
diceSymbols[1] // ⚁
diceSymbols[5] // ⚅val diceValue = (1..6).random()Výraz 1..6 představuje rozsah od 1 do 6 včetně obou krajních hodnot. Funkce random() z něj vybere náhodnou hodnotu.
Odpovídající symbol získáme takto:
diceSymbols[diceValue - 1]Odečítáme jedničku, protože čísla na kostce začínají jedničkou, ale indexy seznamu nulou.
V Android Studiu vytvořte projekt pomocí šablony Empty Views Activity a zvolte jazyk Kotlin.
Budeme upravovat dva soubory:
app/src/main/res/layout/activity_main.xml-
MainActivity.ktv balíčku aplikace
Nahraďte obsah souboru tímto XML:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/llMain"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:padding="24dp"
android:background="#F5F3FF">
<TextView
android:id="@+id/tvTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hoď kostkou"
android:textSize="28sp"
android:textStyle="bold"
android:textColor="#352060" />
<TextView
android:id="@+id/tvDice"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_marginBottom="24dp"
android:text="⚀"
android:textSize="120sp"
android:textColor="#352060" />
<Button
android:id="@+id/btnRoll"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hodit"
android:textSize="20sp"
android:textAllCaps="false" />
</LinearLayout>LinearLayout uspořádává své potomky do řady nebo sloupce.
android:orientation="vertical"Prvky budou pod sebou.
android:gravity="center"Obsah bude vystředěný vodorovně i svisle.
android:layout_width="match_parent"
android:layout_height="match_parent"Kořenový prvek vyplní dostupný prostor rodiče.
Hodnota wrap_content u textů a tlačítka znamená, že se jejich velikost přizpůsobí obsahu.
-
dppoužíváme pro rozměry a vzdálenosti. -
sppoužíváme pro velikost textu. Zohledňuje také uživatelské nastavení velikosti písma.
-
paddingje prostor uvnitř prvku mezi jeho okrajem a obsahem. -
marginje prostor kolem prvku.
android:id="@+id/tvDice"Vytvoří identifikátor, pomocí kterého prvek najdeme v Kotlinu.
Používáme čtyři ID. Prefix označuje typ prvku: ll = LinearLayout, tv = TextView a btn = Button. Za prefixem následuje anglický název v camelCase:
| ID | Význam |
|---|---|
llMain |
Kořenové rozložení |
tvTitle |
Nadpis aplikace |
tvDice |
Text se symbolem kostky |
btnRoll |
Tlačítko |
Řádek package musí odpovídat balíčku vašeho projektu.
package com.example.myapp001
import android.os.Bundle
import android.widget.Button
import android.widget.TextView
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContentView(R.layout.activity_main)
ViewCompat.setOnApplyWindowInsetsListener(
findViewById(R.id.llMain)
) { view, insets ->
val systemBars = insets.getInsets(
WindowInsetsCompat.Type.systemBars()
)
view.setPadding(
systemBars.left,
systemBars.top,
systemBars.right,
systemBars.bottom
)
insets
}
val diceSymbols = listOf("⚀", "⚁", "⚂", "⚃", "⚄", "⚅")
val tvDice = findViewById<TextView>(R.id.tvDice)
val btnRoll = findViewById<Button>(R.id.btnRoll)
btnRoll.setOnClickListener {
lifecycleScope.launch {
// Během animace zabráníme dalšímu kliknutí.
btnRoll.isEnabled = false
// Desetkrát změníme symbol kostky.
repeat(10) {
tvDice.text = diceSymbols.random()
delay(250)
}
// Výsledný hod.
val diceValue = (1..6).random()
tvDice.text = diceSymbols[diceValue - 1]
btnRoll.isEnabled = true
}
}
}
}class MainActivity : AppCompatActivity()MainActivity dědí z AppCompatActivity. Aktivita je komponenta Androidu, která v tomto příkladu hostuje obrazovku aplikace.
override fun onCreate(savedInstanceState: Bundle?)Metodu onCreate() Android volá při vytváření aktivity. Zde připravíme rozhraní a jeho chování.
setContentView(R.layout.activity_main)Načte rozhraní ze souboru activity_main.xml.
R je automaticky generovaná třída s odkazy na prostředky aplikace, například rozložení a ID prvků.
enableEdgeToEdge()Umožní aplikaci vykreslovat obsah také za systémovými lištami.
Následující posluchač nastaví odsazení podle velikosti systémových lišt, aby pod nimi obsah nezůstal schovaný.
Volání
setPadding()zde nahrazuje původní padding z XML. V této vystředěné ukázce to není zásadní; pokud chceme zachovat také původních 24 dp, musíme je k odsazení systémových lišt přičíst.
val tvDice = findViewById<TextView>(R.id.tvDice)
val btnRoll = findViewById<Button>(R.id.btnRoll)Podle ID získáme odkazy na konkrétní prvky rozhraní.
<TextView> a <Button> určují očekávaný typ prvku.
Pro první aplikaci používáme názorné findViewById. Tento přístup je stále použitelný.
V dalších aplikacích budeme používat View Binding. Ten z XML automaticky vygeneruje třídu s typovanými odkazy na prvky, takže je nebudeme muset ručně vyhledávat. Například místo findViewById<TextView>(R.id.tvDice) budeme přistupovat k prvku pomocí binding.tvDice. Samotné XML a jeho ID mohou zůstat stejné.
View Binding se týká varianty s XML. V Jetpack Compose jej pro toto rozhraní nepotřebujeme.
btnRoll.setOnClickListener {
// Kód provedený po kliknutí.
}Blok ve složených závorkách se provede při stisknutí tlačítka.
lifecycleScope.launch {
// Průběh hodu.
}Spustí korutinu navázanou na životnost aktivity. Při zničení aktivity se zruší.
Korutina umožní během animace čekat pomocí delay(), aniž by zablokovala hlavní vlákno a ovládání aplikace.
repeat(10) {
tvDice.text = diceSymbols.random()
delay(250)
}-
repeat(10)provede blok desetkrát. -
diceSymbols.random()vybere náhodný symbol ze seznamu. -
tvDice.text = ...přímo změní text zobrazeného prvku. -
delay(250)pozastaví korutinu na přibližně 250 ms.
Celkem animace trvá přibližně 2,5 sekundy. Stejný symbol může náhodně padnout několikrát za sebou.
btnRoll.isEnabled = falseBěhem animace tlačítko nereaguje na kliknutí. Po dokončení ho znovu povolíme:
btnRoll.isEnabled = trueVytvořte druhý projekt pomocí šablony Empty Activity s podporou Compose.
Tato šablona připraví potřebné závislosti a nastavení.
Pouhé vložení následujícího kódu do Views projektu nestačí. Takový projekt by nejprve potřeboval zapnout podporu Compose v Gradle.
V této variantě nepotřebujeme activity_main.xml. Rozhraní vytvoříme přímo v Kotlinu.
Řádek package opět upravte podle svého projektu.
package com.example.myapp001jc
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
MaterialTheme {
DiceApp()
}
}
}
}
@Composable
fun DiceApp() {
val diceSymbols = listOf("⚀", "⚁", "⚂", "⚃", "⚄", "⚅")
// Stav obrazovky.
var diceValue by remember { mutableStateOf(1) }
var isRolling by remember { mutableStateOf(false) }
// Umožní spustit animaci s prodlevami.
val scope = rememberCoroutineScope()
val backgroundColor = Color(0xFFF5F3FF)
val primaryColor = Color(0xFF352060)
Column(
modifier = Modifier
.fillMaxSize()
.background(backgroundColor)
.safeDrawingPadding()
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = "Hoď kostkou",
fontSize = 28.sp,
fontWeight = FontWeight.Bold,
color = primaryColor
)
Text(
text = diceSymbols[diceValue - 1],
fontSize = 120.sp,
color = primaryColor,
modifier = Modifier.padding(vertical = 24.dp)
)
Button(
enabled = !isRolling,
colors = ButtonDefaults.buttonColors(
containerColor = primaryColor,
contentColor = Color.White
),
onClick = {
isRolling = true
scope.launch {
// Desetkrát změníme symbol kostky.
repeat(10) {
diceValue = (1..6).random()
delay(250)
}
// Výsledný hod.
diceValue = (1..6).random()
isRolling = false
}
}
) {
Text(
text = "Hodit",
fontSize = 20.sp
)
}
}
}setContent {
MaterialTheme {
DiceApp()
}
}Místo načtení XML popíšeme obsah obrazovky pomocí Compose funkcí.
MaterialTheme poskytuje téma pro komponenty Material Design, například tlačítka.
@Composable
fun DiceApp()Anotace @Composable označuje funkci, která se účastní sestavování rozhraní v Compose.
V našem případě popisuje celou obrazovku aplikace.
var diceValue by remember { mutableStateOf(1) }
var isRolling by remember { mutableStateOf(false) }Uchováváme dvě hodnoty:
| Proměnná | Význam | Počáteční hodnota |
|---|---|---|
diceValue |
Aktuální hodnota kostky | 1 |
isRolling |
Zda právě probíhá animace | false |
-
varumožňuje hodnotu měnit. -
mutableStateOf(...)vytvoří stav, jehož změny může Compose sledovat. -
rememberuchová stav mezi opakovanými vyhodnoceními daného místa rozhraní. -
byumožní přímo číst a zapisovat hodnotu místo používání.value.
Když se stav změní, Compose může znovu vyhodnotit části rozhraní, které ho čtou. Tento proces se nazývá rekompozice.
remembersamo nezachová hodnotu při znovuvytvoření aktivity, například po otočení zařízení. Pro jednoduchý uložitelný stav lze později použítrememberSaveable.
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
)Column umisťuje prvky pod sebe, podobně jako svislý LinearLayout.
Parametry zde vystředí obsah vodorovně i svisle.
Modifier
.fillMaxSize()
.background(backgroundColor)
.safeDrawingPadding()
.padding(24.dp)Modifier upravuje rozložení a vzhled prvku. Pořadí jednotlivých úprav je důležité.
V této ukázce:
-
fillMaxSize()vyplní dostupný prostor. -
background(...)nastaví pozadí. -
safeDrawingPadding()odsune obsah od systémových lišt a dalších oblastí, kde by mohl být překrytý. -
padding(24.dp)přidá vnitřní odsazení.
Color(0xFFF5F3FF)Barva je zapsaná ve formátu AARRGGBB:
-
AA– krytí, -
RR– červená, -
GG– zelená, -
BB– modrá.
Úvodní FF znamená plné krytí.
Text(
text = diceSymbols[diceValue - 1],
fontSize = 120.sp,
color = primaryColor
)Text závisí na aktuální hodnotě diceValue.
Při změně diceValue nemusíme vyhledat textový prvek ani mu ručně přiřadit nový text. Compose aktualizuje zobrazení podle stavu.
enabled = !isRollingZnak ! znamená logickou negaci:
- když
isRollingjetrue, tlačítko je zakázané, - když
isRollingjefalse, tlačítko je povolené.
val scope = rememberCoroutineScope()Vytvoří scope navázaný na životnost tohoto místa v Compose rozhraní. Můžeme z něj spustit korutinu při kliknutí. Když dané místo opustí kompozici, scope se zruší.
onClick = {
isRolling = true
scope.launch {
repeat(10) {
diceValue = (1..6).random()
delay(250)
}
diceValue = (1..6).random()
isRolling = false
}
}Průběh je stejný jako v XML variantě:
- Zakážeme tlačítko změnou stavu
isRolling. - Desetkrát změníme číslo kostky.
- Mezi změnami počkáme 250 ms.
- Vybereme výsledný hod.
- Znovu povolíme tlačítko.
| Oblast | XML + Kotlin | Jetpack Compose |
|---|---|---|
| Popis rozhraní | XML | Kotlin |
| Zobrazení rozhraní | setContentView(...) |
setContent { ... } |
| Svislé rozložení | LinearLayout |
Column |
| Text | TextView |
Text |
| Vyhledání prvku | findViewById(...) |
V ukázce není potřeba |
| Změna kostky | Přiřadíme tvDice.text
|
Změníme stav diceValue
|
| Zakázání tlačítka | Změníme isEnabled
|
enabled závisí na stavu |
| Korutina pro animaci | lifecycleScope |
rememberCoroutineScope() |
V XML variantě přímo přikazujeme konkrétnímu prvku, co má zobrazit:
tvDice.text = diceSymbols[diceValue - 1]V Compose popíšeme, co se má zobrazovat podle stavu:
Text(text = diceSymbols[diceValue - 1])Potom měníme stav:
diceValue = (1..6).random()První ukázka předvádí imperativní aktualizaci rozhraní, druhá deklarativní popis rozhraní.
V obou aplikacích zkontrolujte:
- Po spuštění se zobrazuje tvDice s jedním bodem.
- Po kliknutí se diceSymbols střídají přibližně 2,5 sekundy.
- Během animace nelze znovu stisknout tlačítko.
- Po dokončení zůstane zobrazený výsledný hod.
- Tlačítko lze znovu použít.
Vzhled tlačítka a vykreslení znaků se mohou mezi variantami mírně lišit podle tématu a písma.
- Změňte barvu pozadí a velikost kostky.
- Zkraťte animaci na jednu sekundu.
- Během animace změňte text tlačítka na „Házím…“.
- Přidejte druhou kostku.
- Zobrazte součet hodnot obou kostek.