2013-08-28

Unboxing Android Mini Collectible Summer Ed. 13

Bevor der Sommer so ganz langsam zu Ende geht, muss ich doch unbedingt noch ein Unboxing nachschieben; die kleinen Freunde wohnen nun schon seit geraumer Zeit bei Moni und mir…

Android Mini Collectible Summer Ed. 13 (1)Android Mini Collectible Summer Ed. 13 (2)Android Mini Collectible Summer Ed. 13 (3)Android Mini Collectible Summer Ed. 13 (4)Android Mini Collectible Summer Ed. 13 (5)Android Mini Collectible Summer Ed. 13 (6)

2013-08-24

Ultimate Swing, Teil 29

Ich habe in letzter Zeit ein paar Verbesserungen an Notes and Tasks und der Schuhschachtel (Sie erinnern sich vielleicht: ich fasse potenziell wiederverwendbare Teile zu diesem Mini-Framework"chen" zusammen) vorgenommen. Neu ist beispielsweise das Interface IController. Es enthält die Methoden setup(), getModel() und getView(). Die beiden letztgenannten sind praktisch, weil übergeordnete Controller nicht für jedes Modell und jede Sicht untergeordneter Controller eigene Referenzen halten müssen. Die Klasse MainController von Notes and Tasks nutzt dies. Sie ist übrigens kein IController, da sie ja schon das Interface ApplicationCallback implementiert und damit den Lebenszyklus der Anwendung steuert. Natürlich ist auch das eine Art Controller. Die Methode setup() von IController-Implementierungen wird auf dem EDT aufgerufen. Hier können Bedienelemente mit Werten aus dem Modell befüllt oder Einstellungen aus den Preferences übernommen werden.

Eine weitere Ergänzung betrifft Drag and Drop. Wenn Sie eine Textdatei auf das Hauptfenster von Notes and Tasks schieben, wird diese zeilenweise eingelesen und jede Zeile erscheint als neue Aufgabe. Hierzu habe ich das Interface ApplicationCallback um die Methode fileDropped() erweitert. Für jede fallen gelassene Datei wird sie einmal aufgerufen. Die Implementierung der Drag and Drop-Funktionalität ist sehr einfach: In der Schuhschachtel-Klasse Application gibt es die Methode configureFrame(). Sie wird um wenige Zeilen Code erweitert.

 // drag and drop support  
 setTransferHandler(new TransferHandler() {  
     
   @Override  
   public boolean canImport(TransferHandler.TransferSupport support) {  
      // support.setDropAction(COPY); ??  
      return support.isDataFlavorSupported(DataFlavor.javaFileListFlavor);  
   }  
   
   @Override  
   public boolean importData(TransferHandler.TransferSupport support) {  
      boolean success = false;  
      if (canImport(support)) {  
        final Transferable t = support.getTransferable();  
        try {  
           final List<File> l =  
                (List<File>) t.getTransferData(DataFlavor.javaFileListFlavor);  
           for (File f : l) {  
              INSTANCE.callback.fileDropped(f);  
           }  
           success = true;  
        } catch (UnsupportedFlavorException e) {  
           LOGGER.throwing(getClass().getName(), "importData", e);  
        } catch (IOException e) {  
           LOGGER.throwing(getClass().getName(), "importData", e);  
        }  
      }  
      return success;  
   }  
 });  
   

Letztlich wird nur ein Objekt des Typs TransferHandler erzeugt und der Methode setTransferHandler() des Anwendungshauptfensters übergeben.

Die Implementierung der Leseroutine in Notes and Tasks (im MainController) ist schon fertig. Bauen Sie doch einmal eine Testdatei und ziehen diese auf das Hauptfenster. Einige Aufgaben werden angelegt, aber nicht alle. Sind Sie tief genug mit dem Quelltext vertraut, um herauszufinden, was da schief geht? Das Problem liegt in einer Methode, die in fileDropped() aufgerufen wird.

2013-08-21

Berechtigungen erben

Nutzer von Android-Apps sind leider oft leichtfertig, wenn es um das Abnicken von Berechtigungen geht. Umso mehr freut es mich, wenn Anwender nachfragen.

Ein Nutzer meiner App TKBirthdayReminder wollte wissen, warum ich lesend und schreibend auf das Anrufprotokoll zugreife. Ich war erstaunt. Mir war nämlich nicht bewusst, dies zu tun. Ein Blick in das Manifest offenbarte:

READ_CONTACTS, WRITE_CONTACTS, CALL_PHONE, RECEIVE_BOOT_COMPLETED, WAKE_LOCK, GET_ACCOUNTS und READ_SYNC_SETTINGS

Also kein Hinweis auf das Anrufprotokoll.

Nach etwas nicht allzu zielführender Recherche habe ich dann die Google-Doku konsultiert. Unter http://developer.android.com/reference/android/Manifest.permission.html#READ_CALL_LOG und http://developer.android.com/reference/android/Manifest.permission.html#WRITE_CALL_LOG steht sinngemäß: Wenn man die Berechtigungen "Kontakte lesen" und "Kontakte schreiben" anfordert (das muss TKBirthdayReminder tun, um Geburtsdaten lesen und schreiben zu können), “erbt” die App in älteren Systemversionen automatisch auch den lesenden bzw. schreibenden Zugriff auf die Anrufhistorie.

Googles Rat für alle, die die Rechte nicht benötigen: targetSdkVersion auf 16 oder höher setzen

2013-08-06

Ultimate Swing, Teil 28

Im vorherigen Post habe ich eine kleine Baustelle hinterlassen: Radiobuttons und Checkboxen haben eine Hintergrundfarbe, die zumindest unter bestimmten Look and Feels nicht zum Hintergrund von Registerkarten passt. Dieses Problem tritt auf, wenn die Komponenten Teil eines JPanels sind, für das mit setOpaque(false) der Hintergrund ausgeschaltet wurde. Das wiederum ist nötig, weil die Panels sonst (wieder nur unter bestimmten Look and Feels) den Hintergrund der Registerkarten verdecken.

Die Lösung ist einfach. Auch für die betroffenen Komponenten muss setOpaque(false) aufgerufen werden. Übrigens ist das Ganze nicht neu, wie ein Post auf code beach zeigt. Auch auf java.net gab es einen recht interessanten Thread.

Screenshot: Notes and Tasks im Metal-Look
Screenshot: Notes and Tasks im Metal-Look

Unter Windows 8 und dem guten alten Metal ist nun alles in Ordnung. Lassen Sie uns ausprobieren, wie sich Nimbus schlägt. Hierzu müssen wir nur Folgendes auf der Kommandozeile angeben:

-Dswing.defaultlaf=javax.swing.plaf.nimbus.NimbusLookAndFeel 

Screenshot: Notes and Tasks mit Nimbus-Look and Feel
Screenshot: Notes and Tasks mit Nimbus-Look and Feel

Sieht irgendwie komisch aus, oder? Lauert etwa schon der nächste Stolperstein auf uns? Der Trenner wird nicht gezeichnet, weil ich in meinem Quelltext beim Instantiieren einer JTabbedPane die so genannte tabLayoutPolicy auf SCROLL_TAB_LAYOUT gesetzt hatte. Mit diesem Parameter steuern Sie das Verhalten einer Tabbed Pane, wenn nicht alle Register gleichzeitig dargestellt werden können. Spätestens nach der Lektüre des Eintrags zu Fehler 7031915 wissen wir, dass Nimbus SCROLL_TAB_LAYOUT nicht implementiert. Zum Glück ist das Problem aber schnell behoben. Mit WRAP_TAB_LAYOUT klappt das Zeichnen. ...naja, zumindest halbwegs...

Screenshot: Verbesserte Version
Screenshot: Verbesserte Version

Wahrscheinlich fragen Sie sich wie ich, warum auf dem Screenshot kein Rahmen um die Tabbed Pane gezeichnet wird. Auch hierzu gibt es einen Eintrag in der Fehlerdatenbank. Unglücklicherweise harrt Fehler 6726247 aber noch einer Aufarbeitung. Und das wird sicher auch so bleiben.

Ein letzter Versuch, diesmal mit dem Windows Look and Feel. In diesem Fall geben Sie auf der Kommandozeile Folgendes an:

-Dswing.defaultlaf=com.sun.java.swing.plaf.windows.WindowsLookAndFeel

Screenshot: Notes and Tasks mit dem Windows Look and Feel
Screenshot: Notes and Tasks mit dem Windows Look and Feel

Diesmal habe ich nichts auszusetzen. Und was sagen Sie?

