Dej Androidu tablety!

Tablety začínají v určitých oblastech nahrazovat počítače. A Android je jedním z nejrozšířenějších tabletových operačních systémů. Dnes si ukážeme, jak s pomocí Fragmentů upravit androidí aplikaci tak, aby se dokázala přizpůsobit telefonu i tabletu.
Nálepky:
S uvedením Androidu 4.0 Ice Cream Sandwich došlo ke sjednocení mobilní a tabletové vývojové větve. Zatímco Androidy 3.x byly od dvojkových poměrně odlišné, ICS je to samé na mobilech i tabletech, jen se trochu jinak uspořádává obsah.
Díky ukončení schizofrenie nyní lze napsat aplikaci tak, aby dobře vypadala na mobilech i tabletech bez nějaké větší práce. A ve dnešním článku se podíváme, jak na to.
Fragmenty, fragmenty, ty já mám nejraději
Fragment reprezentuje novou designovou filosofii, která přišla už s API 11 (Android 3.0). Jak se liší oproti předchozí?
Před Androidem 3.0 byly věci velmi jednoduché a přímočaré, ale neflexibilní a omezující. Základním prvkem uživatelského rozhraní byla tzv. Activity, která se dá velmi dobře představit jako Controller/Presenter v MVC/P. Každá Activity má nastavené View, což je obdoba View z MVC. Activity se starají o všechny možné funkce, naslouchají událostem a reagují na ně a plní View daty. View jen definují, jak bude výsledné uživatelské rozhraní vypadat. Jak to vypadá v praxi?
Anička je dobrá programátorka a miluje dějepis. A rozhodla se, že naprogramuje androidí aplikaci se seznamem vládců českých zemí a informacemi o každém z nich – kdy se narodil, kdy zemřel, jak dlouho vládl a co důležitého se za jeho vlády stalo.
Na výchozí obrazovce je tedy seznam jmen vládců a po klepnutí na některé z nich se zobrazí detailní informace onom vládci. Aplikace se skládá ze dvou Activit – SeznamActivity získá a zobrazí seznam vládců a při klepnutí na některého z nich spustí DetailActivity, která o něm zobrazí vše, co ví.
public class SeznamActivity extends ListActivity { protected static String[] names = new String[] { // ... }; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setListAdapter(new ArrayAdapter<String>(this, R.layout.name, names)); getListView().setOnItemClickListener(new OnItemClickListener() { public void onItemClick(AdapterView<?> parent, View view, int position, long id) { Intent i = new Intent(SeznamActivity.this, DetailActivity.class); i.putExtra(DetailActivity.INDEX, position); startActivity(i); } }); } }
SeznamActivity zobrazí seznam vládců…
public class DetailActivity extends Activity { public static final String INDEX = "index"; protected static String[] details = new String[] { // ... }; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.detail); int index = getIntent().getIntExtra(INDEX, 0); TextView tv = (TextView) findViewById(R.id.details); tv.setText(details[index]); } }
…a DetailActivity nám poskytne veškerou Aniččinu moudrost. Pro obě Activity je samozřejmě potřeba i View – jednoduché a minimalistické.
<?xml version="1.0" encoding="utf-8"?> <ScrollView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" > <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" > <TextView android:id="@+id/details" android:layout_width="match_parent" android:layout_height="wrap_content" android:textAppearance="?android:attr/textAppearanceMedium" /> </LinearLayout> </ScrollView>
/res/layout/detail.xml
– pro DetailActivity
<?xml version="1.0" encoding="utf-8"?> <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:padding="10dp" android:textAppearance="?android:attr/textAppearanceMedium" android:id="@+id/name"> </TextView>
/res/layout/name.xml
– jedna položka seznamu jmen v SeznamActivity
Díky aplikaci vydělala Anička plno peněz a koupila si tablet. Na něm spustila svou aplikaci, ale byla velmi překvapená a trochu smutná, protože aplikace se jí vůbec nelíbila a nepoužívala se dobře. Vypadala takhle:
Optimalizujeme
Protože je však Anička chytrá, podívala se do dokumentace a našla řešení. Jak už jsme si řekli výše, od Honeycombu (3.0) je možné při tvorbě uživatelského rozhraní používat Fragmenty. Co to je Fragment? Fragment reprezentuje část nebo celé uživatelské rozhraní nějaké Activity i s příslušnými metodami. Na rozdíl od Activity se ale Fragment může stát součástí jiné Activity nebo Fragmentu. Díky tomu můžeme vytvářet flexibilní a znovupoužitelné jednotky uživatelského rozhraní. A tím pádem bez duplikace kódu programovat optimalizovaná rozhraní pro větší displeje (tablety, televize atd.).
Jak na to?
Budeme potřebovat:
- dvě Activity (jednu základní a jednu, která na menších displejích „obalí“ DetailFragment),
- dva Fragmenty (SeznamFragment a DetailFragment),
- několik layoutů.
Compatibility Library
Fragmenty jsou v Android Frameworku až od API 11, ale naštěstí existuje knihovna, která zajistí kompatibilitu i pro nižší verze, v případě Fragmentů až po API 4 (Android 1.6). Vše, co byste kdy mohli potřebovat o knihovně vědět, najdete v oddílu Android Developer stránek jí věnovaném. U Fragmentů tahle knihovna v praxi znamená, že místo Activity budeme dědit FragmentActivity, místo android.app.Fragment budeme dědit android.support.v4.app.Fragment a místo getFragmentManager
v Activity budeme volat getSupportFragmentManager
.
Fragmenty
Fragment má sice životní cyklus podobný Activity (životní cyklus Activity), ale nejčastěji se používají jiné funkce. Místo onCreate
se většinou hodí onCreateView
, které vrací View, jež obaluje to, co má být z Fragmentu vidět. Dalším rozdílem je to, že Fragment se instancuje přímo, bez nějakého Intentu, a pro předání dat potřebných hned po vytvoření se používá statická továrnička, která ze svých argumentů vyrobí Bundle a ten předá čerstvě vyrobenému fragmentu pomocí metody setArguments(Bundle arguments)
. DetailActivity tedy přeměníme na DetailFragment následovně:
public class DetailFragment extends Fragment { public static final String INDEX = "index"; protected static String[] details = new String[] { //... }; public static DetailFragment newInstance(int index) { DetailFragment f = new DetailFragment(); Bundle args = new Bundle(); args.putInt(INDEX, index); f.setArguments(args); return f; } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View v = inflater.inflate(R.layout.detail, container, false); int index = getArguments().getInt(INDEX, 0); TextView tv = (TextView) v.findViewById(R.id.details); tv.setText(details[index]); return v; } }
SeznamActivity je potomkem ListActivity a stejně tak SeznamFragment bude dědit od ListFragmentu ( android.support.v4.app.ListFragment – nesmíme zapomínat na Compatibility Library), který mu poskytne mnoho funkcí usnadňujících práci s ListView. Nebudeme přepisovat onCreateView
, protože bychom si museli vytvořit vlastní layout. Místo toho ListView naplníme daty v jiné metodě, kterou mají Fragmenty oproti Activitám navíc, a to v onActivityCreated
. Tato metoda zajišťuje, že getActivity()
nám vrátí připojenou Activitu, kterou budeme potřebovat jako Context pro ArrayAdapter. Kromě toho ListFragment nabízí metodu onListItemClick
, díky níž nemusíme nastavovat OnListItemClickListener.
Ať už ale použijeme onListItemClick
nebo něco jiného, vždy se musíme rozhodnout, zda budeme spouštět novou Activity, anebo jen změníme Fragment ve vedlejším sloupci. Best practice je definovat nějaké rozhraní jako veřejnou složku Fragmentu a donutit kontejnerovou Activitu toto rozhraní implementovat, ať se ona rozhodne, co dělat dál. SeznamFragment bude vypadat takhle:
public class SeznamFragment extends ListFragment { protected static String[] names = new String[] { // ... }; private OnRulerSelectedListener mOnRulerSelectedListener; @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); Context ctx = getActivity(); setListAdapter(new ArrayAdapter<String>(ctx, R.layout.name, names)); } @Override public void onAttach(Activity activity) { super.onAttach(activity); // Donutíme kontejnerovou Activitu implementovat naše rozhraní try { mOnRulerSelectedListener = (OnRulerSelectedListener) activity; } catch (ClassCastException e) { throw new ClassCastException(activity.toString() + " must implement OnRulerSelectedListener"); } } @Override public void onListItemClick(ListView l, View v, int position, long id) { // Chytře se zbavíme zodpovědnosti za vybrání nějakého vládce mOnRulerSelectedListener.onRulerSelected(position); } public interface OnRulerSelectedListener { public void onRulerSelected(int index); } }
Layouty pro Fragmenty jsou totožné s layouty pro odpovídající Activity.
Activity
Fragmenty už máme hotové, ale musíme je mít do čeho vložit. A než je do něčeho vložíme, musíme vědět, jak se vkládají. Zde si to řekneme jen velmi stručně, pro detailnější informace viz Android Developer Guide. Možnosti přidání jsou dvě, a to buď v layoutu, nebo dynamicky, v programu.
Přidání Fragmentu v layoutu je jednodušší, ale neumožňuje předat Fragmentu data hned při jeho vytvoření. Pro Fragmenty je v layoutu značka <fragment>
, jíž v atributu android:name
předáme název třídy Fragmentu včetně namespace (com.example.MyFragment). Pokud chceme mít v kódu přístup k tomu fragmentu, musíme nastavit buď android:id
( FragmentManager.findFragmentById(int id)
), nebo android:tag
( FragmentManager.findFragmentByTag(String tag)
).
Velmi často se ale nevyhneme přidání Fragmentu v kódu. Nejprve musíme mít vytvořený objekt Fragmentu. Poté získáme FragmentManager (ve FragmentActivity pomocí metody getSupportFragmentManager()
, začneme transakci, přidáme (nahradíme, odebereme, …) Fragment nebo více Fragmentů a transakci commitneme.
Fragment fragment = MyFragment.newInstance("MyString"); // Použijeme getSupportFragmentManager, abychom byli kompatibilní s API<11 FragmentManager fragmentManager = getSupportFragmentManager(); FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); // R.id.fragment_container je id nějaké ViewGroup, která existuje v aktuálním layoutu. // Do ní bude fragment umístěn. fragmentTransaction.add(fragment, R.id.fragment_container); // Na konci nesmíme zapomenout transakci commitnout. fragmentTransaction.commit();
Když máme Fragmenty a víme, jak je někam vložit, musíme je mít kam vložit. A potřebujeme mít něco, co rozhodne o tom, kdy použít jednosloupcový a kdy dvousloupcový layout. A právě proto si vytvoříme ještě dvě Activity. VladciActivity bude hlavní, ta bude obsahovat SeznamFragment a někdy i DetailFragment, bude rozhodovat, jestli se použije jedno-, či dvousloupcový layout a případně bude spouštět DetailActivity, která bude úplně hloupá a jen zobrazí DetailFragment. A my začneme právě DetailActivity.
public class DetailActivity extends FragmentActivity { public static final String INDEX = "index"; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.detail_activity); Intent i = getIntent(); int index = i.getIntExtra(INDEX, 0); DetailFragment f = DetailFragment.newInstance(index); // Přidá fragment do View s id detail getSupportFragmentManager().beginTransaction().add(R.id.detail, f).commit(); } }
A pro novou DetailActivity potřebujeme nový layout (ten starý používá DetailFragment).
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/detail"> </FrameLayout>
/res/layout/detail_activity.xml
– layout pro DetailActivity
VladciActivity bude trochu tvrdší oříšek. Nejprve musíme rozhodnout, jak budeme rozlišovat, kdy použít dvousloupcový a kdy jednosloupcový layout. Já zvolil triviální rozdělení, na výšku bude layout jednosloupcový, na šířku dvousloupcový. Díky tomu je možné si dvousloupcový prohlédnout i na telefonu. V praxi by asi bylo potřeba použít přesnější selekci, pravděpodobně pomocí sw
qualifieru (viz Tabulka 2 ve Providing Resources na Dev Guide). Layouty budou tedy vypadat takto:
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <fragment class="com.example.vladci.SeznamFragment" android:id="@+id/seznam" android:layout_width="match_parent" android:layout_height="match_parent" /> </FrameLayout>
/res/layout/vladci.xml
– layout pro jednosloupcovou VladciActivity
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal" > <fragment class="com.example.vladci.SeznamFragment" android:id="@+id/seznam" android:layout_weight="2" android:layout_width="0px" android:layout_height="match_parent" /> <FrameLayout android:id="@+id/detail" android:layout_weight="3" android:layout_width="0px" android:layout_height="match_parent" /> </LinearLayout>
/res/layout-land/vladci.xml
– layout pro dvousloupcovou VladciActivity
Za upozornění možná stojí, že nastavením android:layout_width na 0px a android:layout_weight na 2, resp. 3 docílíme toho, že tyto dvě View zaplní celou šířku, a to v poměru 2:3.
A když jsme rozhodli, kdy bude aplikace jedno- a kdy dvousloupcová, musíme podle toho správně měnit obsah druhého sloupce resp. spouštět DetailActivity. Ale to je díky chytře navrženému SeznamFragmentu hračka.
public class VladciActivity extends FragmentActivity implements SeznamFragment.OnRulerSelectedListener { private boolean mDualPane; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.vladci); // Pokud je dostupné View s id detail, je layout dvousloupcový mDualPane = findViewById(R.id.detail) != null; } @Override public void onRulerSelected(int index) { if (mDualPane) { // Dvousloupcový layout DetailFragment f = DetailFragment.newInstance(index); FragmentTransaction ft = getSupportFragmentManager() .beginTransaction(); ft.replace(R.id.detail, f); // Voláním FragmentTransaction.addToBackStack dosáhneme toho, // že při stisknutí tlačítka zpět se Fragment vymění s tím, // co v R.id.detail bylo předtím (jiný DetailFragment nebo nic). ft.addToBackStack(null); ft.commit(); } else { // Jednosloupcový layout Intent i = new Intent(this, DetailActivity.class); i.putExtra(DetailActivity.INDEX, index); startActivity(i); } } }
Výsledek
Když máme tohle všechno hotovo a správně všechny Activity zapíšeme do Manifestu (kdybyste věděli, kolik času jsem strávil nadáváním, proč to a to nefunguje – pozn. aut.), můžeme vyzkoušet aplikaci spustit. Na telefonu v portrait módu (na výšku) vypadá stejně, ale na tabletu v landscape módu už vypadá tabletově.
Dnes jsme si ukázali, jak udělat v Androidu layout tak, aby se bez duplikace kódu přizpůsobil tabletům i telefonům. Plno věcí, které jsou pro UX velmi důležité, jsme vynechali (nastavení zvýraznění vybraného prvku seznamu, neošetřili jsme zachování stavu aplikace, když se změní orientace, a plno dalších věcí), ale bylo to tak záměrně, abychom se neodváděli od základního cíle. Pro další studium doporučím Android Developer Guide, která se celou problematickou zabírá mnohem více do hloubky.
Stáhnout si můžete zdrojové kódy celé aplikace Vládci českých zemí . (Při zkoušení ukázkových kódů nezapomeňte přidat Compatibility Library!)
nekde na webu byl clanek, ze uz nekdo pridal podporu pro okynka do androidu,
ze muze bezet vice aplikaci a kazda bude mit svoje okno.
teda ma to smysl akorat u tabletu.
Jo…jmenuje se to cornerstone. Ale nějak mi uniká, jak je to relevantní k článku.
v clanku se fakticky resi, ze tablet ma mnoho prostoru a tak se na nem zobrazi
dve view z mobilu najednou…tj. totez o co jde v okynkach.
anicka ma ted 2 pristroje, tablet a ten puvodni – bude ale moct pouzivat svou aplikaci na pristroji ktery jede na 2.3.3? :)
:) uz jsem si to precet, nebude :)
samozrejme, ze Anicka je dostatocne inteligentna a pouzila volania fragmentov cez support pack a zabezpecila si spatnu kompatibilitu az k 1.6 …
Stáhnul jsem si zdroják, rozbalil a zkopíroval do /sdcard/AppProjects. Do adresářové struktury projektu vladci jsem přidal adresář libs a do něj soubor android-support-v4.jar a …
… na první pokus mi AIDE sestavil funkční aplikaci. Takže parádní ukázkový příklad a navíc ukázka toho, že AIDE je opravdu cestovní řešení na malé projekty. Kromě té knihovny jsem všechno podnikl cestou z práce domů v MHD ;-)
Ahoj Matěji,
článek je skvělý.
Mám dotaz: Proč mám požívat fragmenty, když můžu používat qualifiers jako:
res/layout-sw320dp/
res/layout-sw600dp/
res/layout-sw720dp/
Mohu přece umístit layouty do těchto složek a android rozhodne za mě, který layout se použije na základě rozlišení.
A nebo se pletu?
Děkuji za vysvětlení.
Zdraví Tom
a k tomu mozes robit „user controls“, jedina vyhoda fragmentov je, ze maju zivotny cyklus – resume, pause atd..
„Na rozdíl od Activity se ale Fragment může stát součástí jiné Activity nebo Fragmentu. „