Egy-egy Activity felületére tehetünk egy-egy View-t, amelyre a későbbiekben komponensként hivatkozunk, hiszen az Android felületén a View olyan, mint Swing esetén a JComponent. Egy Activity felületére egy időben csak egy View-t tehetünk, amely az esetek nagy részében ViewGroup, amelybe további View-okat tehetünk, így alakíthatjuk ki az AWT/Swing esetén már megszokott komponensfát.
3.1. A View általánosságban
Mint említettem, a View nagyjából azt tudja, amit a JComponens Swing alatt: ez az osztály minden más komponens őse. Az leszármazott Activity példányunk szoros kapcsolatban van egy View osztály példányával, mégpedig a setContentView metóduson át:
public class HelloActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); TextView textView = new TextView(this); textView.setText("Hello, JavaForum.hu!"); this.setContentView(textView); } }
Ha nem adunk meg semmi különleges paramétert, akkor az átadott View pontosan kitölti az Activity által nyújtott területet. Saját komponenseket a View leszármaztatásával tudunk készíteni.
3.2. Komponensek
A felhasználó által látható View-okat nevezzük komponenseknek, ezek közé tartoznak a feliratok, a gombok, a szövegbeviteli mezők és a többi hasonló komponens. Minden komponens a View osztályból származik, amelyről a későbbiekben bővebben is szót ejtek.
3.2.1. TextView
A TextView komponens feladata egyszerű: a számára átadott szöveget írja ki címkék vagy többsoros szövegblokk formájában. Egy szöveget lehet neki átadni - például StringBuilder használatával, ugyanis a legtöbb megszokottJava API hívás a rendelkezésünkre áll. A programban szereplő LinearLayout-ról kicsit később szót ejtünk, egyelőre elég annyi, hogy alapesetben a komponenseket egymás mellé pakolja:
super.onCreate(savedInstanceState); LinearLayout layout = new LinearLayout(this); TextView textView = new TextView(this); StringBuilder sb = new StringBuilder(); sb.append("Hello, I'm the "); sb.append(this.getTitle()); sb.append("!\n"); sb.append("árvíztűrő tükörfúrógép"); sb.append("\n"); sb.append("ÁRVÍZTŰRŐ TÜKÖRFÚRÓGÉP"); textView.setText(sb.toString()); textView.setTextColor(Color.BLUE); textView.setBackgroundColor(Color.GREEN); layout.addView(textView); setContentView(layout);
A program kimenete a mobil képernyőjén:
3.2.2. Button
A feliratok után a nyomógombok a legtöbbet használt, s egyben a legősibb felhasználói interfészek. Hozzunk létre három nyomógombot, rendre Ok, Cancel és Next feliratokkal:
super.onCreate(savedInstanceState); LinearLayout layout = new LinearLayout(this); Button okButton = new Button(this); okButton.setText("Ok"); okButton.setEnabled(false); layout.addView(okButton); Button cancelButton = new Button(this); cancelButton.setText("Cancel"); layout.addView(cancelButton); Button nextButton = new Button(this); nextButton.setText("Next"); layout.addView(nextButton); setContentView(layout);
Az Ok gomb le lesz tiltva, tehát nem tudjuk megnyomni. Nézzük ezt a mobil kijelzőjén:
3.2.3. ToggleButton
A kapcsolható gomb (kapcsológomb) úgy működik, hogy állapotát megőrzi a lenyomások során, vagyis felváltva lenyomva vagy felengedve marad.
super.onCreate(savedInstanceState); LinearLayout layout = new LinearLayout(this); ToggleButton releasedButton = new ToggleButton(this); layout.addView(releasedButton); ToggleButton pressedButton = new ToggleButton(this); pressedButton.setTextOn("Incoming"); pressedButton.setTextOff("Incoming"); pressedButton.setChecked(true); layout.addView(pressedButton); setContentView(layout);
Ha ennek a gombtípusnak nem adunk át szöveget, akkor az ON vagy az OFF szöveget látja a felhasználó, természetesen a gomb állapotának megfelelően. A komponensnek át tudunk adni saját szöveget is a textOn és a textOffmetódussal, ez hasznos, ha szűrőként szeretnénk használni a kapcsológombunkat:
3.2.4. CheckBox
A jelölőnégyzet hasonló célokat szolgál, mint a kapcsológomb, de más a megjelenése. Általában ha rövid szöveggel jellemezhető a kapcsolandó dolog, akkor kapcsológombot használunk; ha hosszabb szöveggel kell magyaráznunk, akkor pedig jelölőnégyzetet. Ebből adódik az a különbség is, hogy a jelölőnégyzetnek adnunk kell szöveget, amely a ki- vagy bekapcsolható dolgot magyarázza meg; kapcsológomb esetén egy külön TextView kell a magyarázathoz.
super.onCreate(savedInstanceState); LinearLayout layout = new LinearLayout(this); CheckBox uncheckedBox = new CheckBox(this); uncheckedBox.setText("Check this!"); layout.addView(uncheckedBox); CheckBox checkedBox = new CheckBox(this); checkedBox.setText("Uncheck this!"); checkedBox.setChecked(true); layout.addView(checkedBox); setContentView(layout);
Nézzük meg a mobil kijelzőjén:
3.2.5. RadioButton
A rádiógombok a régi rádiókon lévő gombokról kapták a nevüket, ugyanis csak egy lehet egy időben lenyomva közülük, amint egy újabbat nyomunk le, az előzőleg lenyomott gomb jelölése megszűnik. Akkor célszerű használni őket, ha a felhasználónak választania kell több dolog közül, de csak egy opciót választhat. Ebből következően a rádiógombok csak csoportban érzik jól magukat, a csoportot egy RadioGroup fogja össze:
super.onCreate(savedInstanceState); LinearLayout layout = new LinearLayout(this); RadioGroup group = new RadioGroup(this); RadioButton firstButton = new RadioButton(this); firstButton.setText("First"); group.addView(firstButton); RadioButton secondButton = new RadioButton(this); secondButton.setText("Second"); group.addView(secondButton); secondButton.setChecked(true); RadioButton thirdButton = new RadioButton(this); thirdButton.setText("Third"); group.addView(thirdButton); layout.addView(group); setContentView(layout);
Mint látjuk, a csoporthoz kell hozzáadni a rádiógombokat, majd a csoportot rendeljük hozzá ahhoz a konténerhez, amibe szánjuk a gombokat. Általában célszerű a felhasználónak egy alapértelmezett lehetőséget adni, de ügyeljünk rá, hogy a csoporthoz való hozzáadás után állítsuk be az alapértelmezetten bejelölt gombot. Nézzük a kijelzőn:
3.2.6. Chronometer
Ha szükségünk lenne eltelt időt mérni, akkor a Chronometer egy példányára lesz szükségünk, az alábbi esetben az alkalmazás indítása óta eltelt időt tudjuk kiírni (feltéve, ha az alábbi kódrészletet az onCreate metódusba írjuk):
super.onCreate(savedInstanceState); LinearLayout layout = new LinearLayout(this); Chronometer chronometer = new Chronometer(this); chronometer.start(); layout.addView(chronometer); setContentView(layout);
A kijelzőn egyszerűen egy perc:másodpercszámláló jelenik meg, és kezd számolni:
3.2.7. AnalogClock és DigitalClock
Ha ingerünk támadna analóg vagy digitális órát használni különösebb munka nélkül, akkor használhatjuk a kész komponenseket:
super.onCreate(savedInstanceState); LinearLayout layout = new LinearLayout(this); AnalogClock analogClock = new AnalogClock(this); layout.addView(analogClock); DigitalClock digitalClock = new DigitalClock(this); layout.addView(digitalClock); setContentView(layout);
S mindez a kijelzőn:
3.2.8. EditText
Felhasználó által gépelt szöveg beviteléhez használhatjuk az EditText komponenst, amely képes többsoros bevitelt is kezelni. Ha nem adunk meg méretet, görgetősávot vagy különösebb elrendezést, akkor a benne lévő szöveg határozza meg a méretét.
super.onCreate(savedInstanceState); LinearLayout layout = new LinearLayout(this); EditText editText = new EditText(this); editText.setText("árvíztűrő tükörfúrógép"); layout.addView(editText); setContentView(layout);
S mindez a képernyőn:
3.2.9. ImageView
Képek megjelenítéséhez az ImageView komponenst használhatjuk, egyszerűen át kell neki adni a megjelenítendő kép referenciáját... ám ilyet még nem csináltunk. A feladat nem nehéz, a res könyvtárban létre kell hoznunk egy drawable mappát és abba tehetjük a képeket, a platform pedig gondoskodik arról, hogy a következő fordításkor aktualizálja az R.java állományt, amelybe belekerül a kép is, a változó neve a kép nevét veszi fel kiterjesztés nélkül:
package hu.javaforum.android; public final class R { public static final class attr { } public static final class drawable { public static final int logo=0x7f020000; } public static final class layout { public static final int main=0x7f030000; } public static final class string { public static final int app_name=0x7f040000; } }
Nem kell mást tennünk, mint meghatározni a kép címét:
super.onCreate(savedInstanceState); LinearLayout layout = new LinearLayout(this); ImageView imageView = new ImageView(this); imageView.setImageResource(R.drawable.logo); layout.addView(imageView); setContentView(layout);
A kijelzőn pont akkora méretben jelenik meg, amekkora a kép tényleges felbontása:
3.2.10. ImageButton
Az ImageView és a nyomógomb keveréke, amely lehetővé teszi, hogy olyan nyomógombot tegyünk ki a kijelzőre, amelyben szöveg helyett egy kép van:
super.onCreate(savedInstanceState); LinearLayout layout = new LinearLayout(this); ImageButton imageButton = new ImageButton(this); imageButton.setImageResource(R.drawable.logo); layout.addView(imageButton); setContentView(layout);
A kijelzőn a keret mutatja mindössze, hogy gombról van szó:
3.2.11. Egyéb komponensek
A részletezett komponenseken kívül találhatunk még ProgressBar-t, SeekBar-t, autocomplete beviteli mezőt, és egyéb apróságot, érdemes konzultálni a részletes referencia leírással ez ügyben, mivel a komponensek száma minden egyes kiadott verzióval növekszik...
3.3. Mindez XML alapokon
A res mappában a layout alatt található XML fájlokkal eddig nem foglalkoztunk, most vessünk az ott figyelő main.xml állományra egy hosszabb pillantást:
<?xml version="1.0" encoding="UTF-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent"> <TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="Hello Android from NetBeans"/> </LinearLayout>
Ezt a tartalmat a NetBeans plugin generálta a projekt létrehozásakor, és a projekt fordításakor ebből egy Activity által használható elrendezést fog generálni, amely belekerül az R.java fájlba is:
public static final class layout { public static final int main=0x7f030000; }
S erre az erőforrásra a programunkban tudunk hivatkozni, mint View:
package hu.javaforum.android; import android.app.Activity; import android.os.Bundle; public class HelloActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); } }
A programot futtatva igen erős összefüggést kell látnunk a main.xml és a képernyő között:
A res mappa alatti elemekre is tudunk hivatkozni az XML állományból, ehhez a szöveg értékét @ jellel kell kezdenünk és az R.java állomány neveit kell használnunk. Módosítsuk a strings.xml állományt az alábbiak szerint:
<?xml version="1.0" encoding="UTF-8"?> <resources> <string name="app_name">HelloJavaForum</string> <string name="hello">Hello, I\'m the HelloJavaForum!</string> </resources>
Majd fordítsuk le a projektet, hogy a környezet legenerálja az újabb R.javaforrást:
package hu.javaforum.android; public final class R { public static final class attr { } public static final class drawable { public static final int logo=0x7f020000; } public static final class layout { public static final int main=0x7f030000; } public static final class string { public static final int app_name=0x7f040000; public static final int hello=0x7f040001; } }
Ebben már szerepel a korábban hozzáadott logo nevű kép hivatkozása, illetve a most hozzáadott hello nevű szöveg. Módosítsuk a main.xml állományt:
<?xml version="1.0" encoding="UTF-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent"> <TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="@string/hello"/> </LinearLayout>
A kulcsszó itt a @string/hello szöveg, amely az R.javaállomány megfelelő sorára mutat, s egyéb erőforrásokat is ilyen egyszerűséggel tudunk meghivatkozni. Futtassuk újra a programot, s nézzük meg az eredményt:
Felmerülhet a kérdés, hogy az XML fájlban leírt komponenseket miképp tudjuk elérni a programból, hiszen az R.java forrásában nem szerepelnek. Ennek oka, hogy csak azok az erőforrások kerülnek bele az R.java leíróba, amelyeknek adunk azonosítót, vagyis kitöltjük az android:id attribútum értékét:
<?xml version="1.0" encoding="UTF-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" android:id="@+main/layout"> <TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="@string/hello" android:id="@+main/textView"/> </LinearLayout>
Az id mezőre annyi megkötés van, hogy a @+ után kell következzen két név '/' jellel elválasztva. A '/' jel két oldalán lévő szövegből megszokott Java változók lesznek, amelyet meg is tekinthetünk az újra legenerált R.java állományban:
package hu.javaforum.android; public final class R { public static final class attr { } public static final class drawable { public static final int logo=0x7f020000; } public static final class layout { public static final int main=0x7f030000; } public static final class main { public static final int layout=0x7f050000; public static final int textView=0x7f050001; } public static final class string { public static final int app_name=0x7f040000; public static final int hello=0x7f040001; } }
A változás egy main nevű osztály, amelynek lett kettő új mezője. Fontos tudnivaló, hogy az azonosítónak egyedinek kell lennie az alkalmazáson belül, mivel az R.main.textView néven férhetünk hozzá a TextView példányhoz, függetlenül attól, hogy melyik XML állományban szerepel az adott komponens. Az Activity példányban a findViewById metódussal tudjuk lekérdezni az adott erőforrás azonosító alapján a komponenst:
super.onCreate(savedInstanceState); TextView view = (TextView)findViewById(R.main.textView); view.setText("This is it!"); setContentView(R.layout.main);
A lekérdezésnél ügyelnünk kell arra, hogy a típuskonverzió helyesen történjen meg, ezek után már úgy tudjuk használni az adott erőforrást, mintha mi hoztuk volna létre. Nézzük meg, mit is látunk:
Ez bizony nem szép látvány, a platform leállította a program futását. Ennek oka többnyire egy általunk le nem kezelt kivétel, amely így eljut az Android platformig, amely információ hiányában egy ilyen általános hibaüzenettel szórakoztatja a felhasználót. Nos, itt az ideje, hogy elkapjuk a felmerült kivételeket és valamilyen módon kiírjuk őket:
super.onCreate(savedInstanceState); try { TextView view = (TextView) findViewById(R.main.textView); view.setText("This is it!"); setContentView(R.layout.main); } catch (Exception except) { TextView textView = new TextView(this); final Writer result = new StringWriter(); final PrintWriter printWriter = new PrintWriter(result); except.printStackTrace(printWriter); textView.setText(result.toString()); setContentView(textView); }
Egy megszokott try-catch blokk lehet a megoldás, amely hiba esetén létrehoz egy TextView komponenst, amelybe beleírja a kivételben hordozott stacktrace listát:
No igen, ez egy csúnya NullPointerException, amelyet az okozott, hogy előbb kérdeztük le az erőforrások között létrehozott komponenst, mielőtt az létrejött volna, ugyanis ezek a példányok a setContentView metódus hívásakor jönnek létre, javítsuk ki a programot, vagyis vegyük előre a setContentView hívást:
super.onCreate(savedInstanceState); try { setContentView(R.layout.main); TextView view = (TextView) findViewById(R.main.textView); view.setText("This is it!"); } catch (Exception except) { TextView textView = new TextView(this); final Writer result = new StringWriter(); final PrintWriter printWriter = new PrintWriter(result); except.printStackTrace(printWriter); textView.setText(result.toString()); setContentView(textView); }
Nézzük most az eredményt:
Mint láthattuk, az XML leíró előnye, hogy tömörebb lesz tőle a kód, és a fába szervezett komponensek sokkal jobban leírhatók XML-ben, hátránya pedig a fent látható aljas hibához hasonló problémák esélye, hiszen egy fejlesztő környezet messziről kiszúr egy NullPointerException gyanús helyzetet, ugyanez a findViewById esetén nem működik.