Přejít k navigační liště

Zdroják » Mobilní vývoj » Dej Androidu tablety!

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.

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/Pre­senter 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.Frag­ment budeme dědit android.suppor­t.v4.app.Frag­ment 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.suppor­t.v4.app.ListFrag­ment – 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 OnListItemClic­kListener.

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.My­Fragment). 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:layou­t_width na 0px a android:layou­t_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!)

Komentáře

Subscribe
Upozornit na
guest
10 Komentářů
Nejstarší
Nejnovější Most Voted
Inline Feedbacks
View all comments
zdsfasfdasfas

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.

jerrymouse

Jo…jmenuje se to cornerstone. Ale nějak mi uniká, jak je to relevantní k článku.

adsfasdfsadf

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.

mira

anicka ma ted 2 pristroje, tablet a ten puvodni – bude ale moct pouzivat svou aplikaci na pristroji ktery jede na 2.3.3? :)

mira

:) uz jsem si to precet, nebude :)

ruups

samozrejme, ze Anicka je dostatocne inteligentna a pouzila volania fragmentov cez support pack a zabezpecila si spatnu kompatibilitu az k 1.6 …

P3T3

Stáhnul jsem si zdroják, rozbalil a zkopíroval do /sdcard/AppPro­jects. 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 ;-)

Tom

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

mixal11

a k tomu mozes robit „user controls“, jedina vyhoda fragmentov je, ze maju zivotny cyklus – resume, pause atd..

mixal11

„Na rozdíl od Activity se ale Fragment může stát součástí jiné Activity nebo Fragmentu. „

Enum a statická analýza kódu

Mám jednu univerzální radu pro začínající programátorty. V učení sice neexistují rychlé zkratky, ovšem tuhle radu můžete snadno začít používat a zrychlit tak tempo učení. Tou tajemnou ingrediencí je statická analýza kódu. Ukážeme si to na příkladu enum.