20. Februar 2013

Android: Die Fragment-Methoden onSaveInstanceState und onActivityCreated

Der Inhalt dieses Posts soll eine kleine Ergänzung zu diesem kurzen Tutorial, wie in Android Activities ihre Eigenschaften behalten können, auch wenn sie, beispielsweise durch das Drehen des Geräts, neu erzeugt werden, darstellen. Die selbe Problemstellung existiert nämlich auch für Fragments, wobei diese keine onRestoreInstanceState-Methode anbieten. Stattdessen muss in diesem Fall die onActivityCreated-Methode verwendet, die im Lifecylce aufgerufen wird, nachdem die zugehörige Activity erzeugt wurde. 

Der Fragment-Lifecycle (Bildquelle: www.developer.android.com, Klicken zum Vergrößern)

Ausgehend von dem, in dem oben beschriebenen, vergangenen Post, verwendeten Beispiel, soll die Verwendung der entsprechenden Methoden zur Zustandsspeicherung von Fragments nun kurz erläutert werden. Das XML-Layout, das die Oberfläche beschreibt, bleibt dabei das selbe: 
 <?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" >   
     
   <EditText   
    android:id="@+id/edit"   
    android:layout_width="fill_parent"   
    android:layout_height="wrap_content" />   
     
 </LinearLayout>
Der Code des Fragments würde dagegen folgendermaßen aussehen:
 public class ExampleFragment extends Fragment {  
   
   EditText edit;  
   
   @Override  
   public View onCreateView(LayoutInflater inflater, Viewgroup container,Bundle savedInstanceState) {  
     View view = inflater.inflate(R.layout.main, container, false);
     edit = (EditText) view.findViewById(R.id.edit);
     return view; 
   }  
   
   @Override  
   public void onSaveInstanceState(Bundle outState) {  
     super.onSaveInstanceState(outState);  
     outState.putString("text", (String) text.getText());  
   }  
   
   @Override  
   public void onActivityCreated(Bundle savedInstanceState) {  
     super.onOnActivityCreated(savedInstanceState);  
     if (savedInstanceState != null) {  
       edit.setText(savedInstanceState.getString("text"); 
     }
   }  
    
 }  
Die onSaveInstanceState-Methode bleibt dabei exakt die selbe, während die Funktionalität zum Wiederherstellen der Werte aus dem gepeicherten Bundle in diesem Fall von der Methode onActivityCreated übernommen wird. Auch diese bekommt als Parameter das zwischengespeicherte Bundle übergeben aus dem dann der Text ausgelesen werden kann. Als Besonderheit ist jedoch zu beachten, dass unbedingt eine Null-Prüfung auf das Bundle-Objekt erfolgen muss, da dieses null ist, wenn das Fragment erzeugt wird, ohne dass zuvor die onSaveInstanceState-Methode aufgerufen worden ist (also beim erstmaligen Erzeugen des Fragments).

9. Februar 2013

Android: Die Activity-Methoden onSaveInstanceState und onRestoreInstanceState

Standardmäßig verlieren die Activities einer Android-App temporäre Werte, wie Eingaben in Textfelder, die Auswahl von Checkboxen oder ähnliches, wenn sie neu erzeugt werden. Dies kommt beispielsweise vor, wenn vom Portrait- in den Landscape-Modus gewechselt wird oder umgekehrt und ist in so einem Fall oft unerwünscht. Deshalb bietet die Android-API die beiden Methoden onSaveInstanceState(Bundle outState): void und onRestoreInstanceState(Bundle savedInstanceState): void an, die es ermöglichen, zu erhaltene Zustände zwischenzuspeichern und anschließend wiederherzustellen. Die beiden Methoden werden dabei, dem Activity-Lifecycle entsprechend automatisch aufgerufen. Das folgende kurze Beispiel soll die Verwendung der beiden Methoden demonstrieren.

Der Activity-Lifecycle (Bildquelle: www.developer.android.com, Klicken zum Vergrößern)

Das folgende XML Layout File definiert die Oberfläche einer Activity, die ein Textfeld enthält. Ist dort ein Text eingetragen, geht dieser verloren, wenn vom Portrait- in den Landscape-Modus gewechselt wird, oder umgekehrt. Dieser unerwünschte Effekt soll im Folgenden behoben werden.

 <?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" >   
     
   <EditText   
    android:id="@+id/edit"   
    android:layout_width="fill_parent"   
    android:layout_height="wrap_content" >   
     
  </LinearLayout>   
Der Code der dazugehörigen Activity, inklusive der beiden oben genannten Methoden zum Speichern und Wiederherstellen des eingegebenen Textes, sieht dabei folgendermaßen aus:
 public class ExampleActivity extends Activity {  
   
   EditText edit;  
   
   @Override  
   public void onCreate(Bundle savedInstanceState) {  
     super.onCreate(savedInstanceState);  
     setContentView(R.layout.main);  
      
     edit = (EditText)findViewById(R.id.edit);  
   }  
   
   @Override  
   public void onSaveInstanceState(Bundle outState) {  
    super.onSaveInstanceState(outState);  
    outState.putString("text", (String) text.getText());  
   }  
   
   @Override  
   public void onRestoreInstanceState(Bundle savedInstanceState) {  
    super.onRestoreInstanceState(savedInstanceState);  
    text.setText(savedInstanceState.getString("text"));  
   }  
    
 }  
Beim Erzeugen der Activity durch die onCreate-Methode wird das, im XML-File definierte, Textfeld zunächst anhand seiner ID referenziert. Bevor die Activity, beispielsweise durch ein Rotieren des Bildschirms, zerstört wird, wird automatisch die onSaveInstanceState-Methode aufgerufen, in der alle zu sichernden Werte zusammen mit einem String als Key in einem Bundle abgelegt werden können, das als Parameter übergeben wird. Das selbe Bundle wird dann an die onRestoreInstanceState-Methode übergeben, sobald die Activity neu erzeugt wurde. Die gespeicherten Werte können dann über den zuvor angegebenen Schlüssel aus dem Bundle gelesen und zur Wiederherstellung des vorherigen Zustandes verwendet werden.

Nachtrag vom 20. Februar 2013: Unter diesem Post ist von nun an die analoge Vorgehensweise zur Zwischenspeicherung von Zuständen in Fragments beschrieben. 

8. Februar 2013

Entwurfsmuster: Nullobjekt

Als "Nullobjekt" wird sowohl das gleichnamige Verhaltensmuster bezeichnet, als auch das dafür notwendige Objekt. Es ersetzt dabei den Wert "null", wenn keine Referenz auf ein gültiges Objekt vorhanden ist. Methodenaufrufe auf ein Nullobjekt führen dabei ins Leere, d.h. es wird keine Aktion ausgeführt.

Besondere Bedeutung haben Nullobjekte wenn sie in Verbindung mit Fabrikmethoden oder abstrakten Fabriken eingesetzt werden. Der Klient kennt dabei nur die Schnittstelle der, von der Fabrik zurückgegebenen Objekte, die entweder ein konkretes Objekt oder ein Nullobjekt sein können.

Vorteile:
  • Häufig benötigte if-Klauseln, die auf null-Referenzen prüfen, werden nicht weiter benötigt.
  • NullPointerExceptions, die bei vergessenen Null-Prüfungen auftreten können, werden vermieden.
Nachteile:
  • Das Nullobjekt muss die entsprechende Schnittstelle implementieren, was bei vielen Methoden einen großen Aufwand bedeuten kann.

Android: Gestenerkennung mit GestureOverlayView

In einem vergangenen Post wurde bereits die Erkennung von einfachen Gesten in einer Android-App vorgestellt. Das Problem bei der dort vorgestellten Vorgehensweise, die auf der Verwendung der Klasse "GestureDetector" basierte, war, dass nur diejenigen Gesten erkannt wurden, die direkt auf der entsprechenden Activity, bzw. View ausgeführt wurden. Wenn diese  von anderen Komponenten, wie z.B. Buttons verdeckt wurde, schlug die Erkennung fehl. Diese Problematik lässt sich durch die Komponente "GestureOverlayView" umgehen. Diese Möglichkeit der Gestenerkennung  soll in diesem Post vorgestellt werden. Der ursprüngliche Post findet sich hier. Das Beispiel in diesem Post basiert auf dessen Original-Beispiel.

Zunächst einmal muss eine GestureOverlayKomponente in das entsprechende XML Layout File integriert werden. In diesem Beispiel soll die Komponente über allen anderen Komponenten und über die komplette Bildschirmfläche gelegt werden. Der Aufbau der resultierenden XML-Datei sieht hierbei wie folgt aus:
 <?xml version="1.0" encoding="utf-8"?>  
 <android.gesture.GestureOverlayView xmlns:android="http://schemas.android.com/apk/res/android"  
   android:id="@+id/gestureOverlayView"  
   android:layout_width="fill_parent"  
   android:layout_height="fill_parent"  
   android:eventsInterceptionEnabled="true"  
   android:fadeEnabled="false"  
   android:fadeOffset="0"  
   android:gestureColor="#00000000"  
   android:gestureStrokeType="multiple"  
   android:orientation="vertical"  
   android:uncertainGestureColor="#00000000" >  
   
   <LinearLayout  
     android:layout_width="match_parent"  
     android:layout_height="match_parent"  
     android:orientation="vertical" >  
   
     <TextView  
       android:id="@+id/textView"  
       android:layout_width="wrap_content"  
       android:layout_height="wrap_content"  
       android:text="no gesture detected" />  
   </LinearLayout>  
   
 </android.gesture.GestureOverlayView>  

Standardmäßig werden die ausgeführten Gesten auf der Komponente visuell dargestellt, sollen diese jedoch ausschließlich erkannt werden, so ist diese Darstellung unerwünscht und kann vermieden werden, in dem man die zur Darstellung benutzen Farben über die Attribute android:gestureColor="#00000000" und android:uncertainGestureColor="#00000000" auf transparent stellt. Über die Attribute android:fadeOffset="0" und android:fadeEnabled="false" lässt sich die störende Verzögerung zwischen Gestenerkennung und daraus resultierender Aktion elimieren, die dadurch entsteht, das die Visualisierung, wenn vorhanden, zunächst langsam ausgeblendet wird, bevor die Aktion ausgeführt wird.

Im Gegensatz zur Verwendung der Klasse "GestureDetector", bietet die Verwendung einer GestureOverlayView weitreichendere Möglichkeiten, da beliebige Gesten über eine sogenannte Gesture-Library definiert werden können. Dazu bietet das Android-SDK ein Tool namens "GestureBuilder", das im Android-Emulator bereits als App vorhanden ist.Mithilfe dieser App lassen sich beliebige Gesten durch Zeichnen definieren, mit einem Namen versehen und in einer Datei exportieren. Dazu muss der Emulator so konfiguriert sein, dass er eine virtuelle SD-Karte benutzt (dies wurde bereits in diesem Post beschrieben). Die dadurch erzeugte Datei lässt sich beispielsweise über die Eclipse-View "File Explorer" aus dem Emulator in das reguläre Dateisystem übertragen (die Datei ist auf diese Weise unter dem Pfad mnt/sdcard/gesture zu finden). Die so erhaltene Gesture-Library muss imjeweiligen Projekt im Unterverzeichnis res/raw abgelegt werden.

Im GestureBuilder erstellte Gesten (Klicken zum Vergrößern)

Um die im XML-Layout definierte GestureOverlayView-Komponente zu benutzen um, in der Gesture-Library abgelegte, Gesten zu erkennen, muss anschließen der Code der entsprechenden Activity angepasst werden. Diese muss dazu das Interface android.gesture.GestureOverlayView.OnGesturePerformedListener implementieren. Der resultierende Code könnte etwa wie folgt aussehen:
 public class DashboardActivity extends Activity implements  
         OnGesturePerformedListener {  
   
     private GestureLibrary gestureLibrary;  
   
     @Override  
     public void onCreate(Bundle savedInstanceState) {  
         super.onCreate(savedInstanceState);  
         setContentView(R.layout.dashboard_activity);  
   
         gestureLibrary = GestureLibraries.fromRawResource(this, R.raw.gestures);  
   
         if (!gestureLibrary.load()) {  
             finish();  
         }  
   
         GestureOverlayView gestures = (GestureOverlayView) findViewById(R.id.gestureOverlayView);  
         gestures.addOnGesturePerformedListener(this);  
     }  
   
     public void onGesturePerformed(GestureOverlayView overlay, Gesture gesture) {  
         ArrayList<Prediction> predictions = gestureLibrary.recognize(gesture);  
   
         if (predictions.size() > 0) {  
             Prediction prediction = predictions.get(0);  
   
             if (prediction.score > 1.0) {  
                 if (prediction.name.equalsIgnoreCase("swipe_right")) {  
                     TextView textView = (TextView) findById(R.id.textView);  
                     textView.setText("detected swipe right gesture");  
                 } else if (prediction.name.equalsIgnoreCase("swipe_left")) {  
                     TextView textView = (TextView) findById(R.id.textView);  
                     textView.setText("detected swipe left gesture");  
                 }  
             }  
         }  
     }  
   
 }  
Die Activity erhält hierbei eine Instanz der Klasse android.gesture.GestureLibrary, in die, beim Aufruft der onCreate-Methode, die zuvor erstellte Gesture-Library aus der entsprechenden Datei geladen wird. Außerdem wird die in der XML-Datei definierte GestureOverlayView über ihre ID geladen und die Activity wird als Listener an ihr angemeldet.

Die eigentliche Erkennung findet nun in der onGesturePerformed-Methode statt, die automatisch bei der Erkennung einer definierten Geste aufgerufen wird. Es gilt dabei, zu überprüfen, welche Geste erkannt wurde, was über den, im GestureBuilder angegebenen, Namen möglich ist. In diesem Fall wurden zwei Swipe-Gesten definiert, deren Erkennung in einer TextView ausgegeben wird.

Android: Virtuelle SD-Karte für Emulator erzeugen

Der Android-Emulator "AVD" kann mit einer virtuellen SD-Karte, die eigentlich eine Datei darstellt, gestartet werden. So können Dateien aus dem Emulator heraus tatsächlich auf der Festplatte gespeichert werden.

Um eine solche virtuelle Festplatte benutzen zu können, muss zuerst eine entsprechende Datei mit dem dafür vorgesehenen Tool "mksdcard" erstellt werden. Das Tool befindest sich bei einer standardmäßigen Android SDK-Installation unter dem Pfad /tools/mksdcard und erwartet folgende Parameter: 
 mksdcard -l <label> <size> <file>
Die Parameter werden dabei folgendermaßen spezifiziert:
  • <label> Eine optionale Bezeichnung der virtuellen SD-Karte.
  • <size> Ein ganzzahliger Wert, der die Größe der SD-Karte in Bytes beschreibt. Durch das Anhängen von "K" oder "M" kann die größe auch in Kilobytes, bzw. Megabytes angegeben werden. 
  • <file> Der Pfad bzw. Dateiname der zu erzeugenden Datei
Beispielsweise erzeugt der folgende beispielhafte Befehlsaufruf eine Datei mit dem Namen "sdcard", die eine 1024 MB große SD-Karte simuliert und keine Beschreibung enthält.
 $ ./mksdcard 1024M sdcard  
Die offizielle Dokumentation des Tools findet sich unter der Adresse http://developer.android.com/tools/help/mksdcard.html.

Um eine, auf diese Weise erzeugte, virtuelle SD-Karte im Emulator zu nutzen, kann die Datei in dessen Einstellungen ausgewählt werden.

Emulator-Einstellungen mit ausgewählter virtueller SD-Karte (Klicken zum Vergrößern)

Nexus 10: Rooten unter Linux

Um ein Nexus 10-Tablet zu rooten, sind folgende Schritte nötig:
  1. Die Einschalten- und beide Lautstärke-Tasten gleichzeitig für etwa 10 Sekunden gedrückt halten, bis das Nexus 10 im "Bootloader"-Modus neu startet.
    Der Bootloader eines Nexus 10 (Quelle: androidpit.info; Klicken zum Vergrößern)
  2. Das Gerät mit dem dazugehörigen Micro-USB-Kabel mit einem Computer verbinden.
  3. Die zip-Datei "Nexus10RootNew.zip" unter folgender Adresse herunterladen: http://downloadandroidrom.com/file/Nexus10/Nexus10RootNew.zip
  4. Eine Konsole öffnen, in das Downloadverzeichnis wechseln und die heruntergeladene zip-Datei über folgende Befehle in einen neuen Unterordner kopieren und entpacken:
     $ mkdir Nexus10Root  
     $ mv Nexus10RootNew.zip Nexus10Root/Nexus10RootNew.zip  
     $ cd Nexus10Root  
     $ unzip Nexus10RootNew.zip  
  5. Anschließend müssen die über folgenden Befehl noch die Ausführrechte der entpackten Dateien angepasst werden:
     $ sudo chmod 755 *  
  6. Um den Bootloader zu entsperren muss nun folgender Befehler ausgeführt werden:
     $ sudo ./fastboot-linux oem unlock  
  7. Es erscheint eine Nachfrage, ob der Bootloader tatsächlich entsperrt werden soll (Achtung: Dadurch werden alle vorhandenen Daten auf dem internen Speicher des Nexus 10 gelöscht). Über die Lautstärke-Tasten lässt sich zwischen den Optionen navigieren. Nachdem die Option "Yes" gewählt wurde, lässt sich das Gerät über die Einschalten-Taste neu starten. Für den Fall, dass das Gerät anschließend nicht mehr startet, weil es in einer sogenannten "Bootloop" hängt, d.h. dass der Ladescreen nicht mehr verschwindet, gibt es unter der Adresse http://www.youtube.com/watch?v=T1Se7Hr9Cww&feature=player_embedded ein Video, das erklärt, wie das Problem gelöst werden kann.
  8. Anschließen muss man sich, wie beim ersten Anschalten üblich, über sein Google-Konto anmelden.
  9. Die zip-Datei "CWM-SuperSU-v0.98.zip", die sich ebenfalls bei den Dateien befindet, die bereits heruntergeladen wurden, muss nun über das USB-Kabel in ein beliebiges Verzeichnis auf dem Tablet kopiert werden. Falls es Probleme beim Verbinden über USB gibt, kann die Datei auch über den Link http://downloadandroidrom.com/file/tools/SuperSU/CWM-SuperSU-v0.98.zip direkt heruntergeladen werden.
  10. Nun muss wieder einmal in den Bootloader-Modus gebootet werden, indem die Einschalten- und die beiden Lautstärke-Tasten gleichzeitig für längere Zeit gedrückt werden.
  11. Jetzt muss der alternative Recovery-Modus "ClockworkMod Recovery", der beispielsweise das aufspielen von Custom-ROMs oder das Ausführen von Skripten erlaubt, auf das Gerät geflasht werden. Die img-Datei, die ClockworkMod-Recovery enthält, ist bereits in dem zuvor heruntergeladenen Root-Kit enhälten. Um den alternativen Recovery-Modus zu installieren, muss folgender Befehl aus dem jeweiligen Ordner heraus ausgeführt werden:
     $ sudo fastboot flash recovery recovery-clockwork-touch-6.0.1.6-manta-Wfix-by-damien667.img
  12. Nachdem die Dateien auf das Gerät geladen wurden, muss über die Lautstärke-Tasten der "Recovery Mode" gewählt werden und über die Einschalten-Taste der ClockworkMode Recovery-Modus gestartet werden.
  13. In dem daraufhin, nach einiger Zeit, erscheinenden Menü muss die Option "install zip from sdcard" und anschließend die Option "choose zip from sdcard" gewählt werden.
  14. Dann lässt sich über den Verzeichnisbaum die zuvor auf das Gerät kopierte Datei "CWM-SuperSU-v0.98.zip" in dem entsprechenden Ordner auswählen und deren Installation über einen Klick auf "Yes" bestätigen.
  15. Über mehrmaliges Drücken der Einschalten-Taste lässt sich das Gerät anschließend neu starten und sollte nun gerootet sein. Dies lässt sich über spezielle Apps aus dem Google PlayStore, wie beispielsweise der folgenden testen: https://play.google.com/store/apps/details?id=com.joeykrim.rootcheck&hl=de

4. Februar 2013

Android: Gestenerkennung (Swipe)

Die Bedienung eines Smartphones oder Tablets fühlt sich besonders elegant an, wenn die jeweilige App Gesten zur Bedienung anbietet. Dieser Post soll die Gestenerkennung von Wischgesten unter Android rudimentär erklären. Eine Übersicht über alle, von den Android-Entwicklern offiziell vorgesehenen Gesten, findet man unter http://developer.android.com/design/patterns/gestures.html. Dieses Beispiel soll sich dabei um Swipe-Gesten drehen, also eine Wischbewegung in eine bestimmte Richtung, wie sie etwa genutzt werden kann, um zwischen mehreren Seiten zu blättern oder eine Sidebar einzublenden.

Die Gestenerkennung ist innerhalb einer Activity oder einer View möglich. Für die Erkennung bietet die Android-API die Klasse android.view.GestureDetector an. Außerdem muss das Interface android.view.GestureDetector.OnGestureListener implementiert werden, um auf erkannte Gesten reagieren zu können. Für eine beispielhafte Activity könnte die Erkennung einer Swipe-Geste (in diesem Fall von links nach rechts) wie folgt aussehen:
 public class MainActivity extends Activity implements OnGestureListener {  
     
   private GestureDetector gestureDetector;   
   
   @Override  
   public void onCreate(Bundle savedInstanceState) {  
     super.onCreate(savedInstanceState);  
     setContentView(R.layout.activity_main);  
       
     gestureDetector = new GestureDetector(this,this);  
   }  
   
   @Override   
   public boolean onTouchEvent (MotionEvent event) {   
     this.gestureDetector.onTouchEvent(event);  
     return super.onTouchEvent(event);  
   }  
   
   @Override  
   public boolean onDown(MotionEvent event) {   
     return false;  
   }  
   
   @Override  
   public boolean onFling(MotionEvent event1, MotionEvent event2,   
       float velocityX, float velocityY) {  
     if (event1.getX() < event2.getX()) {  
       TextView textView = (TextView) findById(R.id.textView);  
       textView.setText("detected swipe gesture");  
       return true;
     }  
     return false;  
   }  
   
   @Override  
   public void onLongPress(MotionEvent event) {  
        
   }  
   
   @Override  
   public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,  
       float distanceY) {  
     return false;  
   }  
   
   @Override  
   public void onShowPress(MotionEvent event) {  
       
   }  
   
   @Override  
   public boolean onSingleTapUp(MotionEvent event) {  
     return false;  
   }  
   
   @Override  
   public boolean onDoubleTap(MotionEvent event) {  
     return false;  
   }  
   
   @Override  
   public boolean onDoubleTapEvent(MotionEvent event) {  
     return false;  
   }  
   
   @Override  
   public boolean onSingleTapConfirmed(MotionEvent event) {  
     return false;  
   }  
   
 }  
Die Instanz des GestureDetectors wird hierbei in der onCreate-Methode der Activity erzeugt, indem der Context, in diesem Fall die Activity selbst, sowie die entsprechende Listener-Implementation, in diesem Fall auch die Activity selbst, übergeben wird.

Die Activity, die in diesem Fall die Schnittstelle OnGestureListener implementiert, wird dadurch um einige Methoden erweitert. In diesem Beispiel ist nur die onFling-Methode, die für die Erkennung von Wischbewegungen gedacht ist, relevant. Der Methode werden, neben zwei Werten für die horizontale und vertikale Wischgeschwindigkeit, auch jeweils ein MotionEvent für den Start und das Ende der Wischbewegung als Parameter übergeben. Über diese MotionEvents lässt sich die Wischrichtung ermitteln. Handelt es sich um eine horizontale Swipe-Geste von links nach rechts, so wird in diesem Beispiel der Text einer TextView geändert.

Außerdem ist zu beachten, dass die Activity die onTouchEvent-Methode überschreiben muss, um das jeweilige TouchEvent an den GestureDetector und die Superklasse weiterzuleiten.

Die zugehörige XML Layout Datei zu diesem Beispiel sieht wie folgt aus:
 <?xml version="1.0" encoding="utf-8"?>  
 <LinearLayout  
   android:layout_width="match_parent"  
   android:layout_height="match_parent"  
   android:orientation="vertical" >  
   
   <TextView  
     android:id="@+id/textView"  
     android:layout_width="wrap_content"  
     android:layout_height="wrap_content"  
     android:text="no gesture detected" />  
   
 </LinearLayout>  