2013-08-04

Ultimate Swing, Teil 27

Heute möchte ich nicht viel über Programmierung erzählen, sondern ein paar Worte zum Thema Ergonomie sagen. Bitte sehen Sie sich den folgenden Screenshot etwas genauer an:
Screenshot: TKNotesAndTasks
Screenshot: TKNotesAndTasks
Auf den ersten Blick scheinen nur ein paar Details zu stören:
  • Die Hintergrundfarbe der Registerkarte ist uneinheitlich.
  • Der als Hinweis (hint) gedachte Text in der Eingabezeile am oberen Rand sieht aus, als wäre er vom Benutzer eingegeben worden.
  • Die erste Schaltfläche im rechten Bereich schließt nicht bündig mit dem oberen Rand der Liste ab.
Sozusagen Peanuts, oder?
Wenn Sie sich Notes and Tasks schon einmal live angesehen haben, wissen Sie, dass die Eingabezeile auf beide Registerkarten wirkt. Mit ihr können also Aufgaben und Kategorien eingegeben werden. Beim Umschalten wurde der Hinweistext sogar getauscht, damit der Anwender sofort sieht, was er im Begriff ist, einzutippen. Die Erwartung war, auf diese Weise die Eingabe so effizient wie möglich zu gestalten. Clever, oder?
Nein, nicht clever. Mit meiner Implementierung zwinge ich den Anwender nicht nur, mit den Augen zwischen Eingabezeile und Registerkarten zu pendeln, sondern erwarte zudem, dass er den langen Hinweistext liest. Wenn er dann feststellt, sich auf der “falschen” Registerkarte befindet, muss er mit den Augen (und der Maus) wieder pendeln. Die Idee, eine Eingabezeile für alle Aspekte der Anwendung (Aufgaben, Kategorien und später noch Notizen) zu bieten, ist also nicht ausgereift. Das richtige Vorgehen ist, den Anwender zuerst entscheiden zu lassen, mit welchem Teil der Anwendung er arbeiten möchte. Deshalb müssen die Reiter der Registerkarten ganz oben erscheinen. Denn wie wir die Seite eines Buches von links oben nach rechts unten abarbeiten, so betrachten wir auch den Bildschirm und damit die Benutzeroberfläche eines Programms.
Screenshot: Notes and Tasks mit verbesserten Layout
Screenshot: Notes and Tasks mit verbesserten Layout
Das sieht doch schon besser aus, oder? Der Benutzer kann neue Aufgaben anlegen und sieht diese nach dem Drücken der Enter-Taste in der Liste. Also alles in Butter?
Nehmen wir an, Sie möchten den Text einer Aufgabe bearbeiten. Die Idee könnte sein, hierfür die Eingabezeile zu “recyclen”. Bloß dann haben Sie das Problem mit der Pendelei wieder. Außerdem ist es fraglich, ob man häufiger neue Aufgaben erfasst oder den Status bestehender Aufgaben ändert. Ich habe mich deshalb zu folgendem Layout entschlossen:
Screenshot: Eingabezeile im unteren Randbereich
Screenshot: Eingabezeile im unteren Randbereich
Durch das Verschieben in den unteren Bereich sorge ich nicht nur für eine leichte Auffindbarkeit der Eingabezeile, sondern schaffe auch die richtige Gewichtung der Teilbereiche. Damit “funktioniert” sogar das Bearbeiten eines Aufgabentextes in der Eingabezeile, nachdem der Benutzer den Eintrag in der Liste angeklickt hat.
Zum Schluss noch zwei technische Details. Die “falsche” Panelhintergrundfarbe kann ich korrigieren, indem ich für Panels, die auf Registerkarten liegen, mit setOpaque(false) das Zeichnen des Hintergrunds unterbinde. Das ist im folgenden Screenshot zu sehen. Der leichte Versatz der Schaltflächen scheint ein Mac OS X-spezifisches Verhalten von Swing zu sein.
Screenshot: Wo kommen die Ränder her?
Screenshot: Wo kommen die Ränder her?
Denn unter Windows tritt der Versatz nicht auf:
Screenshot: TKNotes unter Windows
Screenshot: TKNotes unter Windows
Blöd nur, dass jetzt der Hintergrund der Checkboxen und RadioButtons nicht passt.
Oh, ich liebe Swing…
Zwinkerndes Smiley