diff --git a/.gitignore b/.gitignore index 3a42aea..cb57835 100644 --- a/.gitignore +++ b/.gitignore @@ -16,7 +16,6 @@ *.war *.nar *.ear -*.zip *.tar.gz *.rar diff --git a/docs/laborok/03-android-ui-adv/assets/MainActivityLayout.png b/docs/laborok/03-android-ui-adv/assets/MainActivityLayout.png deleted file mode 100644 index 799d602..0000000 Binary files a/docs/laborok/03-android-ui-adv/assets/MainActivityLayout.png and /dev/null differ diff --git a/docs/laborok/03-android-ui-adv/assets/MainScreen.png b/docs/laborok/03-android-ui-adv/assets/MainScreen.png new file mode 100644 index 0000000..46e187d Binary files /dev/null and b/docs/laborok/03-android-ui-adv/assets/MainScreen.png differ diff --git a/docs/laborok/03-android-ui-adv/assets/MainScreen_TopBar.png b/docs/laborok/03-android-ui-adv/assets/MainScreen_TopBar.png index de6188d..4c93697 100644 Binary files a/docs/laborok/03-android-ui-adv/assets/MainScreen_TopBar.png and b/docs/laborok/03-android-ui-adv/assets/MainScreen_TopBar.png differ diff --git a/docs/laborok/03-android-ui-adv/assets/MainScreen_empty.png b/docs/laborok/03-android-ui-adv/assets/MainScreen_empty.png new file mode 100644 index 0000000..0baca21 Binary files /dev/null and b/docs/laborok/03-android-ui-adv/assets/MainScreen_empty.png differ diff --git a/docs/laborok/03-android-ui-adv/assets/MainScreen_empty_list.png b/docs/laborok/03-android-ui-adv/assets/MainScreen_empty_list.png new file mode 100644 index 0000000..772e642 Binary files /dev/null and b/docs/laborok/03-android-ui-adv/assets/MainScreen_empty_list.png differ diff --git a/docs/laborok/03-android-ui-adv/assets/MainScreen_scaffold.png b/docs/laborok/03-android-ui-adv/assets/MainScreen_scaffold.png new file mode 100644 index 0000000..cebddf0 Binary files /dev/null and b/docs/laborok/03-android-ui-adv/assets/MainScreen_scaffold.png differ diff --git a/docs/laborok/03-android-ui-adv/assets/SnackBarShowsCorrectly.png b/docs/laborok/03-android-ui-adv/assets/SnackBarShowsCorrectly.png deleted file mode 100644 index d55e049..0000000 Binary files a/docs/laborok/03-android-ui-adv/assets/SnackBarShowsCorrectly.png and /dev/null differ diff --git a/docs/laborok/03-android-ui-adv/assets/menu.png b/docs/laborok/03-android-ui-adv/assets/menu.png deleted file mode 100644 index ce47fc2..0000000 Binary files a/docs/laborok/03-android-ui-adv/assets/menu.png and /dev/null differ diff --git a/docs/laborok/03-android-ui-adv/assets/sample_screen.png b/docs/laborok/03-android-ui-adv/assets/sample_screen.png deleted file mode 100644 index df56563..0000000 Binary files a/docs/laborok/03-android-ui-adv/assets/sample_screen.png and /dev/null differ diff --git a/docs/laborok/03-android-ui-adv/downloads/expense.png b/docs/laborok/03-android-ui-adv/downloads/expense.png deleted file mode 100644 index 42e7d6a..0000000 Binary files a/docs/laborok/03-android-ui-adv/downloads/expense.png and /dev/null differ diff --git a/docs/laborok/03-android-ui-adv/downloads/income.png b/docs/laborok/03-android-ui-adv/downloads/income.png deleted file mode 100644 index f95301d..0000000 Binary files a/docs/laborok/03-android-ui-adv/downloads/income.png and /dev/null differ diff --git a/docs/laborok/03-android-ui-adv/index.md b/docs/laborok/03-android-ui-adv/index.md index 6303388..b25af6b 100644 --- a/docs/laborok/03-android-ui-adv/index.md +++ b/docs/laborok/03-android-ui-adv/index.md @@ -2,26 +2,26 @@ ## Bevezető -A labor célja egy egyszerű felhasználói felület tervezése, kivitelezése. +A labor célja egy egyszerű felhasználói felület tervezése és kivitelezése Android platformon, Jetpack Compose felhasználásával. -A feladat egy kiadás / bevétel naplózására alkalmas alkalmazás elkészítése AndroidWallet néven. Az alkalmazás alap funkcionalitása, hogy a felhasználó fel tudja venni egy listába a kiadásait, bevételeit, illetve törölni tudja az egész lista tartalmát. +A feladat egy kiadás és bevétel naplózására alkalmas alkalmazás elkészítése AndroidWallet néven. Az alkalmazás alap funkcionalitása, hogy a felhasználó fel tudja venni egy listába a kiadásait és a bevételeit, törölni tudja őket, illetve törölni tudja az egész lista tartalmát. A kész alkalmazás mintaképe:

- +

+ Az alkalmazás felépítése és működése a következő: -- Kezdőképernyő a listával (LazyColumn) illetve egy beviteli résszel rendelkezik. Itt a felhasználó beír egy megnevezést és egy összeget, megadja a pénzforgalom irányát, majd ezután el tudja menteni a listába a tranzakcióját. Amennyiben itt bármelyik mező üres, a mentést meg kell akadályoznunk. +- Kezdőképernyő egy listával (LazyColumn) illetve egy beviteli résszel rendelkezik. Itt a felhasználó beír egy megnevezést és egy összeget, megadja a pénzforgalom irányát, majd ezután el tudja menteni a listába a tranzakcióját. Amennyiben itt bármelyik mező üres, a mentést meg kell akadályoznunk. - Egy listaelem felépítése: - Ikon a pénzforgalom irányától függően. - A megadott megnevezés és alatta az összeg. - - A Toolbaron egy menüpont a lista teljes törlésére. - - A lista görgethető kell legyen + - Egy gomb a tétel törlésére. ### Felhasznált technológiák: -- Scaffold, TopBar, Column, Row, Image, OutlinedTextField, Button, ElevatedButton, Text, **LazyColumn** +- **Scaffold**, TopBar, BottomBar, FloatingActionButton, Column, Row, Image, Text, Spacer, OutlinedTextField, IconButton, IconToggleButton, **LazyColumn** - data class @@ -58,277 +58,524 @@ Hozzunk létre egy AndroidWallet nevű projektet Android Studioban: !!!danger "FILE PATH" A repository elérési helye ne tartalmazzon ékezeteket, illetve speciális karaktereket, mert az AndroidStudio ezekre érzékeny, így nem fog a kód lefordulni. Érdemes a C:\\ meghajtó gyökerében dolgozni. -## Menü elkészítése -Azt szeretnénk, ha a képernyő felső részében lenne egy ActionBar, (alkalmazás nevével és) egy törlési opcióval, vagy akár egy legördülő menü opcióval. Ehhez a megvalósításhoz, nagyon jól alkalmazható a Scaffold Composable, ugyanis ennek van egy *topBar* attribútuma, aminek könnyen adhatunk egy ilyen ActionBar-t. Első lépésben hozzunk létre egy Packaget a projek mappájában `appbar` néven, majd ezen belül egy új *Kotlin* classt `TopBar` néven, ezután írjuk bele a következőt: +## Főképernyő elkészítése + +Első lépésként, hogy ezzel a fejlesztés során a későbbiekben ne legyen gond, illesszük be a szöveges erőforrásainkat a `strings.xml` fájlba: + +```xml + + AndroidWallet + Item + Price + List is empty. Start adding salary. + +``` + +Második lépésként, hogy a felhasználói felületületét akadálytalanul el tudjuk készíteni, készítsük el az adat struktúrát, ami a tárolandó adatokat fogja tartalmazni. Szükégünk vagy egy listára, amely az adatokat tartalmazza. Ebben a listában az alábbi `SalaryData` *data class* objektumokat fogunk tárolni. Hozzunk létre egy `data` *package*-et a fő *package*-ünkön belül, majd abba tegyük bele a `SalaryData` osztályt: ```kotlin +package hu.bme.aut.android.androidwallet.data + +data class SalaryData( + val isIncome: Boolean, + val item: String, + val price: String +) +``` + +Az osztály három változója az alábbiakat reprezentálja: + +- `isIncome - Boolean változó amely a kiadás/bevétel állapotért felel.` +- `item - kiadás/bevétel neve` +- `price - kiadás/bevétel értéke` + + +Miután megvagyunk a modell osztállyal, áttérhetünk a felhasználói felületre. Készítsük el a főképernyő vázát, amit majd a labor során feltöltünk tartalommal. Ehhez hozzunk létre egy `screen` *package*-et a `ui` *package*-en belül, majd ebbe egy `MainScreen` nevű új Kotlin classt. Írjuk meg a főképernyőnek a felépítését az alábbi kód alapján: + +```kotlin +package hu.bme.aut.android.androidwallet.ui.screen + +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawingPadding +import androidx.compose.material3.BottomAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import hu.bme.aut.android.androidwallet.data.SalaryData + @OptIn(ExperimentalMaterial3Api::class) @Composable -fun TopBar(title: String, icon: ImageVector, onIconClick: () -> Unit) { - TopAppBar( - title = { Text(text = title) }, - actions = { - IconButton(onClick = onIconClick) { - Icon(imageVector = icon, contentDescription = "Delete", tint = Color.Red) - } +fun MainScreen() { + val context = LocalContext.current + val salaryItems = remember { mutableStateListOf() } + + var isIncome by remember { mutableStateOf(false) } + var item by remember { mutableStateOf("") } + var price by remember { mutableStateOf("") } + + + + Scaffold( + modifier = Modifier.safeDrawingPadding(), + topBar = { + + ///TODO (topbar) + TopAppBar(title = { Text(text = "TopAppbar") }) + }, - colors = TopAppBarDefaults.topAppBarColors(containerColor = MaterialTheme.colorScheme.inversePrimary) + floatingActionButton = { + ///TODO (floatingactionbutton) + + }, + content = + { innerPadding -> + + ///TODO (lista) + Text( + text = "content", + modifier = Modifier.padding(innerPadding) + ) + + + }, + bottomBar = { + BottomAppBar { + ///TODO (TextFields) + Text(text = "BottomAppBar") + + } + + } ) } + +@Composable +@Preview +fun PreviewMainScreen() { + MainScreen() +} ``` -Ezzel a TopBar kész is, azonban ahhoz, hogy a főképernyőt elkészítsük, létre kell hoznunk egy listaelemet, amit majd a LazyColumn-ban fogunk látni. +A `MainScreen` tartalmaz egy [`Scaffold`](https://developer.android.com/develop/ui/compose/components/scaffold)-ot, ami segít minket a felület struktúrájának kialakítában. A `Scaffold` lényege, hogy megad egy vázat (állványt), aminek az előre definiált helyeire ([lyukaiba](https://developer.android.com/develop/ui/compose/layouts/basics#slot-based-layouts)) tudunk saját *Composable*-öket illeszteni. -- Egy listaelem felépítése: - - Ikon a pénzforgalom irányától függően. - - A megadott megnevezés és alatta az összeg. - - A Toolbaron egy menüpont a lista teljes törlésére. - - A lista görgethető kell legyen +Ahhoz, hogy az alkalmazásunk máris futtatható legyen, és ki tudjuk próbálni minden lépés után, cseréljük le a `MainActivity` tartalmát: +```kotlin +package hu.bme.aut.android.androidwallet +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview +import hu.bme.aut.android.androidwallet.ui.screen.MainScreen +import hu.bme.aut.android.androidwallet.ui.theme.AndroidWalletTheme -## Listaelem létrehozása (1 pont) +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { + AndroidWalletTheme { + MainScreen() + } + } + } +} -Az ehhez tartozó képeket le lehet menteni innen: +@Preview(showBackground = true) +@Composable +fun PreviewMainActivity() { + AndroidWalletTheme { + MainScreen() + } +} +``` -* [income](./downloads/income.png) -* [expense](./downloads/expense.png) +Jelenleg így néz ki az alkalmazásunk: -Ezt a két képet másoljuk be a `res/mipmap` mappa egyikébe, ezután hozzunk létre egy új *Packaget* `screen` néven, majd ebben egy új *Kotlin* classt `SalaryCard` néven. +

+ +

-Ennek a következő képpen kell kinéznie: + +### A menüsáv elkészítése ( 1 pont) + +Azt szeretnénk, hogy a képernyő tetején legyen egy `ActionBar` az alkalmazás nevével és egy törlési opcióval, vagy akár egy legördülő menüvel. Mint feljebb láttuk, ehhez a megvalósításhoz, nagyon jól alkalmazható a `Scaffold` *Composable*, ugyanis ennek van egy *topBar* attribútuma, aminek könnyen adhatunk egy ilyen `ActionBar`-t. + +Hozzunk létre egy új *package*-et a már meglévő `ui` csomagban `view` néven, majd ezen belül egy új *Kotlin* classt `TopBar` néven. Töltsük föl a fájlt az alábbi kóddal: ```kotlin +package hu.bme.aut.android.androidwallet.ui.view + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Clear +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.tooling.preview.Preview + + +@OptIn(ExperimentalMaterial3Api::class) @Composable +fun TopBar(title: String, icon: ImageVector, onIconClick: () -> Unit) { + TopAppBar( + title = { Text(text = title) }, + actions = { + IconButton(onClick = onIconClick) { + Icon(imageVector = icon, contentDescription = "Delete", tint = Color.Red) + } + }, + colors = TopAppBarDefaults.topAppBarColors(containerColor = MaterialTheme.colorScheme.inversePrimary) + ) +} + @Preview -fun SalaryCard(isIncome: Boolean = false, item: String = "Item", price: String = "Price") { - Row { - Image(modifier = Modifier - .size(64.dp) - .padding(8.dp), - painter = painterResource(id = if (isIncome) R.mipmap.income else R.mipmap.expense), - contentDescription = "Income/Expense") - Column ( - modifier = Modifier.padding(8.dp) - ) { - Text(text = item) - Text(text = price) - } +@Composable +@OptIn(ExperimentalMaterial3Api::class) +fun PreviewTopBar() { + TopBar(title = "AndroidWallet", icon = Icons.Default.Clear) { + } } ``` -A *SalaryCard* Composable függvény 3 paramétert tartalmaz: +A `TopAppBar`-nak a cím és a szín mellett megadtunk egy akciót is: egy `IconButton`-t amivel a lista törlését fogjuk majd megvalósítani. -- `isIncome - Boolean változó amely a kiadás/bevétel állapotért felel.` -- `item - kiadás/bevétel neve` -- `price - kiadás/bevétel értéke` - -A függvényen belül megtalálható egy *Row*, valamint egy *Column*. A *Row* felel azért, hogy az elemeket horizontálisan egymás mellé lehessen rakni, a Column pedig, hogy az elemeket egymás alá. (Ez utóbbi a kiadás/bevétel neve, illetve értéke miatt szükséges, hogy egymás alatt szerepeljenek) A képet pedig egy Image Composable-val helyezzük el. Itt a `modifier` segítségével sok fajta beállításra van lehetőség, most csak a size-val, illetve a paddinggel foglalkozunk, hogy átláthatóbb legyen. A `painter` segítségével adhatjuk meg a képet, amit szeretnénk megjeleníteni, ezt egy if-else elágazással oldjuk meg, mégpedig a paraméterként kapott `isIncome` segítségével. Miután megvagyunk az Image-val, a `Row`-n belül a `Column`-ba elhelyezünk kettő `Text`-et, a maradék kettő paraméterrel. - -Ahhoz, hogy végezzünk a `SalaryCard` fájllal, még egy fontos lépést végre kell hajtani, ez pedig egy *data class* implementálása. Ez a LazyColumn-nak átadott lista miatt lesz szükséges. +Miután elkészültünk a `TopBar`-unkkal, illesszük is ezt be a `MainScreen` beli `Scaffold` megfelelő helyére. Ezt a következő képpen tesszük meg: adunk neki egy tetszőleges *title*-t (általában az alkalmazás nevét), ez most *Android Wallet* lesz, majd egy icon-t. Használjuk az Android Studio beépített iconjait. Ezután meg kell adnunk egy Lambdát, aminek a segítségével leírjuk, hogy mi történjen, hogyha a felhasználó rákattint az iconra. Jelen esetben ki kell ürítenünk a listánkat. Mivel a listánk állapotként van tárolva `val salaryItems = remember { mutableStateListOf() }`, ezért ha változás történik, akkor az összes Composable újrafordul ami függ tőle: ```kotlin -data class SalaryCardData( - val isIncome: Boolean, - val item: String, - val price: String -) +Scaffold( + modifier = Modifier.safeDrawingPadding(), + topBar = { + TopBar( + title = stringResource(id = R.string.app_name), + icon = Icons.Default.Clear, + onIconClick = { + salaryItems.clear() + } + ) + }, + floatingActionButton = { + ... ``` -Jól láthatjuk, hogy ennek *data classnak* a paraméterezése, ugyanaz mint a *SalaryCard*-nak. Ez a későbbiekben fontos lesz, ugyanis, ennek a Composable függvénynek, fogjuk átadni a *data class* elemeit. +Ha ezzel megvagyunk, akkor már a következőt kell látnunk az alkalmazásunkban: + +

+ +

+ + +Ezzel a `TopBar` kész is. Folytassuk most a beviteli résszel, amit az oldal alján található `BottomAppBar`-ban fogunk megvalósítani. !!!example "BEADANDÓ (1 pont)" - Készíts egy **képernyőképet**, amelyen látszik a **SalaryCard** Composable, illetve a **data class** Kotlin kódja, a **neptun kódod kommentként**, illetve a Design menü-ben a készített Card. (`Alt`+`Shift`+`Right` billentyű kombinációval érhető ez el, vagy a jobb fölső sarokban a Design elemre kattintva). A képet a megoldásban a repository-ba f1.png néven töltsd föl. + Készíts egy **képernyőképet**, amelyen látszik a **TopBar** *Composable* Kotlin kódja, a **neptun kódod kommentként**, illetve a **futó alkalmazás** (emulátoron vagy a készüléket türközve)! A képet a megoldásban a repository-ba f1.png néven töltsd föl! A képernyőkép szükséges feltétele a pontszám megszerzésének. -## Főképernyő elkészítése -Most már csak a főképernyő van hátra, hogy valamit láthassunk is az alkalmazásból. Ehhez hozzunk létre egy `MainScreen` nevű új Kotlin classt a `screen` packagen belül, majd írjuk meg a főképernyőnek a felépítését az alábbi kód alapján: +### Beviteli mezők megvalósítása (1 pont) -```kotlin -@Composable -fun MainScreen() { - var items by remember { mutableStateOf(emptyList()) } - val context = LocalContext.current - Scaffold ( - topBar = { - TopBar(title = "Android Wallet", - icon = Icons.Default.Clear, - onIconClick = { - items = emptyList() - }) - } - ) { innerPadding -> - Column ( - modifier = Modifier - .fillMaxSize() - .padding(innerPadding) - ){ - var item by remember { mutableStateOf("") } - var price by remember { mutableStateOf("") } - Row( - modifier = Modifier - .fillMaxWidth(), - ){ - //TODO (TextFields) - } - var isIncome by remember { mutableStateOf(false) } +A beviteli részt ezúttal nem egy külön *Composable*-ben, hanem a `MainScreen`-en belüli `Scaffold`-ban, helyben fogjuk megvalósítani. +Ide kerül egy `IconButton`, ami a tranzakció irányát mutatja, majd két `OutlinedTextField` a névnek és az árnak. - Row ( - modifier = Modifier - .fillMaxWidth(), - horizontalArrangement = Arrangement.End, - verticalAlignment = Alignment.CenterVertically, - ){ - //TODO (Buttons) - } +Első lépésként töltsük le a tranzakció irányához használt [erőforrásokat](./downloads/res.zip)! - LazyColumn ( - modifier = Modifier - .fillMaxSize() - .padding(8.dp) +Letöltés után másoljuk be a mappa tartalmát a projektünk erőforrásait tartalmazó res mappába. (...\AndroidWallet\app\src\main\res) + +Illesszük be az alábbi kódot a `Scaffold` megfelelő pontjára: + +```kotlin +bottomBar = { + BottomAppBar { + + Row( + modifier = Modifier + .fillMaxWidth(), + ) { + IconToggleButton( + modifier = Modifier.size(64.dp), + checked = isIncome, + onCheckedChange = { isIncome = !isIncome }, ) { - //TODO (items) + Image( + modifier = Modifier.size(64.dp), + painter = painterResource(id = if (isIncome) R.drawable.ic_income else R.drawable.ic_expense), + contentDescription = "expense/income button" + ) } - } - } + OutlinedTextField( + label = { Text(stringResource(R.string.item)) }, + modifier = Modifier + .padding(start = 8.dp, end = 8.dp) + .weight(2f), + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text), + value = item, + onValueChange = { + item = it + } + ) + OutlinedTextField( + label = { Text(stringResource(R.string.price)) }, + modifier = Modifier + .padding(start = 8.dp, end = 8.dp) + .weight(1f), + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + value = price, + onValueChange = { + price = it + } + ) + } + } } ``` -A `MainScreen` tartalmaz egy *Scaffold*-ot, amivel el tudjuk érni, hogy az elején implementált `TopBar`-t átadjuk a topBar paraméterének. Ezt a következő képpen tesszük meg. Adunk neki egy tetszőleges *title*-t (általában az alkalmazás nevét), ez most *Android Wallet* lesz, majd egy icon-t. Használjuk az Android Studio beépített iconjait. Ezután meg kell adnunk egy Lambdát, aminek a segítségével leírjuk, hogy mi történjen, hogyha a felhasználó rákattint az iconra. Jelen esetben ki kell ürítenünk a listánkat. Mivel mind a két változó kapott egy `by remember {mutableStateOf(...)}` értéket, ezért ha változás történik, akkor az összes Composable újrafordul ami függ attól a változótól. Ha ezzel megvagyunk, a következőt kellene látni a Preview-ben. +Itt szintén elvégezzük a szükséges beállításokat. A legtöbb azért felel, hogy barátibb UI-t láthassunk, viszont a legfontosabb a `value`, valamint az `onValueChange` értéke, ugyanis, ha ezek nincsenek beállítva, akkor nem fog megjelenni a begépelt szöveg. A `value`-nak átadjuk az *item* értéket, valamint az `onValueChange` esetén beállítjuk, hogy minden egyes karakter leütésnél új értéket kapjon a változó. Ezzel érhetjük el azt, hogy futási időben láthassuk a *TextField* értékét. + +!!!note "Állapottól függő megjelenítés" + Az `Image` *painter* attribútumánál jól megfigyelhető a *Jetpack Compose* egyik nagy előnye. A megjelenített képet egy állapottól tesszük függővé, amit ott helyben meg tudunk oldani: + ```kotlin + ... + painter = painterResource(id = if (isIncome) R.drawable.ic_income else R.drawable.ic_expense) + ... + ``` -

- -

-### Beviteli mezők és Gombok megvalósítása (1 pont) +### Új elem felvétele -Ezután fejezzük be a *Scaffold* tartalmát. Elsőként a TextField-eket fogjuk megírni az alábbi kód alapján. (`Ezt a //TODO (TextFields)` helyére kell beírni.) +Új elem felvételét a `FloatingActionButton` gomb megnyomásának hatására fogunk felvenni. Szerencsére a `Scaffold` ennek is biztosít helyet. Illesszük tehát be az alábbi kódot: ```kotlin -OutlinedTextField( - label = { Text("Item") }, - modifier = Modifier - .padding(start = 8.dp, end = 8.dp) - .weight(2f), - singleLine = true, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text), - value = item, - onValueChange = { - item = it - } -) -OutlinedTextField( - label = { Text("Price") }, - modifier = Modifier - .padding(start = 8.dp, end = 8.dp) - .weight(1f), - singleLine = true, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - value = price, - onValueChange = { - price = it +floatingActionButton = { + LargeFloatingActionButton( + shape = CircleShape, + onClick = { + if (item.isNotEmpty() && price.isNotEmpty()) { + salaryItems += SalaryData(isIncome, item, price) + } else { + + Toast.makeText( + context, + "", + Toast.LENGTH_SHORT + ) + .show() + } + }) + { + Image( + imageVector = Icons.Default.Add, + contentDescription = "save button" + ) } -) +}, ``` -Itt szintén elvégezzük a szükséges beállításokat, a legtöbb azért felel, hogy barátibb UI-t láthassunk, viszont a legfontosabb a `value`, valamint az `onValueChange` értéke, ugyanis, ha ezek nincsennek beállítva, akkor nem fog megjelenni a begépelt szöveg. a `value`-nak átadjuk az *item* értéket, valamint az `onValueChange` esetén beállítjuk, hogy minden egyes karakter leütésnél új értéket kapjon a változó. Ezzel érhetjük el azt, hogy futási időben láthassuk a *TextField* értékét. +Az itteni gomb esetén az *onClick* eseményt először egy feltétellel ellenőrizzük. Ha üresen maradt a név vagy az ár, akkor egy `Toast` üzenettel figyelmeztetjük a felhasználót. Ha megfelelő a kitöltés, akkor a *salaryItems* listához hozzáadunk egy új elemet a bevitt adatnak megfelelően. Ez az elem a már korábban definiált `data class` egy példánya. -A következő lépésben, a gombok elhelyezését végezzük el, valamint egy text-et is elhelyezünk, amelyről leolvasható lesz a kiadások/bevételek összege. (Ezt a `//TODO (Buttons)` helyére kell elhelyezni.) +Jelenleg így néz ki a felületünk: -```kotlin -ElevatedButton( - modifier = Modifier.padding(8.dp), - onClick = { isIncome = !isIncome }, - colors = ButtonDefaults.elevatedButtonColors( - if (isIncome) Color.Green else Color.Red - ) -) { - Text(text = if (isIncome) "Income" else "Expense") -} -Button( - modifier = Modifier.padding(8.dp), - onClick = { - if(item.isNotEmpty() && price.isNotEmpty()) { - items += SalaryCardData(isIncome, item, price) - } else { - Toast.makeText(context, "Item and price must be filled", Toast.LENGTH_SHORT).show() - } - } -) { - Text(text = "Save", color = Color.Red) -} -``` +