Ein Problem der hier beschriebenen Vorgehensweise kann sein, dass Gesten nur erkannt werden, wenn sie auf dem Hintergrund der Activity ausgeführt werden. Beinhaltet die Activity viele Komponenten wie beispielsweise Buttons, so werden darauf ausgeführte Gesten nicht erkannt. Die Problematik kann durch die Verwendung einer so genannten GestureOverlayView gelöst werden, die transparent über alle anderen Views gelegt wird um die Eingaben abzufangen. Auf diese Möglichkeit soll in einem weiterführenden Post eingegangen werden.

Nachtrag vom 8. Februar 2013: Der Post über oben erwähnte Möglichkeit zur Verwendung einer "GestureOverlayView" ist von nun an hier zu finden.

3. Februar 2013

Android: Lagesensordaten verwenden

Jedes aktuelle Smartphone besitzt heute einen internen Lagesensor, der Daten über die Lage und Ausrichtung des Geräts im Raum erfasst. Wie alle Sensordaten des Geräts, lassen sich auch die Daten dieses so genannten "Accelerometers" auswerten und von Apps verwenden. Das folgende Beispiel soll beispielhaft zeigen, wie so etwas aussehen kann.

Die generelle Vorgehensweise im Umgang mit Sensoren bei der Android-Programmierung ist es, einen entsprechenden Listener, der das Interface android.hardware.SensorEventListener implementiert an einem sogenannten SensorManager zu registrieren. Dieser SensorManager ist ein SystemService, über den alle Sensoren des Geräts angesprochen werden können. Über folgenden Methodenaufruf erhält man eine Instanz auf den SensorManager innerhalb einer Activity:
 SensorManager sensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);  
