Filozofija

Android je sestavljen tako, da vsaka aplikacija teče v natanko eni aktivnosti. In po drugi strani je tudi natanko ena aktivnost naenkrat prikazana na zaslonu. To olajša upravljanje z aktivnostmi v ospredju, a hkrati oteži ponovno uporabo že napisane kode. Včasih bi želeli združiti dve aktivnosti hkrati na zaslonu. Primer tega bi bil brskalnik datotek, ki zna prikazati predogled slik. Ena aktivnost prikazuje datoteke in mape, nato pa s klikom na slikovno datoteko prikažemo to v drugi aktivnosti (ta proces smo obdelali v prejšnji številki). Če pa uporabnik poganja našo aplikacijo na tabličnem računalniku, ima dovolj velik zaslon, da bi prikazal oboje hkrati – datoteke in mape na levi in predogled slike na desni. A kot smo že ugotovili, aktivnosti tega ne dovolijo. Tu vskočijo fragmenti. Fragmenti se obnašajo zelo podobno kot aktivnosti, le da jih lahko imamo več hkrati na zaslonu. Vendar v nasprotju z aktivnostmi fragmenti ne morejo stati samostojno, ampak so lahko le vsebovani znotraj aktivnosti in so nanjo tudi neposredno vezani. S fragmenti je nekaj več dela na začetku, vendar pa je potem sestavljanje grafičnega vmesnika z njimi zelo olajšano.

Konkretno

Fragmente je najlažje spoznati kar s primerom. Zato si poglejmo kodo za primer, ki smo ga omenili zgoraj – aplikacija za brskanje po datotekah in prikaz slik. Pri takšni aplikaciji želimo, da je videti:

1. na tablici v vodoravni legi: na levi strani seznam datotek in map, na desni strani predogled slike,

2. na tablici v pokončni legi: zgoraj seznam datotek in map, spodaj predogled slike,

3. na telefonih v obeh legah: seznam datotek in map čez ves zaslon, ob kliku na datoteko s sliko pa predogled slike čez ves zaslon.

Najprej pripravimo fragmente (zidake).

Brskalnik datotek

Hočemo napraviti zelo preprost brskalnik datotek, ki zna prikazati seznam datotek in map v trenutni mapi in odpreti podmapo. Za prikaz tega je najbolje uporabiti kar ListFragment, ki je, podobno kot ListActivity, namenjen prikazu seznamov. Najprej ustvarimo datoteko s postavitvijo list_files.xml:

<ListView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
</ListView>

Očitno je zelo preprosta, vsebuje le seznam. Nato ustvarimo še datoteko z videzom posamezne vrstice seznama list_file_element.xml:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView android:id="@+id/lbl_file_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="tekst"/>
</LinearLayout>

Tudi to je zelo preprosta koda, saj vsebuje le en pogled TextView, ki prikazuje ime datoteke ali mape.

Nato ustvarimo še kodo, ki skrbi za logiko v ozadju. Shranimo jo v datoteko ListFilesFragment.java:

public class ListFilesFragment extends ListFragment {
private FilesListAdapter adapter;

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View inflate = inflater.inflate(R.layout.list_files, container, false);
adapter = new FilesListAdapter(inflater);
setListAdapter(adapter);
adapter.setFiles(Environment.getExternalStorageDirectory().listFiles());
return inflate;
}

@Override
public void onListItemClick(ListView l, View v, int position, long id) {
File file = (File) adapter.getItem(position);
if (file.isDirectory()) {
adapter.setFiles(file.listFiles());
adapter.notifyDataSetChanged();
} else {
((BrowseImagesActivity) getActivity()).openFile(file);
}
}
}

V tej kodi najprej napihnemo datoteko z videzom, ki smo ga ustvarili prej: list_files, nato napravimo adapter FilesListAdapter.java, ki drži datoteke (implementacija tega je enaka kot pri ListActivity), na koncu pa adapterju še dodamo vse datoteke v korenski mapi kartice SD.

Implementirali pa smo tudi metodo onListItemClick, ki predstavlja klik na vrstico, kjer pogledamo, ali je uporabnik kliknil na datoteko ali mapo. Če je kliknil na datoteko, mu jo odpremo (v aktivnosti BrowseImagesActivity.java, ki jo bomo še ustvarili), če pa je kliknil na mapo, mu naložimo njeno vsebino v seznam.

Prikaz slike

Tu ne bomo šli v podrobnosti, kako implementirati dejanski prikaz slike, ampak samo postavitev ogrodja (fragmentov). Najprej napravimo datoteko z videzom image_preview.xml. Nato napravimo še razred z logiko ImagePreviewFragment.java:

public class ImagePreviewFragment extends Fragment {
private File fileToOpen = null;

public ImagePreviewFragment(File fileToOpen) {
this.fileToOpen = fileToOpen;
}

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.image_preview, container, false);
if (fileToOpen != null) {
openFile(fileToOpen);
}
return view;
}

public void openFile(File file) {
// prikaz slike

}
}

V konstruktorju sprejmemo datoteko, ki jo želimo odpreti. V metodi onCreateView napihnemo videz image_preview, nato pa z metodo openFile odpremo datoteko.

Sestavljanje fragmentov

Zdaj smo prišli do zanimivega dela, saj bomo ta fragmenta sestavili skupaj. Najprej ustvarimo tri datoteke z istim imenom v mapi res:

1. res/layout-sw600dp/browse_images.xml
2. res/layout-sw600dp-land/browse_images.xml
3. res/layout/browse_images.xml

Prva predstavlja videz na pokončnih tablicah (sw600dp pomeni, da ima tablica najkrajšo resolucijo vsaj 600dp), druga na ležečih tablicah (land pomeni »landscape«), tretja pa na telefonih.

Navpične tablice

Ker imamo na tablicah dovolj prostora, želimo prikazati oba fragmenta hkrati, drugega ob drugem, če je tablica obrnjena vodoravno, oziroma drugega pod drugim, če je obrnjena pokonci.

Na navpično usmerjeni tablici želimo fragmenta drugega pod drugim.

Za navpično postavitev ustvarimo datoteko browse_images.xml v mapi res/layout-sw600dp z naslednjo vsebino:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:weightSum="2">
<fragment android:id="@+id/frg_list_files"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_weight="1"
android:name="com.comtrade.android.fragments.ListFilesFragment"/>
<FrameLayout android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:id="@+id/frg_preview_image"
android:layout_weight="1"/>
</LinearLayout>

Zunanja značka LinearLayout nam fragmenta postavi v linearno vrsto, parameter android:orientation pa nam pove, da ju bo postavil drugega nad drugim (vertical). Znotraj te značke imamo dve komponenti.

Prva je konkreten fragment (kot je tudi ime značke), ki z lastnostjo android:name kaže neposredno na razred fragmenta, ki ga želimo prikazati. V našem primeru je to prej ustvarjeni ListFilesFragment.java. Na mestu, kjer stoji ta značka, se bo v aplikaciji torej prikazal naš fragment za brskanje po datotekah.

Druga značka pa še ni neposredno povezana s fragmentom, ampak je razporeditev (orig.: Layout), ki drži en pogled. Ta pogled bo naš fragment, ki ga bomo dodali programsko, glede na uporabnikov klik. Mi bomo tu notri programsko dodali fragment za prikaz slik, lahko pa bi imeli tudi fragmente za prikaz tekstovnih datotek, glasbe ipd. In vsakič ko uporabnik klikne na datoteko, v tej znački zamenjamo stari fragment (če obstaja) z novim. Kodo, ki to počne, bomo napisali pozneje.

Vodoravne tablice

Pri vodoravnih tablicah je v našem primeru datoteka z videzom skoraj popolnoma enaka, le da je shranjena (z istim imenom) v mapi res/layout-sw600dp-land in da ima lastnost android:orientation=”horizontal”. To bo fragmenta postavilo drug ob drugega.

Na vodoravno usmerjeni tablici želimo fragmenta drug ob drugem.

Telefoni

Pri telefonih je zgodba drugačna, če je na njih premalo prostora, da bi prikazali oba fragmenta hkrati. Zato bomo najprej čez vso stran prikazali fragment, ki omogoča brskanje po mapah, ko pa uporabnik klikne na datoteko, bomo čez vso stran prikazali sliko.

V mapi res/layout napravimo najprej datoteko browse_images.xml z vsebino:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment android:id="@+id/frg_list_files"
android:layout_width="match_parent"
android:layout_height="fill_parent"
android:name="com.comtrade.android.fragments.ListFilesFragment"/>
</LinearLayout>

