Vzorová aplikácia
Vytvorme jednoduchý editor pre syntax Markdown spolu s náhľadom syntaxe.
Prispôsobme ju pre tablety: na telefónoch budeme môcť buď upravovať zdrojový text, alebo prezerať jeho náhľad (v dvoch “aktivitách”), a na tabletoch, kde máme dosť miesta, budeme vidieť zdrojový text i náhľad vedľa seba.
Pozn. na stretnutí sme urobili kostru pre HTML editor. Markdown je jednoduchým rozšírením tohto nápadu.
Výsledný projekt
Zdrojový kód sa nachádza na Githube.
Koncepty, ktoré zvládneme
- fragmenty
- lišta akcií (action bar)
- podpora kompatibilnej knižnice v7
- použitie iných Java knižníc
Postup prác
- pripravíme najprv verziu pre oba “panely” vedľa seba
- potom ju prispôsobíme pre použitie na výšku i na šírku
- potom ju prispôsobíme pre malé zariadenia
Fragmenty
- predstavujú “podaktivity”
- autonómne časti používateľského rozhrania, ktorú možno použiť na viacerých miestach.
- z fragmentov možno poskladať používateľské rozhranie aktivity
- vďaka fragmentov môžeme budovať flexibilné rozhrania použiteľné na mnohých typoch zariadení
Fragmenty v našej aplikácii
- textový editor bude tvoriť jeden fragment
- náhľad bude druhý fragment
- fragmenty môžeme ukladať vedľa seba
- dynamicky vytvárať a zahadzovať
Zdroják a náhľad bok po boku
Štruktúra fragmentu
- fragment je trieda dediaca od
Fragment
- pre zachovanie kompatibility importnime
android.support.v4.app.Fragment
z knižnice kompatibility
- pre zachovanie kompatibility importnime
- fragment potrebuje vlastný XML layout
Fragment č. 1: editor Markdownu
- vyrobíme nový fragment, buď ručne, alebo pomocou File | New Android Object | New Blank Fragment.
XML Layout
Layout je tvorený jediným textovým políčkom
<EditText
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/edit_text_markdown_source"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:inputType="textMultiLine"
android:gravity="top|left"
/>
- nastavíme typ na
textMultiline
, čím indikujeme viacriadkový text - nastavíme centrovanie textu na vľavo-hore, pretože štandardné nastavenie centruje vertikálne.
- nastavenia môžeme kombinovať znakom rúry
|
(klasické C/C++ bitové OR) - pozor, toto je iné nastavenie než
layout_gravity
, ktoré ovplyvňuje centrovanie celého komponentu v celom layoute
- nastavenia môžeme kombinovať znakom rúry
Java
package sk.upjs.ics.android.markdownr;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
public class MarkdownSourceFragment extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_markdown_source, container, false);
}
}
- fragment musí mať jeden implicitný konštruktor (bez parametrov, verejný).
- ukážková trieda ho má, pozor ale na situácie, keď vyrobíme viacparametrový konštruktor
naparsovanie layoutu z XML prebehne v metóde
onCreateView()
- inflater: inštancia nafukovača XML
- container: rodičovský kontajner, do ktorého sa načítajú vnorené komponenty z XML
zavoláme
inflate()
na nafukovači, potrebujeme:- identifikátor layoutu so XML
- kontajner, z ktorého sa preberú parametre layoutu deklarované v XML
- indikátor, či sa majú komponenty automaticky napojiť na rodiča: v tomto prípade napojenie nechceme (nevieme, kto bude v konečnom dôsledku rodičom fragmentu)
- podrobné vysvetlenie: Dave Smith: Layout inflation as Intended
- metóda vráti naparsovaný view
Úprava aplikácie
Aj keď naša aplikácia nie je hotová, už môžeme spustiť hlavnú aktivitu s jediným fragmentom. Upravme activity_main.xml
:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"
android:orientation="horizontal"
>
<fragment
android:id="@+id/fragment_markdown_source"
android:name="sk.upjs.ics.android.markdownr.MarkdownSourceFragment"
android:layout_width="0dip"
android:layout_height="fill_parent"
android:layout_weight="1"
/>
</LinearLayout>
- vytvoríme horizontálny lineárny layout
- pre aktivity bok po boku
- fragment deklarujeme elementom
<fragment>
- uvedieme názov triedy s fragmentom, ktorú sme vytvorili
- uvedieme identifikátor, potrebný pre manažovanie stavu po pauzovaní, či obnovení
- dáme fragmentu váhu: 1 pri jednom komponente znamená, že fragment zaberie celú šírku (toto nie je povinné, ale hodí sa to v ďalšom kroku)
Spustenie aplikácie… neúspešné
Pri prvom spustení [na Androide 2.x] dostaneme výnimku, kde príčina bude:
Caused by: java.lang.ClassNotFoundException:
android.view.fragment in loader
dalvik.system.PathClassLoader
[/data/app/sk.upjs.ics.android.markdownr-1.apk]
Alebo (Android 4.4.2):
Caused by: android.app.Fragment$InstantiationException:
Trying to instantiate a class
sk.upjs.ics.android.markdownr.MarkdownSourceFragment
that is not a Fragment
Príčinou je naša aktivita, ktorá nepodporuje fragmenty. Musíme zmeniť jej triedu.
Hlavná aktivita musí dediť od FragmentActivity
!
public class MainActivity extends FragmentActivity {
...
Spustenie aplikácie II.
- spustí sa aktivita
- textové políčko zaberie celú obrazovku
Fragment č. 2: náhľad
Náhľad vybavíme pomocou zabudovaného webového prehliadača WebView
.
Layout (fragment_markdown_preview.xml
)
<?xml version="1.0" encoding="utf-8"?>
<WebView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/web_view_markdown"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
/>
Komponentu prideľme identifikátor, budeme ho potrebovať neskôr pri nastavovaní obsahu.
Java
package sk.upjs.ics.android.markdownr;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.webkit.WebView;
public class MarkdownPreviewFragment extends Fragment {
private WebView webView;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_markdown_preview, container, false);
webView = (WebView) view.findViewById(R.id.web_view_markdown);
webView.loadData("<i>No Markdown source</i>", "text/html; charset=UTF-8", null);
return view;
}
}
- na rozdiel od predošlého fragmentu si nafúknutý view poznačíme do premennej
- z nej totiž budeme vyťahovať komponent(y) pomocou
findViewById()
WebView
- zabudovaný HTML5 prehliadač
- vychádza z Chrome/Chromium/Webkit
- metódou
loadData()
načítame ľubovoľné stringové dáta ako HTML- uveďme HTML kód
- uveďme MIME typ obsahu (content type) vrátane znakovej sady
- kódovanie vynecháme (
null
)
komponent potrebuje právo internetu. Dodajme do manifestu:
<uses-permission android:name="android.permission.INTERNET" />
Úprava hlavnej aktivity
Do activity_main.xml
dodajme druhý <fragment>
:
<fragment
android:id="@+id/fragment_markdown_preview"
android:name="sk.upjs.ics.android.markdownr.MarkdownPreviewFragment"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_weight="1"
/>
Teraz využijeme aj atribút layout_weight
: dva komponenty s váhou 1 dávajú spolu váhu 2 a teda každý zaberie polovicu šírky nadradeného kontajnera.
Pozorujme správanie
- oba fragmenty sa uložia vedľa seba
- môžeme pozorovať, ako sa zachováva rozloženie i pri rotácii zariadenia
Otázka zlých jazykov
- Keďže v každom fragmente je len jediný komponent, pokojne by sme sa obišli aj bez nich; komponenty by sme ukladali vedľa seba v layoute
- Fragmenty sa však tvária ako kontajnery komponentov (v Swingu: panely, v HTML:
<div>
y)
Odlišný layout pri orientácii na výšku
- na malých zariadeniach vyzerá výzor bok po boku veľmi divne
- strašne málo miesta pre monitor
- urobme samostatný layout pre malé displeje i pre veľké
Kvalifikátory (qualifiers)
- umožňujú definovať layouty, konštanty a pod. pre rozličné typy zariadení
- príklady:
- layout-sw600dp: layouty pre zariadenia so šírkou aspoň 600 dp (typizovaný 7palcový tablet)
- layout-sw600dp-land: ako v predošlom bode, ale na šírku
Modifikácia layoutov
- vytvorme adresár
res/layout-sw600dp-land
- Vezmime
res/layout/activity_main.xml
a skopírujme ho doň. Tento dvojpanelový layout použijeme pre horizontálne rotované tabletové zariadenie - upravme
res/layout/activity_main.xml
: vyhoďme z neho deklaráciu pravého (náhľadového fragmentu) - otestujme to na rozličných zariadeniach
Pozorovanie
- Android automaticky načíta správny XML layout podľa zariadenia
- v Java kóde nemusíme nič meniť
Dynamické fragmenty
- na malých displejoch buď zobrazíme zdroják alebo náhľad
- vyriešime to dynamickými fragmentami
- lebo staticky deklarované fragmenty v XML nemožno odstraňovať, ani pridávať za behu
- dynamicky budeme zamieňať fragment zdrojáku za fragment náhľadu a naopak.
- na obrazovke bude vidno stále len jeden fragment
Úprava hlavnej aktivity
Upravme res/layout/activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"
android:orientation="horizontal"
android:id="@+id/layout_root"
/>
- vymazali sme všetky deklarácie fragmentov
- pridali sme však identifikátor! (Budeme ho potrebovať o chvíľu)
Kód
- Identifikátor layoutu (
R.id.layout_root
) sa nachádza v layoute pre jediný fragment. Duálne zobrazenie tento identifikátor nemá. Takto vieme zistiť, v akom režime sa nachádzame.
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); if(isSinglePane()) { showMarkdownSource(); } }
Metóda na zistenie je jednoduchá, overí existenciu komponentu s identifikátorom:
public boolean isSinglePane() {
return findViewById(R.id.layout_root) != null;
}
Ak sme v dvojpanelovom režime, nemusíme robiť nič špeciálne. Načítalo sa totiž XML s fixnými dvoma fragmentmi.
Dynamické nahrádzanie fragmentov: transakcie
- Metóda
showMarkdownSource()
nahradí obsah fragmentu. - dynamické zmeny fragmentov bežia v transakcii:
- postupnosť zmien, ktorá musí prebehnúť celá
- zaradí sa do fronty udalostí v hlavnom vlákne
Kód
private void showMarkdownSource() {
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.layout_root, new MarkdownSourceFragment())
.commit();
}
- získame inštanciu fragment managera (metóda support vráti variant z knižnice kompatibility)
- začneme transakciu
- nahradíme komponent layoutu našim fragmentom. Tu využijeme opäť identifikátor z XML
- transakciu komitneme
Prepínanie medzi dynamickými fragmentami [bonus]
Plán:
- vytvoríme tlačidlo v menu / na action bare
- vytvoríme
enum
so stavmi: zdroják, náhľad, oba - zabezpečíme prepínanie stavov
Položka menu / tlačidlo na actionbare
Upravme res/menu/main.xml
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/show_source_menu"
android:title="Show source"
/>
<item android:id="@+id/show_html_menu"
android:title="Preview"
/>
</menu>
- dodali sme dve položky
- budeme ich dynamicky skrývať a zobrazovať
Položky menu
- menu budeme budovať dynamicky
- využijeme metódu
onCreateOptionsMenu()
- dynamicky usúdime, čo sa má zjaviť a čo nie
Menu vytvoríme klasicky:
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
getMenuInflater().inflate(R.menu.main, menu);
MenuItem showHtmlMenu = menu.findItem(R.id.show_html_menu);
MenuItem showHtmlSourceMenu = menu.findItem(R.id.show_source_menu);
if(mode == Mode.SOURCE) {
showHtmlMenu.setVisible(true);
showHtmlSourceMenu.setVisible(false);
} else if (mode == Mode.PREVIEW) {
showHtmlMenu.setVisible(false);
showHtmlSourceMenu.setVisible(true);
} else {
showHtmlMenu.setVisible(false);
showHtmlSourceMenu.setVisible(false);
}
return true;
}
Položky budeme skrývať a zobrazovať po výbere:
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch(item.getItemId()) {
case R.id.show_html_menu:
showHtml();
supportInvalidateOptionsMenu();
return true;
case R.id.show_source_menu:
showMarkdownSource();
supportInvalidateOptionsMenu();
return true;
}
return super.onOptionsItemSelected(item);
}
Dôležité je zavolať supportInvalidateOptionsMenu
(Android 4.x: invalidateOptionsMenu()
), ktorým úplne prebudujeme menu i položky na lište akcií.
Enum a prepínanie stavov
- po výbere položky menu v
onOptionsItemSelected()
prepneme indikátor a zavoláme príslušnú zmenu fragmentu doplníme metódu na zobrazenie HTML:
private void showHtml() { getSupportFragmentManager() .beginTransaction() .replace(R.id.layout_root, new MarkdownPreviewFragment()) .commit(); mode = Mode.PREVIEW; supportInvalidateOptionsMenu(); }
Sledujme správanie
- menu sa dynamicky skrýva a zobrazuje v lište akcií podľa potreby
- tlačidlá sa tiež dynamicky schovávajú
Knižnica kompatibility v7
- štandardný projekt pre Android 2.x vie využívať knižnicu kompatibility
- a používať veci “z budúcnosti”
- knižnica kompatibility je štandardne
v4
:- čítame “veci z nového Androidu” použiteľné dokonca v API Level 4″ (Android 1.6)
- niektoré nové veci však nemôžu byť v takomto starom API použiteľné
- príklad: ActionBar
- novšia verzia: knižnica kompatibility
v7
: požadujúca minimálne API Level 7 (2.1/Froyo)
Ako importnúť knižnicu?
- knižnica sa dodáva ako JAR
- ten však neobsahuje resources, teda grafické elementy, ktoré chceme používať
Importneme
V Eclipse:
- File | Import… | Existing Android Project…
importneme z útrob Android SDK, napr.
c:\android\sdk\extras\android\support\v7\appcompat
Vznikne tak nový projekt v našom workspace.
Riešenie problémov
- ak sa projekt sťažuje, že je nastavený na konkrétny API Level (napr. 16):
- buď stiahneme príslušný API level cez správcu SDK
- alebo pravý klik na tento projekt, Properties, sekcia Android a vyberme Project Build Target na nejaký iný než je nastavený, stlačme OK. Následne opäť proces zopakujme, a vyberme Project Build Target na taký API Level, oproti ktorému kompilujeme projekty (typicky najnovší Android).
- inak povedané: vyberme ľubovoľný PBT, potvrďme dialóg, a potom ho vráťme späť.
- cheat je prebratý zo StackOverflow
Využitie knižnice v našom projekte
Projekt importnime do nášho projektu:
- pravý klik na tento projekt, Properties, sekcia Android
- sekcia Library
- pomocou tlačidla Add… pridajme knižnicu kompatibility
Riešenie problémov
- Ak sa projekt sťažuje, že je chyba v SHA-1 (uvidíme to v Eclipse, vo view Console), kliknime na adresár libs v našom projekte a odstráňme (Delete) súbor
android-support-v4.jar
Lišta akcií / Action Bar v Android 2.x
- vďaka knižnici kompatibility v7 môžeme mať lištu akcií i v starom Androide
- aktivita, ktorá chce lištu akcií, musí oddediť od
ActionBarActivity
Spustenie aktivity
Uvidíme výnimku:
java.lang.RuntimeException: Unable to start activity ComponentInfo
{sk.upjs.ics.android.markdownr/
sk.upjs.ics.android.markdownr.MainActivity}:
java.lang.IllegalStateException:
You need to use a Theme.AppCompat theme (or descendant)
with this activity.
Musíme upraviť manifest: nastavme tému pre aplikáciu <application>
:
android:theme="@style/Theme.AppCompat.Light.DarkActionBar"
- Táto téma bude svetlá, s tmavou lištou akcií.
- Preskúšajte aj iné možnosti.
Obdivovanie lišty
- Lišta vyzerá analogicky ako v novších Androidoch.
- Tlačidlo s troma bodkami (vpravo) je však stotožnené s ponukou menu v tradičnom duchu Androidu 2.X
- Na Androide 4.x však lišta vyzerá korektne.
Bonus: odovzdávanie informácií medzi fragmentami
- chceme dodávať do fragmentu informácie pri jeho vytváraní
- nemôžeme však využiť konštruktor s viacerými parametrami
- presnejšie, môžeme, ale musíme si vytvoriť aj bezparametrový konštruktor
- Android ho využíva pri dynamickom vytváraní inštnacií
- presnejšie, môžeme, ale musíme si vytvoriť aj bezparametrový konštruktor
- namiesto toho môžeme využiť settery
- alebo ešte lepšie argumenty
- argumenty totiž prežijú aj rekonfiguráciu fragmentu vďaka zabudovanej podpore
onSaveInstanceState()
- viď StackOverflow
- argumenty totiž prežijú aj rekonfiguráciu fragmentu vďaka zabudovanej podpore
Riešenie: statická továrenská metóda
private static final String ARG_KEY_HTML_DATA = "htmlData";
...
public static MarkdownPreviewFragment newInstance(String htmlData) {
MarkdownPreviewFragment fragment = new MarkdownPreviewFragment();
Bundle args = new Bundle();
args.putString(ARG_KEY_HTML_DATA, htmlData);
fragment.setArguments(args);
return fragment;
}
Vyzdvihnutie argumentov
private static final String DEFAULT_HTML = "<i>No Markdown source</i>";
private String getHtmlData() {
Bundle args = getArguments();
if(args != null && args.containsKey(ARG_KEY_HTML_DATA)) {
return args.getString(ARG_KEY_HTML_DATA);
}
return DEFAULT_HTML;
}
Načítanie textu
webView.loadData(getHtmlData(), "text/html", "utf-8");
Použitie z hlavnej aktivity
Upravíme transakciu pri zobrazovaní HTML:
private void showHtml() {
EditText htmlSourceEditText = (EditText) findViewById(R.id.edit_text_markdown_source);
MarkdownPreviewFragment fragment = MarkdownPreviewFragment.newInstance(htmlSourceEditText.getText().toString());
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.layout_root, fragment)
.commit();
mode = Mode.PREVIEW;
}
- najprv z aktuálneho layoutu pre zdrojový kód v Markdowne vytiahneme textové políčko
- získame z neho text
- vytvoríme inštanciu fragmentu statickou metódou, ktorú sme pred chvíľou napísali
- spustíme transakciu
Bonus: udržiavanie stavov
- naše fragmenty sa riadia životným cyklom
- môžu byť odstreľované a vytvárané nanovo
- zatiaľ fungujeme zle:
- skúste otočiť zariadenie, keď je vidieť len fragment so zdrojákom (text sa stratí)
- skúste zobraziť náhľad a potom opäť zdroják (text sa stratí)
- vyriešime to obídením: všetko budeme ukladať do
SharedPreferences
- týmto zaistíme, že dáta prežijú aj reštart telefónu
- samozrejme, toto nie je ideálne, mali by sme využiť klasické mechanizmy
Metódy v MarkdownSourceFragment
@Override
public void onResume() {
super.onResume();
SharedPreferences preferences = getActivity().getPreferences(Activity.MODE_PRIVATE);
String htmlSource = preferences.getString(PREFERENCE_KEY_MARKDOWN_SOURCE, "");
markdownSourceEditText.setText(htmlSource);
}
@Override
public void onPause() {
SharedPreferences preferences = getActivity().getPreferences(Activity.MODE_PRIVATE);
Editor editor = preferences.edit();
editor.putString(PREFERENCE_KEY_MARKDOWN_SOURCE, markdownSourceEditText.getText().toString());
editor.commit();
super.onPause();
}
Bonus: prevod pomocou knižnice
Nechávame na pozorného čitateľa.
Využiť sa môže knižnica MarkdownPapers.
Konfigurácia tlačidiel na lište akcií
- action bar pojme všetky položky z menu
- môžeme ale prispôsobiť výzor a niektoré z nich uviesť ako tlačidlá na lište
Konfigurácia v XML [Android 2.x]
- v Android 4.x stačí dodať atribút
android:showAsAction
- v starých Androidoch musíme definovať vlastný menný priestor v XML
- príklad: definujeme menný priestor
app
s preddefinovanou hodnotou atribút
showAsAction
bude z nového priestoru:<menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" > <item android:id="@+id/show_source_menu" android:title="Show source" app:showAsAction="ifRoom" />
showAsAction="ifRoom"
: položka menu sa zobrazí ako tlačidlo na lište akcií, ak pre ňu bude miesto.
Bonus: rozbehanie konverzie
- do adresára
libs
skopírujememarkdownpapers-core-1.4.2.jar
- nemá žiadne závislosti, okrem bežnej Javy
- konverziu urobíme pomocou
AsyncTask
u a vlastnej metódy. - v
doInBackground()
vykonáme konverziu (beží na pozadí, v samostatnom vlákne) - v
onPostExecute()
aktualizujeme používateľské rozhranie (beží v hlavnom vlákne) chyby propagujeme do hlavného vlákna ako HTML
private void refreshPreview() { AsyncTask<String, Void, String> task = new AsyncTask<String, Void, String>() { @Override protected String doInBackground(String... params) { StringReader in = new StringReader(params[0]); StringWriter out = new StringWriter(); try { Markdown md = new Markdown(); md.transform(in, out); } catch (ParseException e) { return "<i>Syntax error</i>"; } return out.toString(); } @Override protected void onPostExecute(String result) { WebView webView = (WebView) findViewById(R.id.web_view_markdown); webView.loadData(result, MarkdownPreviewFragment.CONTENT_TYPE, MarkdownPreviewFragment.ENCODING); } }; task.execute(getSourceCode()); }
- metódu voláme pri prepnutí do HTML režimu (
showHtml()
) - je vhodné dodať tretie tlačidlo “Refresh” dostupné len v dvojpanelovom režime, ktorým aktualizujeme HTML