Um einen SensorEventListener an dieser SensorManager-Instanz zu registrieren reicht folgender Methodenaufruf:
 sensorManager.registerListener(listener, sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER), SensorManager.SENSOR_DELAY_UI);  
Der Parameter "listener" entspricht dabei dem SensorEventListener, der zum Empfang der Sensordaten registriert werden soll und dessen Implementierung weiter unten noch folgen wird. Die Konstante Sensor.TYPE_ACCELEROMETER bezeichnet den Lagesensor, der in diesem Beispiel verwendet werden soll. Analog dazu funktioniert auch die Verwendung anderer Sensoren. Außerdem lässt sich über den dritten Parameter das Intervall steuern, in dem die Sensordaten aktualisiert werden. Folgende Werte sind hierbei möglich:
  • SENSOR_DELAY_FASTEST: Hierbei werden die Daten so schnell wie technisch möglich aktualisiert. 
  • SENSOR_DELAY_GAME: Das Intervall, das für die Verwendung in Spielen gedacht ist. 
  • SENSOR_DELAY_NORMAL: Entspricht der Standard-Einstellung, die beispielsweise für das Umschalten zwischen vertikaler Ausrichtung und Landscape-Modus genutz wird. 
  • SENSOR_DELAY_UI: Das langsamste Intervall, das für für die Verwendung zur Gestaltung von Benutzeroberflächen gedacht ist.
Der Code für die gesamte Activity, inklusive der Implementierung des SensorEventListeners könnte in etwa wie folgt aussehen:
     public class ExampleActivity extends Activity {  
       
         private SensorManager sensorManager;  
       
         private SensorEventListener accelerometerListener = new SensorEventListener {  
    
             public void onAccuracyChanged(Sensor sensor, int accuracy) {
                 return;
             }
    
             public void onSensorChanged(SensorEvent event) {
                 if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
                     float x = event.values[0];
                     float y = event.values[1];
                     float z = event.values[2];
    
                     TextView xCoor = (TextView) activity.findViewById(R.id.xcoor);
                     TextView yCoor = (TextView) activity.findViewById(R.id.ycoor);
                     TextView zCoor = (TextView) activity.findViewById(R.id.zcoor);
    
                     xCoor.setText("X: " + x);
                     yCoor.setText("Y: " + y);
                     zCoor.setText("Z: " + z);
                 }
             }
    
         };  
       
         @Override  
         public void onCreate(Bundle savedInstanceState) {  
             super.onCreate(savedInstanceState);  
       
             sensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);  
             sensorManager.registerListener(accelerometerListener,  
                     sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER),  
                     SensorManager.SENSOR_DELAY_UI);  
         }  
       
         @Override  
         public void onDestroy() {  
             super.onDestroy();  
             sensorManager.unregisterListener(accelerometerListener);  
         }  
       
     }  
    Zu beachten ist hierbei, dass die Registrierung am SensorManager beim Beenden der Activity wieder rückgängig gemacht werden muss, da ansonsten weiterhin Sensordaten empfangen werden und die Anwendung niemals komplett beendet werden würde. Dies geschieht über einen entsprechenden Methodenaufruf indem die onDestroy()-Methode der Activity überschrieben wird.

    Die Sensordaten des Lagesensors umfassen drei Werte, die den drei Achsen des dreidimensionalen Raumes entsprechen. In diesem Beispiel werden die erfassten Werte durch den SensorEventListener lediglich auf drei TextViews ausgegeben. Hierfür müssen die TextViews mit den entsprechenden IDs natürlich definiert sein, was über folgendes XML Layout File geschieht:
     <?xml version="1.0" encoding="utf-8"?>  
     <TableLayout xmlns:android="http://schemas.android.com/apk/res/android"  
       android:layout_width="fill_parent"  
       android:layout_height="fill_parent" >  
       
       <TableRow>  
       
         <TextView  
           android:id="@+id/xcoor"  
           android:layout_width="wrap_content"  
           android:layout_height="wrap_content"  
           android:text="X Coordinate: " />  
       </TableRow>  
       
       <TableRow>  
       
         <TextView  
           android:id="@+id/ycoor"  
           android:layout_width="wrap_content"  
           android:layout_height="wrap_content"  
           android:text="Y Coordinate: " />  
       </TableRow>  
       
       <TableRow>  
       
         <TextView  
           android:id="@+id/zcoor"  
           android:layout_width="wrap_content"  
           android:layout_height="wrap_content"  
           android:text="Z Coordinate: " />  
       </TableRow>  
       
     </TableLayout>  

    2. Februar 2013

    Android: Bild in ImageView drehen

    Um in einer Android-App ein Bild, das durch eine ImageView-Komponente dargestellt wird, um einen beliebigen Winkel zu drehen, bietet sich die folgende Lösung an, die auf der Verwendung einer Transformations-Matrix (die Klasse android.graphics.Matrix der Android-API) basiert.
     ImageView imageView = (ImageView) findViewById(R.id.imageView);  
     Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher);  
       
     Matrix matrix = new Matrix();  
     matrix.postRotate(30.0f);  
       
     Bitmap rotatedBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);  
       
     imageView.setImageBitmap(rotatedBitmap);  
    Zunächst wird die Instanz der entsprechenden ImageView-Komponente, die in diesem Fall per XML in einem Layout-File definiert wurde, über ihre ID angesprochen. Anschließend wird eine neue Bitmap (eine Instanz der Klasse android.graphics.Bitmap), die ein bestimmtes Bild beinhaltet erstellt. Dann wird die Rotationsmatrix instanziiert und der Drehwinkel, in diesem Fall 30°, über die Methode postRotate(float degree)gesetzt. Über die statische Methode createBitmap(..), die unter anderem eine solche Matrix als Parameter entgegen nimmt, lässt sich eine zweite Bitmap-Instanz erstellen, die das, mithilfe der Matrix gedrehte, Bild enthält. Dieses muss nun nur noch an die zuvor instanziierte ImageView übergeben werden.