+ +

+ + +!!!example "BEADANDÓ (1 pont)" + Készíts egy **képernyőképet**, amelyen látszik a **MainScreen** kódja, **a futó alkalmazás** (emulátoron vagy a készüléket türközve), valamint a **neptun kódod a beviteli mezőbe beleírva elemként a listában, vagy a kódban kommentként**! A képet a megoldásban a repository-ba f2.png néven töltsd föl! + + A képernyőkép szükséges feltétele a pontszám megszerzésének. + + +## Lista elkészítése (1 pont) + +###Listaelem létrehozása +A vezérlőink megvannak, azonban ahhoz, hogy a listát elkészítsük, létre kell hoznunk egy listaelemet, amit majd a `LazyColumn`-ben fogunk látni. +- Egy listaelem felépítése: + - Ikon a pénzforgalom irányától függően. + - A megadott megnevezés és alatta az összeg. + - Egy gomb a tétel törlésére. -Az ElevatedButton helyett más fajta gombot is lehet használni, ez csak demonstrálja, hogy a Compose mennyi lehetőséget kínál a tervezés során. A két gombnak szintén beállítjuk az onClick eseményét, valamint a megjelenítendő szöveget rajtuk. A második gomb esetén az onClick eseményt egy if-else elágazásba kell tenni, hogy ha a felhasználó üresen hagyná, akkor ez figyelmeztesse. Ha nem üres, akkor az items listához hozzáadunk egy új elemet a bevitt adatnak megfelelően. Ez az elem a már korábban definiált `data class` egy példánya. +Hozzunk létre egy a `ui/view` *package*-be egy új *Kotlin* fájlt `SalaryCard` néven. -Az elkészített képernyőt állítsuk be a `MainActivity`-ben, úgy hogy a `setContent` ezt a Composable Screent jelenítse meg. +Ennek a következő képpen kell kinéznie: ```kotlin -class MainActivity : ComponentActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - enableEdgeToEdge() - setContent { - AndroidWalletTheme { - MainScreen() +package hu.bme.aut.android.androidwallet.ui.view + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import hu.bme.aut.android.androidwallet.R + +@Composable +fun SalaryCard(isIncome: Boolean, item: String, price: String) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp) + ) { + Image( + modifier = Modifier.size(64.dp), + painter = painterResource(id = if (isIncome) R.drawable.ic_income else R.drawable.ic_expense), + contentDescription = "Income/Expense" + ) + Spacer(modifier = Modifier.size(8.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + fontSize = 24.sp, + fontWeight = FontWeight.Bold, + text = item, + maxLines = 1 + ) + Spacer(modifier = Modifier.size(12.dp)) + Text( + text = price, + color = Color.Gray, + maxLines = 1 + ) + } + Spacer(modifier = Modifier.size(8.dp)) + IconButton( + modifier = Modifier.align(Alignment.CenterVertically), + onClick = { + ///TODO } + ) { + Image( + imageVector = Icons.Default.Delete, + contentDescription = "delete button", + ) } } } + +@Composable +@Preview(showBackground = true) +fun PreviewIncomeSalaryCard() { + SalaryCard(isIncome = true, item = "item", price = "500 Ft") +} + +@Composable +@Preview(showBackground = true) +fun PreviewExpenseSalaryCard() { + SalaryCard(isIncome = false, item = "item", price = "500 Ft") +} ``` -!!!example "BEADANDÓ (1 pont)" - Készíts egy **képernyőképet**, amelyen látszik a **főképernyő** (emulátoron, készüléket tükrözve vagy képernyőfelvétellel), a kódja, valamint a **neptun kódod a kódban kommentként**. A képet a megoldásban a repository-ba f2.png néven töltsd föl. +A *SalaryCard* Composable függvény 3 paramétert tartalmaz: - A képernyőkép szükséges feltétele a pontszám megszerzésének. +- `isIncome - Boolean változó amely a kiadás/bevétel állapotért felel.` +- `item - kiadás/bevétel neve` +- `price - kiadás/bevétel értéke` -### Listaelem példányosítása LazyColumn-ban (1 pont) +A függvényen belül megtalálható egy `Row`, egy `Spacer`, egy `Column`, még egy `Spacer`, valamint egy `ImageButton`. A `Row` felel azért, hogy az elemeket horizontálisan egymás mellé lehessen rakni, a `Column` pedig, hogy az elemeket egymás alá. (A kiadás/bevétel neve, illetve értéke.) A képet egy `Image` *Composable*-lel helyezzük el. Itt a `modifier` segítségével sok fajta beállításra van lehetőség, most csak a size-zal foglalkozunk, hogy átláthatóbb legyen a kód. A `painter` segítségével adhatjuk meg a képet, amit szeretnénk megjeleníteni, ezt egy if-else elágazással oldjuk meg, mégpedig a paraméterként kapott *isIncome* segítségével. Miután megvagyunk az `Image`-dzsel, a `Row`-n belül a `Column`-be elhelyezünk kettő `Text`-et a maradék kettő paraméterrel. A sor végi törlő gomb funkcionalitásának megoldása önálló feladat lesz. -Végül a LazyColumn-ot is befejezzük a következő kód segítségével. (Ezt a `//TODO (items)` helyére kell elhelyezni.) + +### Listaelem példányosítása LazyColumn-ben + +Végül a `MainScreen`-ünkön a `Scaffold` *content* részébe valósítsuk meg a listát: ```kotlin -items(items.size) { - SalaryCard(isIncome = items[it].isIncome, item = items[it].item, price = items[it].price) -} +content = +{ innerPadding -> + Box( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding) + ) { + if (salaryItems.size == 0) { + Text( + text = stringResource(R.string.label_empty_list), + modifier = Modifier.fillMaxSize(), + textAlign = TextAlign.Center, + ) + } else + LazyColumn( + modifier = Modifier + .fillMaxSize() + ) { + items(salaryItems.size) { + SalaryCard( + isIncome = salaryItems[it].isIncome, + item = salaryItems[it].item, + price = "${salaryItems[it].price} Ft", + ) + if (salaryItems.size - 1 > it) { + HorizontalDivider(modifier = Modifier.fillMaxWidth()) + } + } + } + + } +}, ``` -Az `items(..)`-nek egy méretet kell átadni, ami azt jelöli, hogy mekkora a lista, majd a blokk törzsében el kell helyezni azt a Composable elemet amit látni szeretnénk a LazyColumn-ban. Ez a `SalaryCard` Composable lesz, amit már korábban implementáltunk. Ennek paraméterként megadjuk az `it` elemeit. Ezen végig fog iterálni a LazyColumn, és minden elemet ki fog rajzolni a listából. +Itt is megfigyelhetjük, hogy a lista elemszámától (állapotától) függően vagy egy szöveget, vagy a `LazyColumn` listát helyezzük el a felületre. + +A `LazyColumn`-ön belül az `items(..)` függvénynek egy méretet kell átadni, ami azt jelöli, hogy mekkora a lista. Ez után a blokk törzsében el kell helyezni azt a *Composable* elemet amit látni szeretnénk a listában. Ez a `SalaryCard` *Composable* lesz, amit már korábban implementáltunk. Ennek paraméterként megadjuk az *salaryItems* aktuális (`it`) elemeit. Ezen fog végig iterálni a `LazyColumn`, és minden elemet ki fog rajzolni. Ezzel a lépéssel elérkeztünk a kész alkalmazáshoz, és indítás során a következőt kell látnunk:

- +

!!!example "BEADANDÓ (1 pont)" - Készíts egy **képernyőképet**, amelyen látszik a **MainScreen** kódja, az emulátor a működő alkalmazásról, valamint a **neptun kódod egy elemként a listában, vagy a kódban kommentként**. A képet a megoldásban a repository-ba f3.png néven töltsd föl. + Készíts egy **képernyőképet**, amelyen látszik a **LazyColumn** kódja, a futó alkalmazás (emulátoron vagy a készüléket türközve), valamint a **neptun kódod egy elemként a listában, vagy a kódban kommentként**! A képet a megoldásban a repository-ba f3.png néven töltsd föl! A képernyőkép szükséges feltétele a pontszám megszerzésének. ## Önálló feladatok -### Snack bar (1 pont) - -A Toast üzeneteknél már van egy sokkal szebb megoldás is, ez a [Snackbar](https://developer.android.com/develop/ui/compose/components/snackbar). Cseréljük le a Toast figyelmeztetést Snackbarra! - -!!!example "BEADANDÓ (1 pont)" - Készíts egy **képernyőképet**, amelyen látszik a **Snackbar** kódja, az emulátor a működő alkalmazásról, valamint a **neptun kódod egy elemként a listában, vagy a kódban kommentként**, valamint a SnackBar üzenet üres adat bevitele esetén. A képet a megoldásban a repository-ba f4.png néven töltsd föl. - - A képernyőkép szükséges feltétele a pontszám megszerzésének. - ### Összegző mező (1 pont) -Vegyünk fel egy összegző mezőt a gombok mellé, amely minden bevitt érték után frissül. Figyeljünk arra, hogyha nincs még egyetlen bejegyzés sem, akkor ne jelenjen meg semmi, valamint a felhasználó nem mínusz karakter alapján állítja a kiadás/bevétel állapotot, hanem a kapcsoló alapján kell eldöntenünk, hogy pozitív vagy negatív érték. +Vegyünk fel egy összegző mezőt valahova a felületre, amely minden bevitt érték után frissül. Figyeljünk arra, hogyha nincs még egyetlen bejegyzés sem, akkor ne jelenjen meg semmi, valamint a felhasználó nem mínusz karakter alapján állítja a kiadás/bevétel állapotot, hanem a kapcsoló alapján kell eldöntenünk, hogy pozitív vagy negatív érték. !!!tip "Tipp" Érdemes használni a `Modifier.alpha()` paramétert. @@ -338,10 +585,19 @@ Vegyünk fel egy összegző mezőt a gombok mellé, amely minden bevitt érték !!!example "BEADANDÓ (1 pont)" - Készíts egy **képernyőképet**, amelyen látszik az **összegző mező használata** (emulátoron, készüléket tükrözve vagy képernyőfelvétellel), **a kódja**, valamint a **neptun kódod valamelyik termék neveként**. A képet a megoldásban a repository-ba f5.png néven töltsd föl. + Készíts egy **képernyőképet**, amelyen látszik az **összegző mező használata** (emulátoron, készüléket tükrözve vagy képernyőfelvétellel), **a kódja**, valamint a **neptun kódod valamelyik termék neveként**! A képet a megoldásban a repository-ba f4.png néven töltsd föl! A képernyőkép szükséges feltétele a pontszám megszerzésének. +### Egyes elemek törlése (1 pont) + +Az egyes listaelemeken már jelen van a törlés gomb, azonban még nem működik. Valósítsuk meg az egyedi törlést! + +!!!example "BEADANDÓ (1 pont)" + Készíts egy **képernyőképet**, amelyen látszik a **törlés** kódja, az emulátor a működő alkalmazásról, valamint a **neptun kódod egy elemként a listában, vagy a kódban kommentként**! A képet a megoldásban a repository-ba f5.png néven töltsd föl! + + A képernyőkép szükséges feltétele a pontszám megszerzésének. + ### Bonus