Android Studio

Stiahnite si Android Studio pre svoj OS, podľa pokynov rozbaľte a inštalujte.

Android Studio Setup Wizard

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).

Nový projekt

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).

Príprava zariadenia

Na spustenie máte 2 možnosti:

  1. pripojiť telefón/tablet a spustiť na svojom zariadení
  2. spustiť v emulátore

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.

Príprava spustenia na telefóne

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.

image_caption

Ak sa tam telefón neobjaví, skontrolujte postup v dokumentácii. Môžete pokračovať v spustení aplikácie.

Príprava spustenia na emulátore

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.

Spustenie aplikácie

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!

helloworld

O čom by boli úvodné slajdy:

Verzie SDK

Gradle

layout

Android Jetpack

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.

layout

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.

Android projekt

layout

layout

layout

layout

Po 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).

MainActivity.java

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.

/res/values/strings.xml

<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:

/res/layout/activity_main.xml

<?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>

Vysvetlenia k layoutu:

Vo finále to bude vyzerať takto:

layout

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:

Game.java

/**
 * 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);

}

Slovník slov, ktoré sa majú uhádnuť

Databázu si ukážeme neskôr. Zatiaľ budú všetky slová zapísané v resource priečinku.

/res/values/strings.xml

<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é:

HangmanGame.java

public 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:

HangmanGame.java

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.

Kliknutie

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.

Hádanie písmena

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:

toast

Aktualizácie widgetov

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());
}

Štart novej hry

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();
}

string resource

Koniec hry

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:

MainActivity.java

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

lifecyclestavlogovanie

Úloha na experimentovanie

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.

MainActivity.java

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:

Ú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:

Game.java

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.

shared preferences

V nasledujúcom kóde vidíte ukážku ako uložiť informáciu o najlepšom čase hrania hry.

MainActivity.java

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.

time

Odmeriame čas od začiatku hry po koniec. Pôvodný interface neupravujeme, ale iba doplníme triedu HangmanGame.

HangmanGame.java

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.

MainActivity.java

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.

/res/values/strings.xml

<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()).

MainActivity.java

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.

Kód na gitlabe

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.

Easter eggs v Androide

Kliknite viackrát na verziu Androidu v nastaveniach telefónu

Zaujímavé premenné/metódy v triedach: