ConstrainLayout
a prácu s widgetmiresources
- okrem layoutu aj obrázky a stringyToast
ako vyskakovacie oznámenie používateľoviBundle
SharedPreferences
na rýchle uloženie dát vo formáte kľúč-hodnotaAlertDialog
Stiahnite si Android Studio pre svoj OS, podľa pokynov rozbaľte a inštalujte.
Spustite Android Studio – ak je potrebné, tak urobte plugin updates.
Počas nastavovania môžete zvoliť Standard inštaláciu s predvolenými nastaveniami. Zvyšné potrebné veci viete doinštalovať neskôr (napr. knižnice pre konkrétne verzie Androidu).
Podľa pokynov môžete nakonfigurovať aj hardware acceleration – odporúčam urobiť tak až po ukončení Setup Wizard (na konci sa budú sťahovať rôzne súbory).
Vytvorte nový projekt. Z galérie si môžete vybrať Empty Views Activity.
Ďalšie nastavenia môžete nechať predvolené (prípadne si zmeniť názov aplikácie, jeho umiestnenie a podľa chuti jazyk Java/Kotlin). V tomto projekte budeme používať Javu, avšak jazyky je možné miešať.
Počkajte, kým sa ukončí sťahovanie Gradle a iných vecí (vidieť na lište úplne dole) a project sync (žltá lišta hore).
Voliteľne môže vybehnúť ponuka na zvýšenie heap size pre lepší výkon IDE (vyžaduje reštart Android studia).
Na spustenie máte 2 možnosti:
Môžete si zvoliť ktorúkoľvek možnosť. Emulátor je v mnohom výhodnejší, kedže si viete vyskúšať aplikáciu na rôznych typoch zariadení, s rôznymi verziami Androidu, rozlíšením a inými nastaveniami.
Odporúčam zistiť si svoju verziu Androidu (podľa toho budú niektoré kroky vyzerať rôzne) – zistíte to v nastaveniach. Od Android 11 je možné spúšťať aplikácie aj cez Wi-Fi (vyžaduje to ďalšiu konfiguráciu). Na začiatok je vhodné pripojiť telefón/tablet cez kábel.
Na telefóne si v nastaveniach zapnite Developer options a povoľte USB debugging. Developer options sú za bežných okolností neviditeľné. V nastaveniach v sekcii About Phone je potrebné 7x ťuknúť na Build Number. Viac detailov v dokumentácii. Pri niektorých modeloch (napr. Xiaomi Mi 10) je potrebné mať vloženú sim kartu na aktivovanie USB debugging.
Na Windowse je potrebné doinštalovať OEM BUS drivers.
Po pripojení telefónu k PC je zo skúsenosti potrebné povoliť prístup k dátam (teda nie len na nabíjanie) a povoliť USB debugging pre konkrétny PC. Ak je všetko v poriadku, tak v hornej lište by mala byť viditeľná nová možnosť s modelom konkrétného telefónu (viď obrázok). Trvá to krátku chvíľu, kým sa to tam objaví po pripojení k PC.
Ak sa tam telefón neobjaví, skontrolujte postup v dokumentácii. Môžete pokračovať v spustení aplikácie.
V hornej lište alebo v menu Tools nájdete Device manager (v starších verziách Android Studio sa to volá AVD manager).
Vytvorte si nové virtuálne zariadenie. V ponuke sú predvolené modely, generické podľa veľkosti obrazovky a tiež možnosť nakonfigurovať si vlastný hardvérový profil. Odporúčam niektorý z vybraných modelov, ktorý má pri sebe ikonu Play Store (ja zvyknem na cvičeniach používať Nexus 5, pretože má menší displej a je vhodnejší na prezentovanie – vyberte si ľubovoľnú verziu).
V ďalšej ponuke je potrebné zvoliť verziu Androidu a stiahnúť príslušné súbory (ja použijem najnovšiu verziu, môžete si zvoliť aj staršiu – ale z odporúčaných system image, ktoré sú kompatibilné s google play).
Ostatné nastavenia môžu ostať predvolené. Spustenie zariadenia je intuitívne. Môžete si prezrieť nastavenia, čo sa dá s emulátorom robiť (vrátanie simulovania prichádzajúceho hovoru). Emulátor sa otvára ako samostatná aplikácia, ak ho chcete mať vložený v Android Studiu, na lište vpravo dole sa viete dostať do nastavení, kde to zmeníte.
Zeleným tlačidlom Run App môžete spustiť aplikáciu.
V mojom prípade vybehla notifikácia build failed. Celú správu si môžete prečítať v okne Build (nájdete ho na dolnej lište). Hláška informovala, že sa nepodarilo nainštalovať nejaké balíčky, lebo nebola akceptovaná licencia.
V tom prípade otvorte SDK manager (na hornej lište vpravo alebo v menu Tools). V kategórii SDK tools nainštalujte Android SDK Command-line Tools (latest). Opakujte spustenie aplikácie – tentokrát sa nainštalujú potrebné veci pred spustením.
V ideálnom prípade na telefóne alebo v emulátore vidíte aplikáciu s textom Hello World!
O čom by boli úvodné slajdy:
CompileSdkVersion
– použité pri kompilácii, odporúča sa najnovšia verzia, zmena neovplyvní správanie aplikácie za behuMinSdkVersion
– minimálne požiadavka overovaná Google PlayTargetSdkVersion
- voči akej verzii je appka testovaná. Ak je API na zariadení väčšie, Android môže spustiť compatibility behaviormin (lowest possible) <= target == compile (latest SDK)
Groovy
alebo Kotlin
Jetpack is a suite of libraries to help developers follow best practices, reduce boilerplate code, and write code that works consistently across Android versions and devices so that developers can focus on the code they care about.
build.gradle (Module:app)
Aktivita
je základným komponentom Android aplikácie. Zatiaľ si môžeme asociovať aktivitu s oknom, ktoré vidíme v aplikácii. Neskôr budeme mať viac aktivít, resp. jedna aktivita bude používať fragmenty
na modulové zobrazenie UI.
Z dokumentácie: An activity is the entry point for interacting with the user. It represents a single screen with a user interface.
xml
súbor, základné nastavenia projektuxml
alebo iné súbory (png
a pod.)layout
- rozloženie widgetov v aktivitevalues
- stringy, štýly, farbyPo vytvorení nového projektu s jednou Empty views activity si doplníme niektoré resources. Jazyk zvolíme Java
, ostatné nastavenia môžu ostať predvolené (napr. verzia Androidu).
Krok č. 1: Obrázky šibenice stiahneme a rozbalíme do priečinka res/drawable
.
Krok č. 2: V hlavnej aktivite si v poli uložíme čísla pre jednotlivé resources (obrázky).
public class MainActivity extends AppCompatActivity {
private final int[] gallowsIds = {
R.drawable.gallows0,
R.drawable.gallows1,
R.drawable.gallows2,
R.drawable.gallows3,
R.drawable.gallows4,
R.drawable.gallows5,
R.drawable.gallows6
};
...
Krok č. 3: Doplníme si vybrané stringy v resources.
<resources>
...
<string name="insert_a_letter">Insert a letter</string>
<string name="gallows_image">Gallows Image</string>
</resources>
Android odporúča ukladať všetky stringy z aplikácie v tomto súbore. V prípade zmeny jazyka aplikácie/telefónu je možné načítať iný resource s preloženými slovami.
Ako hlavný kontajner pre layout použijeme ConstraintLayout
, kde nastavujeme jednotlivé constraints pre všetky widgety. To je možné urobiť klikaním v GUI pri súbore /res/layout/activity_main.xml
alebo písaním xml
kódu.
ConstraintLayout
je súčasťou Android Jetpack, čo sú často používané prvky v Androide. Ešte o tom budeme počuť. Dopad na našu aplikáciu je v tom, že je potrebné definovať dependency. Túto máme automaticky danú ak sme využili empty aktivitu pri vytváraní projektu. Inak dependencies nájdeme vo File->Project Structure
alebo priamo v súbore build.gradle (Module :app)
. Gradle je niečo podobné ako Maven. Ten, ktorý používame je písaný v jazyku Groovy.
V posledných rokoch sa dostáva do popredia tzv. Compose, čo umožňuje iný prístup k tvorbe layoutu. Preto sa nemusíme veľmi venovať vytváraniu layoutu, ale skúsiť si to môžete. Nakoniec si skopírujte kód:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<ImageView
android:id="@+id/imageViewGallows"
android:layout_width="0dp"
android:layout_height="0dp"
android:src="@drawable/gallows0"
android:contentDescription="@string/gallows_image"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/textViewGuessedWord"
/>
<TextView
android:id="@+id/textViewGuessedWord"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:textAllCaps="true"
android:textAppearance="@android:style/TextAppearance.Large"
android:typeface="monospace"
android:letterSpacing="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/imageViewGallows"
app:layout_constraintBottom_toTopOf="@id/editTextLetter"
/>
<EditText
android:id="@+id/editTextLetter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:textAppearance="@android:style/TextAppearance.Large"
android:typeface="monospace"
android:gravity="center"
android:hint="@string/insert_a_letter"
android:maxLength='1'
android:inputType="textCapCharacters"
android:textColor="@color/black"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/textViewGuessedWord"
app:layout_constraintBottom_toBottomOf="parent"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
ConstraintLayout
má definovanú šírku a výšku, ktoré zapĺňajú celú obrazovku. Veľkosť widgetu môže byť jedna z nasledovných možností: width="100dp"
). dp
je density-independent pixel, lebo klasický px môže mať rôznu veľkosť na rôznych zariadeniach. Pri textoch sa používa jednotka sp
(scale-independent pixel).ConstraintLayout
. Zapíše sa napr. layout_width="0dp"
@id/...
sa odkazujeme na nejaké konkrétne ID, pomocou @+id/...
vytvárame nové ID.id
).ImageView
má definovaný src
atribút. Zatiaľ to je jeden obrázok šibenice, budeme to v priebehu hry dynamicky meniť.TextView
zobrazuje hádané slovo. Sú tam rôzne nastavenia textu - veľké písmená, rozostupy medzi znakmi, väčší font, monospace (aby každý znak zaberal rovnaké miesto).EditText
slúži na prijatie vstupu od používateľa. inputType
naznačí aká klávesnica sa použije (vyskúšajte si password, number a pod.). Okrem toho sa definuje, že sa môže vložiť iba jeden znak.Vo finále to bude vyzerať takto:
Aj kvôli demonštrícii práce s interface v Kotline, aj kvôli oddeleniu hernej logiky a zobrazenia na androide, nastavíme aplikáciu tak, že bude obsahovať interface Game
, kde sú definované metódy, ktoré poskytuje objekt hry. Každá hra (a stav tej hry) je reprezentovaná objektom nejakej triedy, ktorý bude implementovať toto rozhranie.
Neuhádnutý znak je kódovaný _
a počet pokusov je 6
(k týmto pokusom máme rôzne obrázky šibenice).
Prezrite si jednotlivé metódy:
/**
* Objekt danej triedy obsahuje informacie o hladanom slove aj o aktualnom stave hry - uhadnuta cast slova, pocet zostavajucich pokusov.
* V konstruktore vygenerujte nahodne slovo, ktore sa bude hladat. Mozete pouzit zoznam stringov, z ktoreho vyberiete nahodny.
*/
public interface Game {
/**
* Defaultny pocet pokusov na zaciatku hry.
*/
int DEFAULT_ATTEMPTS_LEFT = 6;
/**
* Kodovany neuhadnuty znak. Na zaciatku je slovo zlozene z tychto znakov.
*/
char UNGUESSED_CHAR = '_';
/**
* Oznaci ci je hra skoncena.
*
* @return true ak hra skoncila vitazne - slovo bolo uhadnute.
*/
boolean isWon();
/**
* Vrati aktualny retazec.
*
* @return neuhadnute znaky v retazci nie su odhalene, pouzije sa UNGUESSED_CHAR
*/
CharSequence getGuessedCharacters();
/**
* Aktualne hladane slovo.
*
* @return hladane slovo.
*/
String getChallengeWord();
/**
* Vrati zostavajuci pocet pokusov.
*
* @return pocet pokusov
*/
int getAttemptsLeft();
/**
* Hrac zadal dane pismeno. Spracuje sa jeho tip.
*
* @param character pismeno.
* @return true ak uhadol.
*/
boolean guess(char character);
}
Databázu si ukážeme neskôr. Zatiaľ budú všetky slová zapísané v resource priečinku.
<resources>
...
<string-array name="dictionary">
<item>android</item>
<item>java</item>
<item>kotlin</item>
<item>upjs</item>
<item>mississippi</item>
</string-array>
</resources>
/res/values/strings.xml poskytuje priestor na sústredenie všetkých textov z aplikácie na jedno miesto. Je to výhodné hlavne pri viacjazyčných aplikáciach. Môžeme mať verziu tohto súboru napr. pre anglický a slovenský jazyk.
V tomto xml
súbore je možné mať viacero pokročilých vecí, napr. Quantity strings (zero, one, two, few, many,...) ale aj zvýrazňovanie textu, formátovanie, špeciálne znaky.
Rozhranie implementujeme pomocou novej triedy, ktorá bude obsahovať 3 inštančné premenné:
word
- slovo, ktoré chceme uhádnuťwordInProgress
- aktuálny stav hry - uhádnuté písmená sú odkryté, neuhádnuté majú _
attemptsLeft
- počet zostávajúcich pokusovpublic class HangmanGame implements Game{
private String word;
private StringBuilder wordInProgress;
private int attemptsLeft;
public HangmanGame(String[] words) {
}
@Override
public boolean isWon() {
return false;
}
@Override
public CharSequence getGuessedCharacters() {
return null;
}
@Override
public String getChallengeWord() {
return null;
}
@Override
public int getAttemptsLeft() {
return 0;
}
@Override
public boolean guess(char character) {
return false;
}
}
Niektoré z metód sú iba gettre na inštančné premenné (aj keď občas s iným názvom, prípadne typom. CharSequence
je interface implementovaný aj triedou String
, aj StringBuilder
).
V konštruktore je potrebné nastaviť úvodné hodnoty premenných. V tomto návrhu je na vstupe celý zoznam slov, kde sa vyberie náhodné slovo. Ak ste doteraz používali na náhodné celé číslo iba Math.random()
, odporúčam pozrieť na triedu Random
.
public HangmanGame(String[] words) {
Random random = new Random();
int index = random.nextInt(words.length);
word = words[index];
wordInProgress = new StringBuilder();
for (int i = 0; i < word.length(); i++) {
wordInProgress.append(UNGUESSED_CHAR);
}
}
Samotné hádanie písmena vyriešime prechodom slovom. V prípade, že niekto uhádol písmeno A
, a znovu to písmeno skúsi hádať, tak je to ok, nestráca život. V prípade ak sa také písmeno v slove nenachádza, tak metóda vráti false
. Vo výsledku môžeme mať takýto kód:
@Override
public boolean guess(char character) {
boolean success = false;
for (int i = 0; i < word.length(); i++) {
if (word.charAt(i) == character) {
wordInProgress.setCharAt(i, character);
success = true;
}
}
if (!success) {
attemptsLeft--;
}
return success;
}
Výsledný kód bude vyzerať takto:
import java.util.Random;
public class HangmanGame implements Game{
// java
private String word;
// _a_a
private StringBuilder wordInProgress;
private int attemptsLeft;
public HangmanGame(String[] words) {
Random random = new Random();
int index = random.nextInt(words.length);
word = words[index];
wordInProgress = new StringBuilder();
for (int i = 0; i < word.length(); i++) {
wordInProgress.append(UNGUESSED_CHAR);
}
attemptsLeft = DEFAULT_ATTEMPTS_LEFT;
}
@Override
public boolean isWon() {
return word.equals(wordInProgress.toString());
}
@Override
public CharSequence getGuessedCharacters() {
return wordInProgress;
}
@Override
public String getChallengeWord() {
return word;
}
@Override
public int getAttemptsLeft() {
return attemptsLeft;
}
@Override
public boolean guess(char character) {
boolean success = false;
for (int i = 0; i < word.length(); i++) {
if (word.charAt(i) == character) {
wordInProgress.setCharAt(i, character);
success = true;
}
}
if (!success) {
attemptsLeft--;
}
return success;
}
}
V MainActivity.java
prepojíme layout s kódom. Metóda onCreate
sa spustí pri vytvorení aktivity. Existujúci kód obsahuje volanie setContentView(R.layout.activity_main)
, ktorý povie aký layout sa má vykresliť. Následne vieme v tejto metóde získať aj referencie na objekty prislúchajúce jednotlivým widgetom:
private ImageView imageGallows;
private TextView text;
private EditText inputEditText;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
imageGallows = findViewById(R.id.imageViewGallows);
text = findViewById(R.id.textViewGuessedWord);
inputEditText = findViewById(R.id.editTextLetter);
Trieda R
je vygenerovaná automaticky a obsahuje číselné referencie na jednotlivé resources.
Pre doplnenie funkcionality potrebujeme vedieť spustiť novú hru a obslúžiť kliknutie. V tejto verzii bude potvrdenie zadaného písmena vykonané po kliknutí na obrázok šibenice.
V metóde onCreate
pridáme listener na kliknutie:
imageGallows.setOnClickListener(view -> onImageClick());
Kedysi sa definovala onClick metóda priamo v xml layoute. To je od Android 31 deprecated. Podľa dokumentácie je potrebné implementovať triedu, ktorá reaguje na kliknutie. V jave to môžeme urobiť anonymnou triedou alebo pomocou lambda výrazu (to je ten kus kódu vyššie). Lambda výrazy v jave ponúkajú zjednodušenie niektorých situácii, v Kotline sa s tým stretneme v oveľa väčšej intenzite. Tento kód hovorí, že implementujeme triedu s jednou metódou (tak je nastavený interface), kde na vstupe metódy je jeden parameter (nazveme ho view
) a vykoná sa to, čo je za znakom ->
. V našom prípade tam bude viac vecí, preto sa tam volá iná metóda.
V metóde onImageClick
prepojíme interface, resp. jeho implementáciu. Nezabudnite si doplniť inštančnú premennú:
private Game game;
Po kliknutí sa z widgetu EditText
zoberie písmeno, ak tam je, tak sa nastaví na lowerCase
, vymaže sa obsah EditText
, aby to nemusel robiť používateľ. Ak to je písmeno, tak zavoláme metódu game.guess(letter)
. Podľa odpovede aktualizujeme text, ak sa písmeno uhádlo, alebo šibenicu ak to nebol dobrý pokus. Výsledná metóda vyzerá takto:
private void onImageClick() {
CharSequence text = inputEditText.getText();
if (text == null || text.length() == 0) {
Toast.makeText(this, R.string.insert_a_letter, Toast.LENGTH_SHORT).show();
return;
}
char letter = Character.toLowerCase(text.charAt(0));
inputEditText.setText("");
if (letter < 'a' || letter > 'z') {
Toast.makeText(this, "PISMENO", Toast.LENGTH_SHORT).show();
return;
}
boolean success = game.guess(letter);
if (success) {
updateText();
} else {
updateImage();
}
}
Toast
slúži na krátke zobrazenie informácie používateľovi:
Atribúty widgetom sa dajú nastavovať nie len v xml súbore, ale aj v kóde. V tomto prípade implementujeme dve metódy, ktoré sme už použili pri kliknutí na šibenicu:
private void updateImage() {
int index = Game.DEFAULT_ATTEMPTS_LEFT - game.getAttemptsLeft();
imageGallows.setImageResource(gallowsIds[index]);
}
private void updateText() {
text.setText(game.getGuessedCharacters());
}
Pri vytvorení aktivity sa má spustiť nová hra. Prezieravo jednotlivé veci dávame do samostatných metód. Neskôr sa to ukáže ako výhoda. Teda doplníme:
@Override
protected void onCreate(Bundle savedInstanceState) {
...
startNewGame();
}
private void startNewGame() {
String[] words = getResources().getStringArray(R.array.dictionary);
game = new HangmanGame(words);
updateImage();
updateText();
}
Po kliknutí na šibenicu je potrebné overť, či nedošlo ku koncu hry. Prípadne pri ďalšom kliknutí spustiť novú hru. Ako bonus môžeme podfarbiť šibenicu zelenou alebo červenou farbou použitím LightingColorFilter
. Ten je založený na násobení a sčítaní farieb. Čierna má hodnotu 0, biela 255 vo všetkých troch zložkách. Prenásobením inou farbou čierna ostane čiernou a biela sa zmení.
private void onImageClick() {
if (game.isWon() || game.getAttemptsLeft() == 0) {
startNewGame();
return;
}
...
boolean success = game.guess(letter);
if (success) {
updateText();
if (game.isWon()) {
imageGallows.setColorFilter(new LightingColorFilter(Color.GREEN, Color.BLACK));
}
} else {
updateImage();
if (game.getAttemptsLeft() == 0) {
imageGallows.setColorFilter(new LightingColorFilter(Color.RED, Color.BLACK));
}
}
}
Finálny kód pre fungujúcu aktivitu vyzerá takto:
import androidx.appcompat.app.AppCompatActivity;
import android.graphics.Color;
import android.graphics.LightingColorFilter;
import android.os.Bundle;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
private final int[] gallowsIds = {
R.drawable.gallows0,
R.drawable.gallows1,
R.drawable.gallows2,
R.drawable.gallows3,
R.drawable.gallows4,
R.drawable.gallows5,
R.drawable.gallows6
};
private Game game;
private ImageView imageGallows;
private TextView text;
private EditText inputEditText;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
imageGallows = findViewById(R.id.imageViewGallows);
imageGallows.setOnClickListener(view -> onImageClick());
text = findViewById(R.id.textViewGuessedWord);
inputEditText = findViewById(R.id.editTextLetter);
startNewGame();
}
private void onImageClick() {
if (game.isWon() || game.getAttemptsLeft() == 0) {
startNewGame();
return;
}
CharSequence text = inputEditText.getText();
if (text == null || text.length() == 0) {
Toast.makeText(this, R.string.insert_a_letter, Toast.LENGTH_SHORT).show();
return;
}
char letter = Character.toLowerCase(text.charAt(0));
inputEditText.setText("");
if (letter < 'a' || letter > 'z') {
Toast.makeText(this, "PISMENO", Toast.LENGTH_SHORT).show();
return;
}
boolean success = game.guess(letter);
if (success) {
updateText();
if (game.isWon()) {
imageGallows.setColorFilter(new LightingColorFilter(Color.GREEN, Color.BLACK));
}
} else {
updateImage();
if (game.getAttemptsLeft() == 0) {
imageGallows.setColorFilter(new LightingColorFilter(Color.RED, Color.BLACK));
}
}
}
private void startNewGame() {
// vygenerovat slovo, vybrat z databazy
String[] words = getResources().getStringArray(R.array.dictionary);
// objekt stavu hry
game = new HangmanGame(words);
// update obrazku
updateImage();
// update slova - textView
updateText();
imageGallows.setColorFilter(null);
}
private void updateImage() {
int index = Game.DEFAULT_ATTEMPTS_LEFT - game.getAttemptsLeft();
imageGallows.setImageResource(gallowsIds[index]);
}
private void updateText() {
text.setText(game.getGuessedCharacters());
}
}
Aktivita v Androide má svoj životný cyklus
Vyskúšajte si prekryť aj iné metódy ako je onCreate
a do jednotlivých metód pridať logovací výpis. Sledujte, ktorá metóda sa zavolá ak ... (napr. otvoríte aplikáciu, medzitým niekto zavolá, príde sms a pod.)
Problém - spustite aplikáciu. Rozohrajte hru tak, aby šibenica bola v nejakom inom ako počiatočnom stave, aby v slove bolo aspoň jedno písmeno uhádnuté a aby bolo zadané nové písmeno na hádanie. Otočte displej na šírku.
EditText
zachoval svoj obsah. ImageView
a TextView
sa zmenil.
Vysvetlenie - pri zmene konfigurácie (v tomto prípade z Portrait režimu na Landscape) sa aktivita reštartovala = zavolá sa metóda onCreate
(viete si to overiť logovaním). Widgety si ukladajú svoj stav, ale volanie onCreate
spôsobí, že sa spustí nová hra.
Riešenie - Bundle
je mapa, kde kľúčom sú stringy a hodnotami rôzne Parcelable
veci. Pri zničení aktivity sa volá metóda onSaveInstanceState
s parametrom typu Bundle
- tam uložíme aktuálny stav hry. Pri spustení aktivity máme Bundle
ako parameter metódy onCreate
. Pri úplne prvom spustení je tam null
. Kľúč do mapy si zvolíme ľubovoľný, môžeme ho dať do premennej ako konštantu.
import androidx.annotation.NonNull;
public class MainActivity extends AppCompatActivity {
private static final String BUNDLE_KEY = "Game";
...
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
imageGallows = findViewById(R.id.imageViewGallows);
imageGallows.setOnClickListener(view -> onImageClick());
text = findViewById(R.id.textViewGuessedWord);
inputEditText = findViewById(R.id.editTextLetter);
if (savedInstanceState == null) {
startNewGame();
} else {
game = (Game) savedInstanceState.getSerializable(BUNDLE_KEY);
updateImage();
updateText();
}
}
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putSerializable(BUNDLE_KEY, game);
}
...
}
Aby tento kód fungoval, potrebujeme zaručiť schopnosť objektov z interface Game
tváriť sa ako hodnota v Bundle
.
Interface Parcelable
umožňuje zabaliť objekty. Podobný interface je aj Serializable
a ten vieme v Bundle
tiež použiť. Serializable
:
Parcelable
(vyrába nejaké dočasné objekty)Serializable
. Typicky bežne používané java triedy túto vlastnosť spĺňajú.Úprava rozhrania Game
je preto veľmi jednoduchá. Serializable
nastavujeme nie len pre HangmanGame
ale už v interface, teda je potrebné rozšíriť (extends
) pôvodné rozhranie. Trieda, ktorá implementuje interface Game
, musí implementovať aj rozhranie Serializable
:
import java.io.Serializable;
public interface Game extends Serializable {
...
}
Otáčanie obrazovky už teraz nebude problém.
V Jave sme boli zvyknutí, že ak potrebujeme niečo uložiť, tak jednoduchý spôsob je použiť súbory. V Androide to nezvykne byť prvá možnosť.
Android má SQLite databázu, ktorá je lokálna. K nej sa dá pristúpiť elegantne pomocou knižnice Room
(ukážeme si onedlho). Okrem toho je možné ukladať veci do rôznych typov úložísk podľa toho, či majú byť prístupné aj z iných aplikácii. Časť z toho budeme mať pokryté v ďalších aplikáciach.
SharedPreferences umožňujú jednoduché a rýchle ukladanie dát typu kľúč & hodnota.
V nasledujúcom kóde vidíte ukážku ako uložiť informáciu o najlepšom čase hrania hry.
import android.content.Context;
import android.content.SharedPreferences;
public class MainActivity extends AppCompatActivity {
...
private void updateBestTime(long time) {
SharedPreferences pref = getPreferences(Context.MODE_PRIVATE);
long bestTime = pref.getLong("time", Long.MAX_VALUE);
if (time < bestTime) {
SharedPreferences.Editor editor = pref.edit();
editor.putLong("time", time);
editor.apply();
announceBestTime(time);
}
}
...
}
Môžete si všimnúť, že pri čítaní z mapy má metóda getLong
dva parametre - kľúč a default hodnotu v prípade, že taký kľúč v mape nie je. Pri zápise sa používa editor, ktorý robí zápis. String time
si môžeme uložiť aj ako konštantnú hodnotu, podobne ako bundle key.
Metódu announceBestTime
doplníme neskôr.
Odmeriame čas od začiatku hry po koniec. Pôvodný interface neupravujeme, ale iba doplníme triedu HangmanGame
.
import android.os.SystemClock;
public class HangmanGame implements Game{
...
private long startTime;
...
public HangmanGame(String[] words) {
...
startTime = SystemClock.elapsedRealtime();
}
public long getTime() {
return SystemClock.elapsedRealtime() - startTime;
}
...
}
Nie každá implementácia rozhrania Game
poskytuje metódu getTime
. Je preto potrebné urobiť pretypovanie. Volanie instanceof
overí, či sa v danej premennej nachádza referencia na zadanú triedu alebo jej potomka.
public class MainActivity extends AppCompatActivity {
...
private void onImageClick() {
...
if (game.isWon()) {
imageGallows.setColorFilter(new LightingColorFilter(Color.GREEN, Color.BLACK));
if (game instanceof HangmanGame) {
HangmanGame hangmanGame = (HangmanGame) game;
long time = hangmanGame.getTime();
updateBestTime(time);
}
}
...
}
...
}
Dialóg v Androide je vyskakovacie okienko, ktoré okrem základného podania informácie umožňuje aj interakciu a môže byť ľubovoľne dizajnované. V tomto prípade si vystačíme s jednoduchou ukážkou s využitím AlertDialog
.
Okrem toho si môžete všimnúť pokročilejšie využitie strings.xml
, kde je v stringu your_time
vložená hodnota %d
, ktorá označuje celé číslo. Viac o možnostiach formátovania numerického výstupu si môžete pozrieť napr. v java dokumentácii.
<resources>
...
<string name="best_time">BEST TIME</string>
<string name="your_time">Your time is %dms</string>
</resources>
Podobne ako SharedPreferences.Editor
, tak aj pomocou AlertDialog.Builder
najprv nastavíme požadované vlastnosti dialógu a až následne sa vytvorí. Pri dialógoch je potrebné urobiť navyše aj zobrazenie (show()
).
public class MainActivity extends AppCompatActivity {
...
private void announceBestTime(long time) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.best_time);
builder.setMessage(getString(R.string.your_time, time));
AlertDialog dialog = builder.create();
dialog.show();
//builder.setTitle("").setMessage("").create().show();
}
...
}
Metódy, ktoré má builder
sú urobené tak, že vždy vrátia referenciu na AlertDialog.Builder
. To umožňuje urobiť aj zreťazené volania - viď zakomentovaný kód vyššie.
Prezentácia, ďalšie aplikácie sú na stránke https://ics.science.upjs.sk/vma/.
Nasleduje rovnaká aplikácia, ale zapísaná v jazyku Kotlin.
Kliknite viackrát na verziu Androidu v nastaveniach telefónu
Zaujímavé premenné/metódy v triedach: