Week 03

AlertDialog, zmena orientácie aktivity, životný cyklus aktivity, verzia SDK

Úvod

  • http://sli.do#ki-smart - anketa a otázky pre prednášajúceho
  • https://join.slack.com/t/ki-smart/signup - slack pre rýchlu komunikáciu
  • hackaton - zatiaľ nie sú žiadne ďalšie informácie
  • coderfest - už budúci utorok, zaregistrujte sa čo najskôr!
  • Časť 118 CZ Podcastu je venovaná firemným hackatonom. Trvá niečo vyše 30 minút a zúčastnení sa rozprávajú o tom, ako hackatony fungujú vo firemnom prostredí, čo im prinášajú a či je to vlastne celé dobré ;)
  • oss víkend
    • o Linuxe, ale nie len o ňom
    • ak ste sa stretli s niečím (nie niekým) zaujímavým, príďte o tom porozprávať - je to skúsenosť prísť porozprávať pred nie veľmi veľkú skupinu ľudí
  • prvý šprint
    • webová reprezentácia šprintu pribudne zajtra a bude pribúdať priebežne - agile
    • user stories
    • diskutujte!
  • kultúra - Blade Runner 2049

Torch Application - Part III

  • Zatiaľ sme spravili len kostru aplikácie, aj keď sa na obrazovke zariadenia zobrazuje všetko potrebné. Zariadenie však nesvieti. A keď zariadenie nesvieti, nebude ani žiadny profit. Dnes sa teda pozrieme na to, ako naše zariadenie rozsvietiť. A samozrejme - kopec vecí okolo.

Adding Dialog

  • Minule sme pri zistení, že zariadenie neobsahuje blesk, zavreli aktivitu a o vzniknutej situácii sme používateľa informovali zobrazením Toast-u. Sú však samozrejme aj iné spôsoby, ako je možné používateľa oboznámiť. Jednou z nich je aj vytvoriť dialógové okno, ktoré zmizne až po tom, čo na ňom používateľ stlačí príslušné tlačidlo.

  • zobrazenie dialógového okna s potrebnou informáciou môže vyzerať napr. takto:

    AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
    
    builder.setTitle("Error")
            .setMessage("Sorry, your device doesn't support flash light!")
            .setPositiveButton("OK", new DialogInterface.OnClickListener() {
                public void onClick(DialogInterface dialog, int which) {
                    // closing the application
                    finish();
                }
            });
    AlertDialog dialog = builder.create();
    dialog.show();

Changing orientation

  • Pozrieme sa teda na ďalšie spôsoby využitia zdrojov. Dnes začneme zmenou vzhľadu aplikácie vzhľadom na zmenu orientácie zariadenia (na výšku (portrait) a na šírku (landscape)).

  • Ak teda chceme zmeniť vzhľad aktivity vzhľadom na orientáciu zariadenia, cez menu Android Studia File > New > Android Resource File zobrazíme dialógové okno na vytvorenie nového zdroja. Ako názov zadáme názov existujúceho layout súboru pre hlavnú aktivitu (activity_main.xml). Zvyšok prostredie vyplní za nás. Z kvalifikátorov vyberieme už len Orientation a zvolíme voľbu Landscape.

  • Na pozadí sa v projekte vytvoril v priečinku res/ priečinok res/layout-land, do ktorého bol uložený práve vytvorený layout.

  • Návrhy sa vyberú automaticky po zmene orientácie v prípade, ak neexistuje verzia návrhu v priečinku res/layouts/. Názvy návrhov musia byť rovnaké ako tie na výšku. To môžeme rovno vyskúšať spustením takto upravenej aplikácie a pri otočení zariadenia na ležato, sa zobrazí zatiaľ prázdny layout.

  • bez zmeny kódu teda vytvoríme len nový návrh, ktorý bude vyzerať takto:

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal" android:layout_width="match_parent"
    android:layout_height="match_parent">
    
    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/image"
        android:src="@drawable/bulb_off"/>
    
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Turn On"
        android:id="@+id/button"
        android:layout_gravity="right"
        android:onClick="toggle"/>
    </LinearLayout>

Activity Lifecycle

  • S našou aplikáciou však máme stále jeden veľký problém - po zmene orientácie dôjde k resetnutiu stavu aktivity (spustí sa nanovo). To nie je veľmi dobré, nakoľko môžeme stratiť veľmi dôležité údaje (napr. výsledok športovej činnosti po 30min, stav rozohratej hry, …).

  • Problém by bolo možné vyriešiť zakázaním zmeny orientácie aplikácie. Toto je možné zabezpečiť pridaním nasledujúceho atribútu pre potrebnú aktivitu v súbore s manifestom aplikácie:

    <activity android:name=".MainActivity"
              android:screenOrientation="portrait">
  • Aby sme lepšie porozumeli tomu, prečo je to vlastne tak, pozrime sa na životný cyklus aktivity (slide). Aktivita sa teda môže nachádzať v jednom z týchto stavov:

    • starting - V tomto momente dochádza k spusteniu aktivity a teda jej vytvoreniu ako objektu. Volajú sa postupne metódy onCreate(), onStart() a onResume()
    • running - Stav, v ktorom aplikácia vykonáva svoju činnosť v tzv. UI vlákne - je viditeľná a aktívna.
    • paused - Aplikácia stratila fokus, napr. je prekrytá inou nie plneobrazovkovou priesvitnou aktivitou. Aplikácia je teda čiastočne viditeľná a drží svoj stav. Pri prechode do tohto stavu volá metódu onPause().
    • stopped - Ak je aktivita kompletne prekrytá inou aktivitou, je v tomto stave. Stále síce obsahuje svoje členské premenné, ale nie je používateľovi viditeľná.
    • destroyed - Stav, kedy sa aktivita ukončuje (buď s povolením používateľa alebo automaticky). Ak sa znova zobrazí používateľovi, aktivita je vytvorená nanovo.
  • Basically, whenever Android destroys and recreates your Activity for orientation change, it calls onSaveInstanceState() before destroying and calls onCreate() after recreating. Whatever you save in the bundle in onSaveInstanceState, you can get back from the onCreate() parameter.

  • Vytvoríme teda metódu onSaveInstanceState() a uložíme v nej stav baterky:

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putBoolean("state", this.isOn);
    }
  • A následne upravíme aj metódu onCreate(), v ktorej v prípade, že bol stav aktivity uložený, tak si ho odpamätáme:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    
        if(savedInstanceState != null){
            this.isOn = savedInstanceState.getBoolean("state");
            if(this.isOn == true){
                ImageView image = (ImageView) findViewById(R.id.image);
                Button button = (Button) findViewById(R.id.button);
    
                image.setImageResource(R.drawable.bulb_on);
                button.setText(R.string.turn_off);
            }
        }else{
            this.isOn = false;
        }
    }
  • Tento fragment kódu sa nám čiastočne prekrýva s metódou toggle(), takže môžeme urobiť menší refaktoring:

    • vytvoríme súkromnú metódu render(), ktorá len vyrenderuje aktivitu na základe aktuálneho stavu (na základe hodnoty členskej premennej isOn, a
    • aktualizujeme metódu toggle(), z ktorej presunieme všetok fragment kódu týkajúci sa renderovania do metódy render().
  • Metóda toggle() teda bude vyzerať nasledovne:

    public void toggle(View view){
        player.start();
        this.isOn = !this.isOn;
        render();
    }
  • Metóda render() bude po refaktoringu vyzerať nasledovne:

    private void render(){
        Button button = (Button) findViewById(R.id.button);
        ImageView image = (ImageView) findViewById(R.id.image);
        Camera.Parameters params = this.camera.getParameters();
    
        if(this.isOn == true) {
            image.setImageResource(R.drawable.bulb_on);
            button.setText(R.string.turn_off);
    
            params.setFlashMode(Camera.Parameters.FLASH_MODE_TORCH);
            this.camera.setParameters(params);
            this.camera.startPreview();
        }else{
            image.setImageResource(R.drawable.bulb_off);
            button.setText(R.string.turn_on);
    
            params.setFlashMode(Camera.Parameters.FLASH_MODE_OFF);
            this.camera.setParameters(params);
            this.camera.stopPreview();
        }
    }
  • Následne metóda onCreate() bude vyzerať takto:

Obtaining and Realeasing Camera

  • Ako sme spomínali, kameru je možné získať pomocou volania: java Camera camera = Camera.open();

  • Akonáhle otočím jej orientáciu, zosype sa opäť s výnimkou java.lang.RuntimeException a s hláškou: “Fail to connect to camera service”.

  • Ak si spomenieme na životný cyklus aktivity (slide), pri otočení zariadenia dochádza k ukončeniu a znovuvytvoreniu aktivity. To znamená, že ak chceme, aby novovytvorená aktivita mala prístup ku kamere a jej blesku, potrebueje toto zariadenie pri ukončení aplikácie uvoľniť. Ináč ho nebude môcť používať nie len aktivita našej aplikácie, ale ani žiadne iné aplikácie.

  • Kameru teda uvoľníme volaním:

    camera.release();
  • Je však potrebné a dôležité kameru uvoľniť resp. získať na správnom mieste, resp. v správnom čase. Môžeme sa pozrieť znovu na životný cyklus aktivity a nájsť spoločne správne miesto (metódu), v ktorej kameru uvoľníme a rovnako tak správne miesto (metódu), v ktorej kameru získame.

  • Za vhodné miesto pre uvoľnenie kamery sa javí metóda onPause(). Jej kód môže vyzerať nasledovne:

    @Override
    protected void onPause() {
        super.onPause();
    
        if (this.camera != null) {
            this.camera.release();
        }
    }
  • Podobne - vhodné miesto pre získanie objektu kamery sa javí metóda onResume(), ktorej kód následne môže vyzerať takto:

    @Override
    protected void onResume() {
        super.onResume();
    
        this.camera = Camera.open();
        this.params = this.camera.getParameters();
    
        update();
    }
  • Ak sme postupovali správne, baterka pri otočení zariadenia zhasne a následne sa rozsvieti znova pri znovuvytvorení aktivity.

Conclusion

  • Až teraz môžeme povedať, že je aplikácia hotová. Bolo by ešte možné vytvoriť niekoľko úprav (napr. zrušiť orientáciu na šírku, aby aplikácia zbytočne neblikala), ale tu sa už dá hovoriť o profite.

Additional Resources