Ta datoteka vsebuje le fragment za brskanje po mapah. Za prikaz slike pa moramo ustvariti nov pogled. V isti mapi (res/layout) ustvarimo še eno datoteko z videzom, poimenujmo jo image_preview.xml. Tudi vsebini te datoteke se bomo izognili, ker je tokrat poudarek na fragmentih.

Majhen zaslon telefona zmore prikazati le en fragment naenkrat.

Logika za fragmenti

Omenili smo že, da fragment ne more obstajati brez »starševske« aktivnosti. Zato najprej ustvarimo to. Ustvarimo razred BrowseImagesActivity.java:

public class BrowseImagesActivity extends Activity {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.browse_images);
}

public void openFile(File file) {
boolean isTablet = getResources().getBoolean(R.bool.isTablet);
if (isTablet) {
getFragmentManager().beginTransaction()
.replace(R.id.frg_preview_image, new ImagePreviewFragment(file))
.commit();
} else {
Intent intent = new Intent(this, ImagePreviewActivity.class);
intent.putExtra("file", file);
startActivity(intent);
}
}
}

V metodi onCreate najprej nastavimo videz, ki smo ga prej ustvarili – browse_images. Nato pa ustvarimo javno metodo openFile(File file), ki bo poskrbela za prikaz slike. Klic te metode smo že sprogramirali prej: ((BrowseImagesActivity) getActivity()).openFile(file).

Ta metoda najprej preveri, ali je naša naprava tablica ali telefon (podrobnosti v okvirju). Če je tablica, moramo s FragmentManagerjem zamenjati fragment, ki (morebiti) trenutno prikazuje sliko, z novim fragmentom s sliko, ki jo je uporabnik pravkar kliknil. Proces, ki to napravi, je v bistvu transakcija, v kateri določimo, kaj naj se zgodi. Mi smo s kodo .replace(R.id.frg_preview_image, new ImagePreviewFragment(file)) določili, da se mora v pogledu z id-jem “frg_preview_image” zamenjati stari fragment (če obstaja) z na novo ustvarjenim fragmentom ImagePreviewFragment.java, kateremu v konstruktor podamo sliko, ki jo želimo prikazati. Transakciji lahko določimo še nekatere parametre, kot na primer animacijo ob zamenjavi. In ne pozabimo klicati metode .commit(), saj se drugače transakcija oziroma zamenjava fragmenta ne izvede.

Če uporabljamo aplikacijo na telefonu, pa ne zamenjamo fragmenta, ampak ustvarimo novo aktivnost in ji kot parameter dodamo datoteko. Nova aktivnost ima naslednjo kodo:

public class ImagePreviewActivity extends Activity {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
File file = (File) getIntent().getSerializableExtra("file");
getFragmentManager().beginTransaction()
.replace(android.R.id.content, new ImagePreviewFragment(file))
.commit();
}
}

Ta aktivnost niti ne potrebuje svojega videza, saj že ob ustvarjanju poženemo vstavitev fragmenta. Koda .replace(android.R.id.content, new ImagePreviewFragment(file)) nam pove, da bomo fragment ImagePreviewFragment.java postavili kot (edino) vsebino trenutne aktivnosti.


Identifikacija naprave
Obstaja eleganten način preverjanja, ali naša aplikacija teče na telefonu ali tablici. V mapi res/ ustvarimo dve podmapi: values in values-sw600dp. V vsako damo datoteko basics.xml z naslednjo vsebino:

<resources>
<bool name="isTablet">false</bool>
</resources>

pri čemer v datoteki v mapi values-sw600dp false zamenjamo s true. Na tablicah bo parameter isTablet nastavljen na true, na telefonih pa na false. Lastnost programsko dobimo s kodo:

getResources().getBoolean(R.bool.isTablet)

Zaključek

Na zgoraj opisani način lahko napišemo aplikacijo, ki spretno uporablja fragmente, ne glede na to, na kateri napravi teče. Kot smo videli, je glavnina dela padla na grajenje fragmentov samih, njihovo sestavljanje pa je bilo precej trivialno. Sprva je videti uporaba fragmentov zapletena, a obljubljamo, da bo po nekaj poskusih postala tako naravna kot pisanje for zank.
Kot vedno kodo, uporabljeno v tem članku, najdete na GitHubu: http://goo.gl/6UFjgJ, za vprašanja pa sem na voljo po elektronski pošti.

Moj mikro, Maj - Junij 2014 | Rok Končina