005 ‐ Objednávka v2 ‐ více aktivit, constraint layout, scrollview - zizanek/-github-tz-pma-2026-priprava GitHub Wiki
Budeme pokračovat s objednávkou... ale víceméně začneme znovu....
MyApp005ObjednavkaV2
Tvorba ConstraintLayoutu od nuly: Krok za krokem
Při tvorbě moderního uživatelského rozhraní v Androidu je doporučeno využívat ConstraintLayout. Následující návod popisuje, jak krok za krokem vystavět obrazovku z prázdného rozvržení a jak správně definovat vazby (constraints) mezi jednotlivými prvky.
1. Příprava projektu (Zkopírování zdrojů)
Než se začne se samotnou tvorbou vizuální části, je nutné do nového projektu přenést zdroje (resources) z předchozích úkolů:
- Obrázky: Soubory (např. fotografie kol) jsou zkopírovány do složky
res/drawable. - Texty a barvy: Je přenesen obsah souborů
strings.xmlacolors.xml. - Logika aplikace: Je připraven kód s posluchači událostí (
setOnClickListener) z předchozíhoMainActivity.kt.
2. Postup skládání prvků na obrazovku
Při vkládání komponent na plochu se v Android Studiu (v záložce Design) uplatňují následující pravidla a postupy.
A. Chybějící vazba (Červená chyba)
- Situace: Na prázdnou plochu je přetažen první prvek (např.
TextViewpro nadpis). - Problém: Prvek sice na ploše zůstane, ale ve stromu komponent (Component Tree) je signalizována červená chyba.
- Vysvětlení: Systém nezná pozici prvku. Pokud není definována alespoň jedna vodorovná a jedna svislá vazba, prvek je při spuštění na reálném zařízení automaticky přesunut do levého horního rohu (na souřadnice
[0,0]).
B. Vytvoření prvních vazeb (Kotevní body a centrování)
- Postup: Kotevní body (kolečka) na hranách prvku jsou tažením myši připojeny k hornímu, levému a pravému okraji obrazovky.
- Výsledek: V zobrazení kódu (Split) jsou automaticky vygenerovány atributy jako
app:layout_constraintTop_toTopOf="parent". Nastavením protichůdných vazeb (tahem zleva i zprava k okrajům displeje) je dosaženo automatického vycentrování prvku.
C. Zlaté pravidlo ConstraintLayoutu: Použití 0dp
- Postup: Na plochu je vložen obrázek kola (
ImageView) a je ukotven k okrajům. - Zásada: Pokud je vyžadováno dynamické roztažení prvku mezi dvěma kotevními body, nesmí být použita hodnota
match_parent. Namísto toho je šířka (nebo výška) nastavena na hodnotu0dp(v grafickém editoru označováno jako Match Constraint). Prvek se následně chová elasticky a přesně vyplní vymezený prostor.
D. Vazby mezi prvky navzájem (Řazení pod sebe)
- Postup: Pod nadpis je vložen prvek
RadioGrouppro výběr kola. - Princip: Horní hrana bloku s přepínači není vázána na okraj obrazovky, ale na spodní hranu nadpisu (v kódu zapsáno jako
app:layout_constraintTop_toBottomOf="@id/tvOrderTitle"). Tím je zajištěno, že při změně velikosti nebo posunutí nadpisu jsou přepínače automaticky posunuty spolu s ním.
E. Plochá struktura (Umístění prvků vedle sebe)
- Postup: Obrázek kola je umístěn vizuálně napravo od přepínačů.
- Princip: Tohoto rozložení je dosaženo pouhým definováním levé vazby obrázku na pravou hranu prvku
RadioGroup(atributapp:layout_constraintStart_toEndOf="@id/rgBikes"). - Výhoda: Na rozdíl od staršího
LinearLayoutu, kde by bylo nutné vytvářet další obalující kontejnery, je vConstraintLayoutuveškeré rozložení řešeno v jedné rovině (tzv. plochá hierarchie). To eliminuje složité vnořování a výrazně zrychluje vykreslování uživatelského rozhraní.
Oživení formuláře: Výpočet ceny, odeslání dat (Intent), příjem dat v jiné aktivitě
Když je vizuální část (Layout) hotová, je potřeba obrazovce vdechnout život pomocí kódu v MainActivity.kt. Cílem této části je zajistit dvě věci:
- Dynamický výpočet ceny: Kdykoliv uživatel přepne model kola nebo zaklikne příplatek, cena v modrém poli se musí okamžitě přepočítat.
- Odeslání objednávky: Po kliknutí na tlačítko "Objednat" se vybraná data zabalí a odešlou do nové obrazovky (aktivity) pomocí tzv.
Intentu.
1. Průběžný výpočet ceny
Aby nebylo nutné psát výpočet ceny pro každé tlačítko zvlášť, je nejlepší vytvořit si univerzální metodu updatePrice(). Ta se jednoduše zavolá pokaždé, když uživatel na cokoliv klikne.
Metoda pro výpočet (vložit do třídy MainActivity, mimo onCreate)
Tato metoda zkontroluje, co je aktuálně vybráno, sečte částky a vypíše výsledek do textového pole tvTotalPrice.
private fun updatePrice() {
var totalPrice = 0
// 1. Zjištění základní ceny podle vybraného kola
totalPrice += when (binding.rgBikes.checkedRadioButtonId) {
binding.rbBike1.id -> 95000 // Oiz M10
binding.rbBike2.id -> 115000 // Oiz M20
binding.rbBike3.id -> 135000 // Oiz M30
else -> 0
}
// 2. Přičtení příplatkové výbavy (každý prvek např. +5000 Kč)
if (binding.cbFork.isChecked) totalPrice += 5000
if (binding.cbSaddle.isChecked) totalPrice += 5000
if (binding.cbHandleBar.isChecked) totalPrice += 5000
// 3. Zobrazení výsledku v modrém poli
binding.tvTotalPrice.text = "Celkem: $totalPrice Kč"
}
Zprovoznění ViewBindingu a posluchačů (vložit do onCreate)
V metodě onCreate se nejprve nastaví View Binding a následně se přidají posluchače na všechny interaktivní prvky.
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// Zavoláme výpočet hned po startu, aby se zobrazila výchozí cena (jinak by tam bylo 0 Kč)
updatePrice()
// Nastavení posluchačů změn pro RadioGroup (výběr kola)
binding.rgBikes.setOnCheckedChangeListener { _, _ -> updatePrice() }
// Nastavení posluchačů kliknutí pro CheckBoxy (příplatky)
binding.cbFork.setOnClickListener { updatePrice() }
binding.cbSaddle.setOnClickListener { updatePrice() }
binding.cbHandleBar.setOnClickListener { updatePrice() }
// ... Logika pro tlačítko Objednat následuje v dalším kroku
}
2. Vytvoření nové aktivity (SummaryActivity)
Abychom mohli odeslaná data z první obrazovky někde zobrazit, musíme vytvořit novou aktivitu (novou obrazovku). Každá vizuální aktivita v Androidu se skládá ze dvou částí, které musíme vytvořit:
- Logika: Kotlin třída (např.
SummaryActivity.kt) - Vzhled: XML layout (např.
activity_summary.xml)
Průvodce v Android Studiu naštěstí umí vygenerovat obě tyto části současně.
Postup vytvoření
- V levém panelu Project si rozbalte složku
app->javaa najděte název vašeho balíčku (např.com.example.myapp005objednavkav2). - Klikněte na tento balíček pravým tlačítkem myši.
- V kontextové nabídce zvolte New -> Activity.
- Z nabídky šablon vyberte Empty Views Activity.
Důležité upozornění pro novější verze Android Studia: Vybírejte vždy variantu obsahující slovo "Views". Volba pouhé "Empty Activity" by v moderních verzích vytvořila obrazovku pro technologii Jetpack Compose (bez XML souboru).
- Objeví se dialogové okno k nastavení nové aktivity. Vyplňte jej takto:
- Activity Name:
SummaryActivity(Název naší Kotlin třídy) - Layout Name:
activity_summary(Doplní se automaticky, název XML souboru) - Source Language: Kotlin
- Activity Name:
- Klikněte na tlačítko Finish.
Co se stalo na pozadí?
Android Studio právě automaticky vykonalo tři nezbytné kroky:
- Vytvořilo soubor
SummaryActivity.kts prázdnou metodouonCreate. - Ve složce
res/layoutvytvořilo prázdný souboractivity_summary.xml. - Zapsalo novou aktivitu do
AndroidManifest.xml. Tento krok je kritický – pokud by systém o aktivitě v manifestu nevěděl, při kliknutí na tlačítko "Objednat" by aplikace okamžitě spadla.
3. Odeslání dat do druhé aktivity (Intent)
Tlačítko btnOrder již nebude vypisovat text na stejnou obrazovku. Místo toho se vytvoří Intent (úmysl), do kterého se data vloží přes metodu putExtra a následně se spustí nová aktivita.
Tento kód se vloží zpět do metody onCreate (pod výše uvedené posluchače):
binding.btnOrder.setOnClickListener {
// 1. Zjištění názvu vybraného kola
val bikeName = when (binding.rgBikes.checkedRadioButtonId) {
binding.rbBike1.id -> binding.rbBike1.text.toString()
binding.rbBike2.id -> binding.rbBike2.text.toString()
binding.rbBike3.id -> binding.rbBike3.text.toString()
else -> "Neznámé kolo"
}
// 2. Extrakce výsledné ceny (pro zjednodušení bereme rovnou hotový text z TextView)
val currentPriceText = binding.tvTotalPrice.text.toString()
// 3. Vytvoření Intentu směřujícího na novou aktivitu (SummaryActivity)
val intent = Intent(this, SummaryActivity::class.java)
// 4. Přibalení dat jako klíč-hodnota
intent.putExtra("EXTRA_BIKE_NAME", bikeName)
intent.putExtra("EXTRA_TOTAL_PRICE", currentPriceText)
// Přidání informace o nadstandardní výbavě (boolean hodnoty)
intent.putExtra("EXTRA_HAS_FORK", binding.cbFork.isChecked)
intent.putExtra("EXTRA_HAS_SADDLE", binding.cbSaddle.isChecked)
intent.putExtra("EXTRA_HAS_HANDLEBAR", binding.cbHandleBar.isChecked)
// 5. Spuštění druhé aktivity
startActivity(intent)
}
Nyní můžeme přejít do souboru activity_summary.xml a začít tvořit vzhled druhé obrazovky.
4. Vzhled druhé obrazovky a použití ScrollView (activity_summary.xml)
Na telefonech se velmi často stává, že se veškerý obsah obrazovky (například dlouhá rekapitulace objednávky, dlouhý článek nebo obchodní podmínky) nevejde na displej. Pokud bychom použili pouze běžný ConstraintLayout, text by byl na konci displeje nekompromisně oříznut a uživatel by nikdy neviděl tlačítko umístěné dole.
Tento problém řeší komponenta ScrollView. Vytvoří na obrazovce "rolovací okno", které uživateli umožní posouvat obsah prstem nahoru a dolů.
Zlaté pravidlo ScrollView:
ScrollViewsmí obsahovat pouze jednoho přímého potomka. Nelze do něj vložit přímo pět různých textů. Vždy do něj musíte vložit jeden hlavní "obalový" kontejner (např.ConstraintLayout) a až do tohoto kontejneru vkládat jednotlivé vizuální prvky.
Pro demonstraci scrollování v našem projektu jsme pod rekapitulaci ceny přidali záměrně dlouhý text fiktivních obchodních podmínek. Ten vytlačí tlačítko "Zpět" mimo viditelnou oblast displeje a donutí vás k posouvání.
Následující kód vložte do souboru activity_summary.xml:
<?xml version="1.0" encoding="utf-8"?>
<ScrollView 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"
android:fillViewport="true"
tools:context=".SummaryActivity">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">
<TextView
android:id="@+id/tvSummaryHeader"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="Rekapitulace objednávky"
android:textSize="28sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tvSelectedBike"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="42dp"
android:text="Vybrané kolo: "
android:textSize="20sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tvSummaryHeader" />
<TextView
android:id="@+id/tvExtras"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="Příplatková výbava:\n- žádná"
android:textSize="18sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tvSelectedBike" />
<TextView
android:id="@+id/tvFinalPrice"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="42dp"
android:background="#81D4FA"
android:gravity="center"
android:padding="16dp"
android:text="Celková cena: 0 Kč"
android:textSize="24sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tvExtras" />
<!-- NOVÁ ČÁST PRO DEMONSTRACI SCROLLOVÁNÍ -->
<TextView
android:id="@+id/tvTermsTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="42dp"
android:text="Obchodní podmínky"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tvFinalPrice" />
<TextView
android:id="@+id/tvTermsText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="1. ÚVODNÍ USTANOVENÍ\nTyto obchodní podmínky upravují vzájemná práva a povinnosti smluvních stran vzniklé v souvislosti nebo na základě kupní smlouvy uzavírané mezi prodávajícím a kupujícím prostřednictvím internetového obchodu.\n\n2. UZAVŘENÍ KUPNÍ SMLOUVY\nVeškerá prezentace zboží umístěná ve webovém rozhraní obchodu je informativního charakteru a prodávající není povinen uzavřít kupní smlouvu ohledně tohoto zboží. Kupní smlouva vzniká odesláním objednávky kupujícím a jejím přijetím prodávajícím.\n\n3. CENA ZBOŽÍ A PLATEBNÍ PODMÍNKY\nCenu zboží a případné náklady spojené s dodáním zboží dle kupní smlouvy může kupující uhradit prodávajícímu bezhotovostně převodem na účet prodávajícího, nebo dobírkou při převzetí.\n\n4. ODSTOUPENÍ OD KUPNÍ SMLOUVY\nKupující bere na vědomí, že dle ustanovení občanského zákoníku nelze mimo jiné odstoupit od kupní smlouvy o dodávce zboží, které bylo upraveno podle přání kupujícího nebo pro jeho osobu.\n\n5. PRÁVA Z VADNÉHO PLNĚNÍ\nPráva a povinnosti smluvních stran ohledně práv z vadného plnění se řídí příslušnými obecně závaznými předpisy.\n\n6. OCHRANA OSOBNÍCH ÚDAJŮ\nOchrana osobních údajů kupujícího, který je fyzickou osobou, je poskytována v souladu s nařízením GDPR. Kupující souhlasí se zpracováním těchto svých osobních údajů: jméno a příjmení, adresa bydliště, identifikační číslo, daňové identifikační číslo, adresa elektronické pošty a telefonní číslo.\n\n7. ZÁVĚREČNÁ USTANOVENÍ\nPokud vztah založený kupní smlouvou obsahuje mezinárodní (zahraniční) prvek, pak strany sjednávají, že vztah se řídí českým právem. Tímto nejsou dotčena práva spotřebitele vyplývající z obecně závazných právních předpisů."
android:textSize="14sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tvTermsTitle" />
<!-- KONEC NOVÉ ČÁSTI -->
<Button
android:id="@+id/btnBack"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:layout_marginBottom="32dp"
android:text="Zpět k úpravě"
android:textSize="20sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tvTermsText" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
Poznámka: K čemu slouží
android:fillViewport="true"? Tento atribut říká komponentěScrollView, aby její vnitřní obsah "vyplnil minimálně celou viditelnou oblast displeje".
- Bez tohoto nastavení: Pokud je na obrazovce málo textu, vnitřní
ConstraintLayoutse na výšku "scvrkne" jen na nezbytné minimum. Tlačítko zarovnané ke spodnímu okraji (toBottomOf="parent") by pak nezůstalo na dně displeje, ale "viselo" by nehezky uprostřed obrazovky hned pod textem.- S tímto nastavením: Vnitřní kontejner se vždy roztáhne minimálně přes celý displej. Tlačítko díky tomu zůstane bezpečně ukotvené úplně dole. Pokud je obsahu naopak moc, obrazovka se chová jako klasické rolovací plátno.
5. Přijetí dat a zobrazení rekapitulace (SummaryActivity.kt)
Když máme hotový vzhled, musíme druhé obrazovce vdechnout život. Úkolem souboru SummaryActivity.kt je zachytit data, která nám poslal Intent z první obrazovky, poskládat z nich srozumitelný text a vypsat ho do připravených textových polí.
Zároveň zde využijeme moderní přístup Edge-to-Edge, který zajistí, že se náš rolovací obsah nebude schovávat pod horní systémovou lištu (s hodinami) nebo pod spodní ovládací tlačítka telefonu.
package com.example.myapp005objednavkav2 // Zkontrolujte si svůj název balíčku
import android.os.Bundle
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.example.myapp005objednavkav2.databinding.ActivitySummaryBinding
class SummaryActivity : AppCompatActivity() {
private lateinit var binding: ActivitySummaryBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 1. Aktivace moderního roztažení aplikace přes celou obrazovku
//enableEdgeToEdge()
// Inicializace ViewBindingu
binding = ActivitySummaryBinding.inflate(layoutInflater)
setContentView(binding.root)
// 2. Ochrana před překryvem systémovými lištami (využívá naše nové ID "main")
ViewCompat.setOnApplyWindowInsetsListener(binding.main) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
// Přidá vnitřní okraj (padding) odpovídající velikosti horní a spodní systémové lišty
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
// 3. Přijmutí dat z Intentu
val bikeName = intent.getStringExtra("EXTRA_BIKE_NAME") ?: "Neznámé kolo"
val totalPrice = intent.getStringExtra("EXTRA_TOTAL_PRICE") ?: "Celkem: 0 Kč"
val hasFork = intent.getBooleanExtra("EXTRA_HAS_FORK", false)
val hasSaddle = intent.getBooleanExtra("EXTRA_HAS_SADDLE", false)
val hasHandlebar = intent.getBooleanExtra("EXTRA_HAS_HANDLEBAR", false)
// 4. Sestavení textu pro příplatkovou výbavu
var extrasText = "Příplatková výbava:\n"
var hasAnyExtra = false
if (hasFork) {
extrasText += "- Lepší vidlice\n"
hasAnyExtra = true
}
if (hasSaddle) {
extrasText += "- Lepší sedlo\n"
hasAnyExtra = true
}
if (hasHandlebar) {
extrasText += "- Lepší řídítka\n"
hasAnyExtra = true
}
// Pokud uživatel nezaškrtl nic
if (!hasAnyExtra) {
extrasText += "- žádná"
}
// 5. Zobrazení dat v textových polích
binding.tvSelectedBike.text = "Vybrané kolo: $bikeName"
binding.tvExtras.text = extrasText
binding.tvFinalPrice.text = totalPrice
// 6. Tlačítko Zpět pro návrat na úpravu objednávky
binding.btnBack.setOnClickListener {
finish()
}
}
}
Co se v kódu děje:
- Ochrana okrajů (Edge-to-Edge): Funkce
enableEdgeToEdge()roztáhne aplikaci přes celou plochu displeje. Následný blok kóduViewCompat.setOnApplyWindowInsetsListenerzjistí, jak vysoké jsou systémové lišty (baterie, signál, navigační tlačítka), a přidá našemu hlavnímuScrollView(proto to IDmain) přesně tak velký vnitřní okraj (padding), aby text nebyl těmito lištami překryt. - Rozbalení Intentu: Metoda
intent.getStringExtra("KLÍČ")vytáhne textová data. Operátor?:(tzv. Elvis operátor) určuje výchozí hodnotu pro případ, že by pod tímto klíčem žádná data nedorazila. ProCheckBoxypoužívámegetBooleanExtra(). - Skládání textu (String manipulation): Na základě proměnných typu
Boolean(pravda/nepravda) programově rozhodneme, které řádky se přidají do finálního textu pro příplatkovou výbavu. - Ukončení aktivity: Metoda
finish()nedělá nic jiného, než že aktuální obrazovku "zavře". Android si pamatuje historii (tzv. Back Stack), takže nás automaticky vrátí zpět na předchozíMainActivitys nezměněným původním výběrem.