Home

 

 

Android 3.0-Oberflächen


Ein Beitrag von Arno BeckerArno Becker

Android 3.0 bringt einige interessante Neuerungen, wie zum Beispiel Speichermedienverschlüsselung mit sich. Breites Interesse weckt es aber wegen seinen neuen Möglichkeiten, Oberflächen für Tablet PCs zu erstellen. Dank neuer Techniken kann man schnell ansprechende Oberflächen für die großen Bildschirme erzeugen.

Die Menge der Neuerungen in Android 3.0 bietet genug Stoff für mehrere Artikel. Daher werden wir hier anhand eine Beispiels die Gestaltung von Oberflächen mittels Fragments und Action Bar erklären.

Neuerungen

Android 3.0 wird oft auch als „Android für Tablets“ bezeichnet. Ganz richtig ist dies nicht, da Android 3.0 abwärtskompatibel ist. Anwendungen früherer Versionen laufen auch auf den großen Bildschirmen, auch wenn diese etwas „aufgeräumt“ aussehen. Richtig ist jedoch, dass Android 3.0 das Programmieren für große Bildschirme komfortabel macht. Dank einer neuen Technik, „Fragments“ genannt, kann man den Bildschirm in unterschiedliche Content-Bereiche unterteilen, die innerhalb einer Activity laufen und miteinander interagieren können.

Das klassische Optionsmenü, für welches man eine spezielle Taste am Android-Gerät brauchte, ist nun in den Action Bar gewandert, zumindest solange dieser angezeigt wird. Er ersetzt den bisherigen Titelbalken (Title Bar) einer Activity und kann nun mit dem Anwender interagieren, z.B. in dem er das Menü anzeigt. Somit fällt die Menütaste weg.

Auch die beiden weiteren Pflichtknöpfe „Back“ und „Home“ sind bei Android 3.0 auf die Oberfläche gewandert. Der frühere Statusbalken ist an den unteren Bildschirmrand verschoben worden und heißt nun „System Bar“. Er beherbergt zusätzlich die beiden Funktionen „Back“ und „Home“ und dient zum Wechsel in den Vollbildmodus, bespielsweise wenn man ein Video anschauen möchte. Weiterhin empfängt er Notifications, zeigt den Systemstatus und die Uhrzeit an.

Fragments

Seit Android 3.0 lassen sich Activites aus mehreren Fragments zusammensetzen. Diese Fragments stellen autonome Content-Bereiche dar, die zwar innerhalb einer Activity ausgeführt, aber als eigene Klassen implementiert werden. Sie können, genau wie Activities, ein Layout besitzen und übernehmen somit die Funktion, die früher eine Activity hatte. Folgende Fragment-Klassen stehen zur Verfügung.

android.app.Fragment
android.app.DialogFragment
android.app.ListFragment
android.preference.PreferenceFragment
android.webkit.WebViewFragment

 Deutlich erkennt man schon am Namen die Ähnlichkeit zu Activites.

Lebenszyklus von Fragments

Fragments besitzen einen umfangreicheren Lebenszyklus. Er ist mit dem Lebenzyklus der Activity verzahnt, die das Fragment erzeugt. Die einzelnen Lebenszyklusmethoden für die Erzeugung eines Fragments bis zu dessen Anzeige lauten:

onAttach (Activity)
onCreate(Bundle)
onCreateView(LayoutInflater, ViewGroup, Bundle)
onActivityCreated(Bundle)
onStart()
onResume()

 onActivityCreated wird aufgerufen, nachdem die onCreate-Methode der Activity durchlaufen wurde, zu der das Fragment gehört. In dieser Methode steht somit die vollständige View-Hierarchie zur Verfügung, also auch die der Activity darüber. Dadurch ließe sich beispielsweise auch auf Views der Activity zugreifen. Insbesondere eignet sich diese Methode für dynamische Anpassungen des Layouts, beispielsweise wenn Zustände im Bundle gespeichert wurden.

Generell kann diese Methode für Initialisierungen genutzt werden, da sie recht spät im Lebenszyklus des Fragments und seiner Activity aufgerufen wird.

Wesentlich früher wird die Methode onAttach aufgerufen, nämlich wenn das Fragment der Activity zugeordnet wird.

onCreateView muss eine View zurückgeben. Hier können Initialisierungen speziell am Layout der View, also der Oberfläche des Fragments, vorgenommen werden. Ein Zugriff auf das Layout der Activity darf hier nicht erfolgen, da diese zu dem Zeitpunkt noch nicht existiert.

Zu guter Letzt bleibt noch die Methode onDetach. Sie wird aufgerufen, wenn die Activity das Fragment nicht mehr braucht (z.B. wenn das Layout wechselt). Hier können Aufräumarbeiten erfolgen.

Activities reloaded

Activities kommt nun zusätzlich die Aufgabe zu, die Fragments zu verwalten. Natürlich kann man auch weiterhin Activities ohne Fragments implementieren. Aber durch das Einbetten von Fragments in das Layout der Activity erhält man autonome Content-Bereiche, die ein- und ausgeblendet oder ersetzt werden können.

Je nach Anwendungsfall kann eine App aus nur einer Activity bestehen. Mehrere Fragments können an ein und derselben Stelle plaziert werden und es wird jeweils nur eins davon eingeblendet. Die Anzeige einer anderen Art von Content an derselben Stelle wird durch ausblenden des alten Fragments und Einblenden des neuen Fragments realisiert.

Natürlich kann auch für jede Darstellungsart eine eigene Activity mit eigenem Layout verwendet werden.

Action Bar

Der Action Bar dient nun zur Anzeige des Menüs und ersetzt den alten Titelbalken. Neben dem Menü (rechts gelegen) kann er noch Tabulatoren (Tabs) aufnehmen (links positioniert). Ein entsprechender Listener kann registriert und auf Ereignisse reagieren (Methode onTabSelected). Der Action Bar lässt sich jederzeit mittels seiner Methoden show und hide ein- bzw. ausblenden.

Mein Haus, mein Auto, mein Boot

Nach der Theorie folgt nun die Praxis. Neben Fragments, Activities mit Fragments und dem Action Bar wollen wird anhand eines Programmierbeispiels auch gleich die neue Drag-and-Drop-Funktionalität und eine Animation zeigen. Dazu dient uns eine Anwendung die wir auch YAASA (Yet Another Android Shop App) hätten nennen können. Wir implementieren fast die gleiche Funktionalität wie ein Shop, nur dass wir uns eine kleine Präsentation zusammen, mit der wir beim nächsten Klassentreffen eindrucksvoll präsentieren können, wie weit wir es gebracht haben. Wir können einen „Warenkorb“ mit jeweils einem Bild von unserem Haus, unserem Auto und unserem Boot zusammenstellen und diesen dann präsentieren („Mein Haus, mein Auto, mein Boot!“). Das schafft Eindruck, Freude oder Neider, je nach Auswahl der Bilder. Abbildung 1 zeigt einen Screenshot der Anwendung.

Das Beispiel beruht auf der „HoneycombGallery“, einem Beispielprojekt, welches im Android-SDK unter <AndroidSDK>/samples/android-11/HoneycombGallery zu finden ist. Das vollständige Programm befindet sich auf der CD zum Heft.

 android-3.0-oberflaechen-android01
Abbildung 1 | Startbildschirm der Beispielanwendung

Vorbereitung

Wer noch kein Android 3.0-Tablet besitzt muss mit dem Emulator auskommen. Ein möglichst schneller Rechner ist hier von großem Vorteil. Das Starten der Anwendung dauert sonst recht lange und die Animationen laufen nicht flüssig ab. Am besten ist es daher, auf einem echten Gerät zu entwickeln.

Bei der Entwicklung mit dem Emulator führt man über den AVD Manager unter „Available Packages“ ein Update auf Android 3.x aus, falls nicht schon geschehen. Dann legt man eine neues AVD mit dieser Android-Version an. Dadurch wird die Bildschirmauflösung automatisch auf WXGA gesetzt, was in der Praxis meist 1280x800 Pixeln entspricht. Als Target wählt man „Android 3.0 – API Level 11“.

Implementierung

Wir legen zunächst ein neues Projekt mit Android 3.0 als Build Target an. Wir lassen auch gleich eine Activity mit Namen „MainActivity“ erzeugen.

Listing 1 zeigt das Android Manifest. Da die Anwendung nur auf Android 3.0-Geräten laufen wird, ist derzeit der Tag <support-screens> eigentlich überflüssig. Problematisch kann es werden, wenn Fragments und Action Bar auch in den Android 2.x-Zweig übernommen werden, was derzeit geplant ist. Dann müssen große Oberflächen stark verkleinert werden. Einen Eindruck davon liefert der Emulator mit einem AVD mit einer Bildschirmauflösung von 320x480. Abbildung 2 zeigt die Oberfläche im Querformat.

 android-3.0-oberflaechen-android02
Abbildung 2 | Android 3.0-Oberfläche auf kleinem Bildschirm.

Alle Content-Bereiche (Fragments) werden vom Android-System automatisch scrollbar gemacht. Die Oberfläche ist somit prinzipiell bedienbar. Die unvollständige Überschrift der Drag and Drop-Fläche ließe sich durch ein optimiertes Layout korrigieren. Schön ist die Darstellung jedoch nicht. Daher legen wir mittels des <support-screens>-Tags fest, dass die Anwendung nur auf „xlarge“-Bildschirmen läuft. Auch die SDK-Version setzen wir fest auf „Honeycomb“ (API-Level 11).

Listing 1: Android Manifest

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android=
"http://schemas.android.com/apk/res/android"
      
package="de.visionera.fragments"
      android:versionCode="1"
      android:versionName="1.0">
      
  <uses-sdk android:minSdkVersion=
"11"
            android:targetSdkVersion="11" />
  
  <supports-screens android:smallScreens=
"false" 
                    android:normalScreens="false"
                    android:largeScreens="false"
                    android:xlargeScreens="true" />    

  <application android:icon=
"@drawable/icon" 
               android:label="@string/app_name"
               android:hardwareAccelerated="true" >
  
    <activity android:name=
".MainActivity"
              android:label="@string/app_name">
        <intent-filter>
            <action android:name=
"android.intent.action.MAIN" />
            <category android:name=
"android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>
    
    <activity android:name=
".ShowActivity" />

  </application>
</manifest>

 Nun erzeugen wir die Startseite der Anwendung. Die Klasse MainActivity vereinigt alle Fragments und den Action Bar. Die Anordnung der Fragments wird durch das Layout bestimmt. Listing 2 zeigt das Layout der MainActivity.

Listing 2: Layout der Klasse MainActivity

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android=
"http://schemas.android.com/apk/res/android"
      
package="de.visionera.fragments"
      android:versionCode="1"
      android:versionName="1.0">
      
  <uses-sdk android:minSdkVersion=
"11"
            android:targetSdkVersion="11" />
  
  <supports-screens android:smallScreens=
"false" 
                    android:normalScreens="false"
                    android:largeScreens="false"
                    android:xlargeScreens="true" />    

  <application android:icon=
"@drawable/icon" 
               android:label="@string/app_name"
               android:hardwareAccelerated="true" >
  
    <activity android:name=
".MainActivity"
              android:label="@string/app_name">
        <intent-filter>
            <action android:name=
"android.intent.action.MAIN" />
            <category android:name=
"android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>
    
    <activity android:name=
".ShowActivity" />

  </application>
</manifest>

Neu sind hier die <fragment>-Tags. Im name-Attribut wird eine selbst-implementierte Fragment-Klasse angegeben. Diese bringt ihr eigenes Layout mit, so wie man es von Activites kennt. Damit nimmt jedes Fragment einen bestimmten Anteil des Bildschirms ein.

Gut steuern lässt sich der „Platzverbrauch“ über das weight-Attribut. Es gibt das Platzverhältnis in Relation zu den anderen Views oder Fragments an. Liegen beispielsweise drei Views oder Fragments in einem LinearLayout untereinander und sie haben die weight-Attribute 1, 2 und 1, dann nehmen sie 25 %, 50 % und 25 % der zur Verfügung stehenden Fläche ein.

Zu beachten ist, dass bei einem vertikalen Layout das layout_height-Attribut gleichzeitig auf „0dp“ gesetzt werden sollte, damit das layout_weight-Attribut die volle Kontrolle über die anzuzeigende Höhe der View erhalten kann und die View entsprechend „stauchen“ kann, was gerade bei Bildern wichtig ist.

FragmentManager, Transaktionen und Back Stack

Fragments benötigen zwingend entweder eine Id (android:id) oder einen Tag (android:tag), damit beim Neustart einer Activity, beispielsweise durch das Drehen des Bildschirms, das Fragment automatisch neu gestartet wird.

Alternativ zur Deklaration der Fragments in einer Layout-XML-Datei können die Fragments zur Laufzeit programmatisch erzeugt und einem Layout (genauer: einer ViewGroup) hinzugefügt werden. Dazu benötigt die ViewGroup eine Id, damit man ihr ein Fragment hinzufügen kann.

Fragment meinFragment = new MeinFragment();
FragmentTransaction transaction = getFragmentManager().beginTransaction();

transaction.replace(R.id.right_layout, meinFragment);
transaction.addToBackStack(null);

transaction.commit();

Solche Operationen wie Zufügen, Löschen oder Austauschen von Fragments müssen in einer Transaktion erfolgen. Jede Transaktion muss mit commit beendet werden. Eine Transaktion bietet unter anderem die Methoden add, replace und remove. Mittels show und hide können Fragments ein- und ausgeblendet werden.

Transaktionen verwaltet der FragmentManager, den man in einer Activity mittels der Methode getFragmentManager erhält. Er hat drei Aufgaben: Suchen und Markieren (Tagging) von Fragments, Transaktionen und den Back Stack verwalten.

Mit Hilfe von Transaktionen definiert man einzelne atomare Fragment-Operationen. Mit Hilfe des Back Stacks kann man eine Operation, wie das Hinzufügen oder ersetzen eines Fragments, rückgängig machen. Jede Transaktion kann mittels der Methode addToBackStack an die Back-Schaltfläche im System Bar gekoppelt werden. Ersetzt man wie im Code oben ein Fragment durch ein anderes und drückt dann die Back-Schaltfläche (links unten auf dem Bildschirm), so wird das alte Fragment wieder angezeigt.

Vorsicht ist geboten, wenn Fragments per Transaktion aus dem FragmentManager entfernt werden. Das Fragment „lebt“ als Objekt weiter und verbraucht Speicherplatz. Es muss entweder zerstört oder recycelt werden [2].

Doch nun zurück zu unserer Beispielanwendung. Listing 3 zeigt die Klasse MainActivity.

Listing 3: Implementierung der Startseite

public class MainActivity extends Activity 
        implements ActionBar.TabListener {
        
        @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        Directory.initializeDirectory();

        ActionBar bar = getActionBar();

        for (int i = 0; i < Directory.getCategoryCount(); i++) {
            bar.addTab(bar.newTab().setText(Directory.getCategory(i).getName())
                    .setTabListener(this));
        }

        bar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM | ActionBar.DISPLAY_USE_LOGO);
        bar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
        bar.setDisplayShowHomeEnabled(true);      
    }

    @Override
    public void onTabSelected(Tab tab, FragmentTransaction ft) {
        final ArticleListFragment articleListFragment =
            (ArticleListFragment) getFragmentManager()
            .findFragmentById(R.id.article_list);

        articleListFragment.populateAricleList(tab.getPosition());
        articleListFragment.selectPosition(0);        
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        MenuInflater inflater = getMenuInflater();
        inflater.inflate(R.menu.main_menu, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case R.id.item_shoppingcart:
                final Fragment shoppingCart =
                    getFragmentManager().findFragmentById(R.id.cart_content);
                
                FragmentTransaction ft = getFragmentManager().beginTransaction();
                ft.setCustomAnimations(android.R.animator.fade_in,
                        android.R.animator.fade_out);
                TextView tv = (TextView)findViewById(R.id.txt_drag_drop);
                
                if (shoppingCart.isHidden()) {
                    ft.show(shoppingCart);
                    tv.setVisibility(TextView.VISIBLE);
                } else {
                    ft.hide(shoppingCart);  
                    tv.setVisibility(TextView.GONE);
                }
                ft.commit();
                break;
            case R.id.item_help:
                //TODO - FragmentDialog
                break;                
            case R.id.item_show:                
                final CartFragment cartFragment =
                   (CartFragment) getFragmentManager().findFragmentById(R.id.cart_content);
                final Intent intent = new Intent(this, ShowActivity.class);
                intent.putExtra("cartitems",
                    cartFragment.getCartItems().toArray(new CartItem[3]));
                startActivity(intent);
                break;
            default:
                break;
        }
        return super.onOptionsItemSelected(item);
    }
        @Override
public void onTabReselected(Tab tab, FragmentTransaction ft) { }
        @Override
public void onTabUnselected(Tab tab, FragmentTransaction ft) { }
}

Diese Activity verwaltet drei Content-Bereiche und den Action Bar. Die Bereiche sind die linke Liste (ArticleListFragment) mit den Häusern, Autos oder Booten, abhängig von der Tab-Auswahl im Action Bar. Rechts oben befindet sich die Detailansicht in Form eines Bildes, realisiert durch ein Fragment (ArticleDetailsFragment). Unten rechts befindet sich eine Liste, die per Drag-and-Drop befüllt wird. Sie nimmt jeweils ein Haus, ein Auto und ein Boot auf.

In der onCreate-Methode wird der Action Bar mit Tabs befüllt. Die Werte liefert die Klasse Directory und ihre Hilfsklassen (DirectoryCategory und DirectoryEntry), die wir aus dem HoneycombGallery-Beispiel übernommen haben. Jedes Tab erhält eine Kategorie als Namen und einen Listener, um auf Ereignisse reagieren zu können. Um Platz zu sparen, können die Tab-Einträge auch als Drop-Down-Liste dargestellt werden. Dann ist mittels bar.setNavigationMode die Darstellung auf ActionBar.NAVIGATION_MODE_LIST zu ändern und die Implementierung anzupassen.

Das Berühren eines Tabs wird in der Methode onTabSelected ausgewertet. In dieser Methode sieht man, wie man aus der Activity heraus auf die von ihr verwalteten Fragments zugreift. Ähnlich wie findViewById im Zusammenhang mit Activities kommt bei Android 3.0 der Methode findFragmentById eine zentrale Rolle zu.

Die Methode wird hier genutzt, um zu einer Kategorie (Haus, Auto, Boot) die entsprechenden vordefinierten Einträge in einer Liste anzuzeigen. Später wird dies im noch zu erstellenden ArticleListFragment in der Methode populateArticleList implementiert.

Der Action Bar beherbergt seit Android 3.0 das frühere Optionsmenü. Für die Activity bedeutet dies keine Veränderung, die Implementierung ist exakt dieselbe.

Wir nutzen die onOptionsItemSelected-Methode, um unter anderem eine Interaktion zwischen Action Bar und einem Fragment zu demonstrieren. Klickt man auf das Einkaufwagen-Symbol, so verschwindet die Liste mit den ausgewählten Statussymbolen unten rechts. Auch hier verwenden wir wieder eine Transaktion, um darin mittels der Methoden show bzw. hide das CartFragment ein- und auszublenden. Dies soll jedoch animiert erfolgen. Mittels der Standard-Animatoren fade_in und fade_out der R-Klasse von Android lässt sich dies auf einfache Art und Weise erledigen.

Die Anzeige einer Hilfeseite mittels der Klasse FragmentDialog haben wir aus Platzgründen ausgespart.

Die Anzeige der späteren Präsentation der ausgewählten Statussymbole erfolgt durch Klick auf das Auge-Symbol im Action Bar. Hierfür wurde eine eigene Activity verwendet (siehe Listing 7)

Fragmente für Listendarstellungen nutzen

Kommen wir nun zu unserem ersten Fragment. Die Klasse ArticleListFragment (siehe Listing 4) ist von ListFragment abgeleitet und realisiert die linke Auswahlliste. Da das Fragment zusammen mit der Activity erzeugt wird und für die gesamte Lebensdauer der Activity angezeigt wird (es ändern sich nur seine Inhalte), können wir die Methode onActivityCreated für Initialisierungen verwenden. Alternativ käme onCreate in Frage, falls das Fragment zur Laufzeit durch ein anderes ersetzt würde. Denn die Activity verwaltet die Fragments und wird nicht neu erzeugt, wenn sich ihre Fragments ändern. Hingegen wird die Methode onCreate eines Fragments jedes mal aufgerufen, wenn das Fragment neu erzeugt wird. Es gilt also je nach Anwendungsfall die Initialisierungen des Fragments an der richtigen Stelle vorzunehmen.

Das ArticleListFragment implementiert eine Drag-and-Drop-Funktion. Basis dafür ist ein onItemLongClickListener, der in der Methode onActivityCreated erzeugt wird.

Listing 4: Fragment zur Darstellung von Listen mit Drag-and-Drop-Funktion.

public class ArticleListFragment extends ListFragment {
        
        private int mCategory = 0;
    private int mCurPosition = 0;
    
    @Override
    public void onActivityCreated(Bundle savedInstanceState) {    
        super.onActivityCreated(savedInstanceState);
        
        populateArticleList(0);
        
        selectPosition(mCurPosition);
        
        ListView lv = getListView();
        lv.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
        lv.setCacheColorHint(Color.TRANSPARENT);
        lv.setOnItemLongClickListener(new OnItemLongClickListener() {
            public boolean onItemLongClick(AdapterView<?> av, View v, int pos, long id) {
                showDetails(pos);
                
                final String title = (String) ((TextView) v).getText();

                final Intent intent = new Intent();
                intent.putExtra("category", mCategory);
                intent.putExtra("position", pos);
                final ClipData data = ClipData.newIntent(title, intent);
                v.startDrag(data, new MyDragShadowBuilder(v), null, 0);
                
                return true;
            }
        });
    }

    @Override
    public void onListItemClick(ListView l, View v, int position, long id) {
        showDetails(position);
    }
    
    public void populateArticleList(int category) {        
        DirectoryCategory cat = Directory.getCategory(category);
        String[] items = new String[cat.getEntryCount()];
        for (int i = 0; i < cat.getEntryCount(); i++) {
            items[i] = cat.getEntry(i).getName();
        }    
        setListAdapter(new ArrayAdapter<String>(getActivity(),
                android.R.layout.simple_list_item_activated_1, items));
        mCategory = category;
    }
    
    public void selectPosition(int position) {
        ListView lv = getListView();
        lv.setItemChecked(position, true);
        showDetails(position);
    }

    void showDetails(int position) {
        mCurPosition = position;

        getListView().setItemChecked(position, true);

        ArticleDetailsFragment articleDetailsFragment =
                (ArticleDetailsFragment) getFragmentManager()
            .findFragmentById(R.id.article_details);
        articleDetailsFragment.updateDetails(mCategory, position);            
    }
    
    private class MyDragShadowBuilder extends View.DragShadowBuilder {
        private Drawable mShadow;

        public MyDragShadowBuilder(View v) {
            super(v);
            mShadow = new ColorDrawable(Color.LTGRAY);
            mShadow.setBounds(0, 0, v.getWidth(), v.getHeight());
        }

        @Override
        public void onDrawShadow(Canvas canvas) {
            super.onDrawShadow(canvas);
            mShadow.draw(canvas);            
        }
    }
}

 Die Methode demonstriert zwei neue Funktionen, bzw. Klassen von Android 3.0. Die erste ist ClipData. Dies ist lediglich ein Container für Daten, die in einem internen Clipboard abgelegt werden. Das Thema ist wesentlich komplexer als es hier den Anschein hat. Android bringt ein umfangreiches Clipboard mit, von dem wir hier nur die einfachste Funktion genutzt haben. Es lassen sich Daten in Form von Text, Intents oder URIs an das Clipboard übergeben. Wir erzeugen in der Methode onItemLongClick einen Intent. Daten, die dem Clipboard hinzugefügt werden, müssen einen MIME-Type erhalten. Da wir die statische Methode newIntent von ClipData nutzen, wird der MIME-Type automatisch aufMIMETYPE_TEXT_INTENT gesetzt.

Die zweite neue Klasse, die in Listing 4 vorgestellt wird, ist View.DragShadowBuilder. Mit Hilfe dieser Klasse lassen sich durch einen Long-Klick für Drag-and-Drop markierte Objekte grafisch hervorheben. Im Konstruktor erhält man die View und kann deren Größe ermitteln. Es wird ein Drawable erzeugt (mShadow), welches einen grauen Hintergrund erzeugt.

In der Methode onDrawShadow erhält man einen Canvas als Zeichenfläche, der mit der Drag-Bewegung verschoben wird. Wir zeigen darauf den grauen Hintergrund von der Größe der View (dem Listeneintrag) an.

Ansonsten bietet die Klasse nur typische Listenfunktionalität, wie man sie von der Klasse ListActivity kennt.

Zu erwähnen ist noch, dass in der Methode showDetails dafür gesorgt wird, dass zu jedem ausgewählten Listeneintrag auch das passende Bild angezeigt wird.

Fragments

Die Anzeige des in der Liste ausgewählten Bildes erfolgt in der Klasse ArticleDetailsFragment. Listing 5 zeigt die Implementierung.

Listing 5: Detailansicht mittels der Klasse Fragment

public class ArticleDetailsFragment extends Fragment {  
        @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        
        final View view = inflater.inflate(R.layout.details_layout, null);

        view.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                final ActionBar bar = ArticleDetailsFragment.this.getActivity()
                        .getActionBar();
                if (bar != null) {
                    if (bar.isShowing()) {
                        bar.hide();
                    } else {
                        bar.show();
                    }
                }
            }
        });
        return view;        
    }

    public void updateDetails(final int category, final int position) {
        
        final ImageView imageView = (ImageView) getView().findViewById(R.id.image);
        imageView.setImageDrawable(Directory.getCategory(category).getEntry(position)
                .getDrawable(getResources()));        
    }
}

 Die Anzeige wird in der öffentlichen Methode updateDetails aktualisiert. Die Methode getView liefert das Fragment selbst. Hier wird man wieder daran erinnert, dass Fragments letztendlich Views in einer Activity sind, obwohl sie in ihrer Implementierung stark an Activities erinnern.

Der Zugriff auf die ImageView zur Anzeige des Bildes erfolgt schließlich wie gewohnt mittels findViewById. Je nach Kategorie und Position in der Auswahlliste liefert die Klasse Directory das korrekte Drawable, also das anzuzeigende Bild, welches der ImageView übergeben wird.

Um zu demonstrieren wie man den Action Bar verschwinden lassen kann, besitzt das Fragment einen On-Click-Listener, der in der Methode onCreateView erzeugt wird. Der Action Bar wird ein- bzw. ausgeblendet, wenn man auf das ArticleDetailsFragment klickt, welches nur aus dem Bild besteht. Den Action Bar muss man sich mit Hilfe der Methode getActionBar über die Activity holen, die das Fragment anzeigt.

Das Bild vergrößert sich automatisch ein wenig, wenn der Action Bar ausgeblendet wird, da dem Fragment nur mehr Platz zur Verfügung steht. Dies erreicht man durch Angabe eines zusätzlichen XML-Attributs bei der Definition der ImageView im Layout (Quellcode auf der CD):

android:scaleType="fitCenter"

Anzeige der Auswahlliste

So wie einen Warenkorb können wir in der Anwendung eine Auswahlliste mit einem Haus, einem Auto und einem Boot befüllen. Hierzu implementieren wir ein zweites ListFragment (Listing 6). Es verwaltet eine Liste von CartItems (mCartItems) die über einen selbst implementierten Array-Adapter (CartArrayAdapter) mit dem ListFragment verbunden ist.

Listing 6: Auswahlliste mit Drop-Funktion

public class CartFragment extends ListFragment {
        
        
        private final List<CartItem> mCartItems = new ArrayList<CartItem>();
        
        private CartArrayAdapter mListAdapter;  
                
        @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        
        
        mListAdapter =
                new CartArrayAdapter((Context) getActivity(), mCartItems);
        setListAdapter(mListAdapter);
      
        final ListView listView = getListView();
        listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
        
        populateCart();
        
        listView.setOnDragListener(new View.OnDragListener() {
            @Override
            public boolean onDrag(View v, DragEvent event) {
                switch (event.getAction()) {
                        case DragEvent.ACTION_DRAG_STARTED:                
                            return processDragStarted(event);
                        case DragEvent.ACTION_DROP:                            
                    return processDrop(event);
                }
                return false;
            }
        });
    }
        
        private boolean processDragStarted(DragEvent event) {
                ClipDescription clipDesc = event.getClipDescription();
        if (clipDesc != null) {
            return clipDesc.hasMimeType(ClipDescription.MIMETYPE_TEXT_INTENT);
        }
        return false;
        }
        
        boolean processDrop(DragEvent event) {
                final ClipData data = event.getClipData();
                
        if (data != null) {
                
            if (data.getItemCount() > 0) {
                final Item item = data.getItemAt(0);
                final Intent intent = item.getIntent();
                if (intent != null) {                
                    final int categoryId = intent.getIntExtra("category", -1);
                    final int entryId = intent.getIntExtra("position", -1);
                    
                    // Warenkorb aktualisieren:
                    final DirectoryCategory cat = Directory.getCategory(categoryId);
                    final DirectoryEntry entry = cat.getEntry(entryId);
                    
                    // bei gleicher Kategorie: ersetzen...
                    for (int pos = 0; pos < mCartItems.size(); pos++) {
                        if (mCartItems.get(pos).categoryId == categoryId) {
                                mCartItems.remove(pos);  
                        }                      
                    }
                    
                    CartItem cartItem = new CartItem();
                    cartItem.categoryId = categoryId;
                    cartItem.entryId = entryId;
                    cartItem.itemName = entry.getName();
                    cartItem.imageRessourceId = entry.getResourceId();
                    mCartItems.add(cartItem);                                        

                    populateCart();
                    
                    return true;
                }
            }
        }
        return false;
        }

    @Override
    public void onListItemClick(ListView l, View v, int position, long id) {
    
        ArticleDetailsFragment articleDetailsFragment =
                (ArticleDetailsFragment) getFragmentManager()
            .findFragmentById(R.id.article_details);
        
        CartItem cartItem = mListAdapter.getItem(position);
        articleDetailsFragment.updateDetails(cartItem.categoryId, cartItem.entryId);            
    }
    
    private void populateCart() {  
        mListAdapter.notifyDataSetChanged();                    
    }
    
    public List<CartItem> getCartItems() {
                return mCartItems;
        }
}

 Auch dieses Fragment wird mit der Activity erzeugt, weshalb wir die Initialisierungen in der onActivityCreated-Methode vornehmen.

Da das Fragment als Ziel einer Drag-and-Drop-Aktion dient, fügen wir ihm einen Drag-Listener hinzu. Über diesen können wir reagieren, wenn ein Drag-Event startet oder ein Drop-Event erfolgt.

In der Methode processDragStarted prüfen wir, ob es sich um den richtigen MIME-Type, also um einen Intent handelt, damit wir kein falsches Drag-and-Drop-Event auswerten. Die Methode processDrop hat die Aufgabe, anhand der im Intent übergebenen Kategorie und Position in der Auswahlliste ein CartItem zu erzeugen und dieses dem ListFragment hinzuzufügen. Kategorie und Position werden im Intent übermittelt. Da von jeder Kategorie nur eine Position gespeichert wird, werden alte Einträge bei gleicher Kategorie durch neue ersetzt.

In der Methode onListItemClick wird dafür gesorgt, dass das richtige Bild im ArticleDetailsFragment angezeigt wird, wenn man auf einen Listeneintrag klickt.

Animationen

Android 3.0 bringt ein verbessertes Animation-Framework mit [3]. Bisher ließen sich Views nur auf bestimmte, festgelegte Art und Weisen animieren. Dieses Verfahren heißt „View Animation“. Typische Animationen waren Ein- und Ausblenden, Verschieben, Drehen oder Skalieren von Views. Es war Aufgabe des Systems, die Views als Ganzes zu animieren. Android hat dazu schlicht die View als Bild verschoben, die Position im Layout aber unverändert gelassen.

Dies hatte einige unschöne Quereffekte. Ein per Animation verschobener Button konnte nur auf seiner alten Position angeklickt werden und nicht dort, wo sich jetzt das Bild des Buttons befindet. Nach der Animation musste man folglich das Layout programmatisch zur Laufzeit anpassen, was bekanntermaßen aufwändig ist, da man sich die Zustände merken muss, damit die Activity, z.B. nach dem Drehen des Bildschirms noch genauso aussieht wie vorher.

Mit Android 3.0 wird die „Property Animation“ eingeführt. Der Unterschied liegt darin, dass nun jede beliebige Property einer View animiert werden kann. Animationen sind sogar nicht mehr an Views gebunden. Jedes beliebige Objekt mit setter-Methoden für seine Attribute kann „animiert“ werden.

Animation bedeutet folglich, dass man die Werte eines Attributs einer Klasse über die Zeit verändern kann. Ist die Klasse zufällig eine View wird das Ergebnis sofort sichtbar. Animiert man eine komplette View, so wird nicht nur die Darstellung der View verschoben, sondern die View als Ganzes. Somit funktioniert im Falle eines Buttons nun auch die onClick-Methode an der richtigen Stelle.

Wir zeigen in unserem Beispielprogramm, wie man eine Animation einer ViewGroup implementieren kann. In unserem Fall handelt es sich bei der ViewGroup um das Layout der Präsentations-Activity. Wenn wir auf das Auge-Symbol im Action Bar drücken (siehe Abbildung 1), wird eine Activity aufgerufen, die die drei Bilder (Haus, Auto und Boot) einschweben lässt.

Listing 7 zeigt die ShowActivity. Ihr Layout ist ein leeres LinearLayout. In der onCreate-Methode werden die drei notwendigen ImageViews erzeugt und dem Layout hinzugefügt.

Listing 7: ein einfaches animiertes Layout mit Hilfe von Properties Animation

public class ShowActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {    
        super.onCreate(savedInstanceState);

        setContentView(R.layout.show_layout);        

        final LinearLayout linearLayout =
            (LinearLayout) findViewById(R.id.show_layout_id);
        
        final Animator[] animators = createAnimations();

        final LayoutTransition transition = new LayoutTransition();
        transition.setDuration(2000);
        linearLayout.setLayoutTransition(transition);
                      
        final Parcelable[] items = getIntent().getParcelableArrayExtra("cartitems");
        int i=0;
        for (Parcelable cartItem : items) {            
            if (cartItem != null && cartItem instanceof CartItem) {
                transition.setAnimator(LayoutTransition.APPEARING, animators[i++]);

                final ImageView imageView = new ImageView(this);                  
                imageView.setImageResource(((CartItem)cartItem).imageRessourceId);
                imageView.setAdjustViewBounds(true);
                imageView.setMaxWidth(420);              
                linearLayout.addView(imageView);
            }
        }
    }

    private Animator[] createAnimations() {        
        final PropertyValuesHolder propValuesOne =
            PropertyValuesHolder.ofFloat("y", -420f, 400f);
        final PropertyValuesHolder propValuesTwo =
            PropertyValuesHolder.ofFloat("y", 800f, 0f);
        
        final Animator firstImageAnimation =
                ObjectAnimator.ofPropertyValuesHolder(this, propValuesOne);
        final Animator secondImageAnimation =
                ObjectAnimator.ofPropertyValuesHolder(this, propValuesTwo);
        final Animator thirdImageAnimation =
                ObjectAnimator.ofPropertyValuesHolder(this, propValuesOne);
        
        return new Animator[]{firstImageAnimation,
                        secondImageAnimation, thirdImageAnimation};
    }
}

Die zentrale Klasse für Animationen seit Android 3.0 ist android.animation.Animator. Diese Oberklasse ist die Basis für verschiedene Animationsklassen. Wir werden hier die Klasse ObjectAnimator zeigen, da wir ganze Views animieren und nicht einzelne Properties. Komplexer ist die Animation einzelner Properties einer View mit Hilfe der Klasse ValueAnimator. Einen guten Überblick zum Animieren von Properties liefert.

Die Methode createAnimation in Listing 7 definiert drei Objekt-Animationen mit Hilfe von PropertyValuesHolder-Klassen. Zwei Bilder sollen von oben nach unten einschweben und das mittlere Bild von oben nach unten. Die Property für die Y-Position der ImageView auf dem Bildschirm heißt „y“. Mittels PropertyValuesHolder.ofFloat wird für diese Property der Start- und Endpunkt definiert. Basis ist der obere rechte Eckpunkt des Bildes, weshalb einer der Startwerte im Negativen liegt.

Entsprechend den drei dynamisch in der onCreate-Methode erzeugten ImageViews des Layouts werden anschließend drei Animator-Objekte erzeugt. Hierbei hilft die Klasse ObjectAnimator mit ihrer statischen Methode ofPropertyValuesHolder. Die Methode erzeugt ein ObjectAnimator-Objekt welches die in den PropertyValueHolder-Objekten gespeicherten Werte als Beschreibungsvorschrift für die Animation enthält.

Möchte man mehrere Properties gleichzeitig oder nacheinander animieren, sollte man sich die Klasse android.animation.AnimatorSet näher anschauen.

Nachdem die Animationen definiert wurden, wird in der onCreate-Methode mit Hilfe der Klasse LayoutTransition definiert, wie das gesamte Layout zu animieren ist. Als Animationsdauer werden 2 Sekunden (2000 Millisekunden) gesetzt und dem Layout die Transition („Übergang“ oder „Überleitung“) hinzugefügt.

Innerhalb der for-Schleife wird nun die Verbindung zwischen den definierten Animationen und der Transition hergestellt. Auf der Transition wird dazu die Methode setAnimator aufgerufen.

Fragments und Animationen vor Android 3.0 einsetzen

Es steht noch nicht fest, ob die neuen Android 3.0-Zweige des Quellcodes in frühere Android-Versionen einfließen werden. Zur Zeit bietet Google mit dem „Android Compability Package“ eine Möglichkeit, zumindest einen Teil der Funktionalität von Android 3.0 in Programmen ab Android 1.6 zu verwenden. Dazu muss man über den AVD Manager das oben genannte Package installieren und die Jar-Datei „android-support-v4.jar“ in sein Projekt einbinden. Sie befindet sich im Ordner <ANDROID-SDK>/extras/android/compatibility/v4.

Die Library stellt einen großen Teil der neuen Fragment- und Animation-API bereit. Allerdings muss man beachten, dass zum Beispiel der Action Bar vollständig fehlt. Eine Android 3.0-App durch einfaches einbinden der Compatibility Library auch für Android 2.x lauffähig zu machen, scheitert zumindest daran.

Fazit

Große und kleine Bildschirme erfordern unterschiedliche Programmoberflächen. Google hat versucht, beides unter einen Hut zu bekommen und liefert mit den Fragments die Möglichkeit, für alle Bildschirmgrößen mit wenig Programmieraufwand gute Ergebnisse zu erzielen. Die Activity dient nun als Container für Fragments. Die API bietet viele Möglichkeiten, auf die Fragments zuzugreifen. Bei unterschiedlichen Bildschirmgrößen kann dies genutzt werden, um mehr oder weniger Fragments in einer Activity anzuzeigen.

Der frühere Title Bar heißt nun Action Bar und hat zusätzliche Funktionen wie Tabulatoren (Tabs) und das Optionsmenü erhalten. Er ist nicht in der Android Compatibility Library enthalten und somit nur ab Android 3.0 verfügbar.

Animationen in Android sind oft kritisiert worden. Durch die neue Technik der Property Animation können beliebig viele Properties einer View animiert werden. Views, die ihre Position zur Laufzeit durch eine Animation verändern, werden nun „echt“ im Layout verschoben, wodurch Buttons nun z.B. auf Klickereignisse korrekt reagieren. Die API ist umfangreich, bietet aber fast alles, was das (Designer-) Herz begehrt.