niedziela, 10 stycznia 2010

Android TimePicker enhancement

Android has some nice UI concepts & components, I recently liked Toast Notification both from Android user and developer perspective.
User cannot miss that notification, but it's not iritating. And or developer - it's easy to show the kind of notification - just one line of code

Toast.makeText(getApplicationContext(), "message from Android", Toast.LENGTH_LONG).show();

However some components would benefit from some polishing - especially TimePicker, which allows user to pick a Time - Hour and Minute.
But it has a drawback - imagine You have 22:59 choosen:


and You click '+' to add one minute.
What do You expect? I expect to see 23:00 - I hope it's not too extravagant.
But Android gives You

yes, just 22:00, and if You want 23:00 You have to manually increase hour.

I would like my Users to be in better situation, so I decided to enhance TimePicker to support that. It wasn't that hard - it only needed one specific time change listener, which is handling approprate hour change and also delegating the event to another listener when done. It's strange that TimePicker is able to handle only one Listener each type, but You can always get around it.


import android.widget.TimePicker;
import android.widget.TimePicker.OnTimeChangedListener;

public class HourChangingListener implements OnTimeChangedListener {

int lastMinute = Integer.MIN_VALUE;

private final OnTimeChangedListener delegate;

public HourChangingListener(OnTimeChangedListener delegate) {
this.delegate = delegate;
}

@Override
public void onTimeChanged(TimePicker timePicker, int hour, int minute) {
if (minute == 0 && lastMinute == 59) {
lastMinute = minute;
incrementAndSetHour(timePicker, hour);
} else if (minute == 59 && lastMinute == 0) {
lastMinute = minute;
decrementAndSetHour(timePicker, hour);
} else {
delegate.onTimeChanged(timePicker, hour, minute);
}
lastMinute = minute;
}

private void decrementAndSetHour(TimePicker timePicker, int hour) {
setSafeHour(timePicker, hour - 1);
}

private void incrementAndSetHour(TimePicker timePicker, int hour) {
setSafeHour(timePicker, hour + 1);
}

private void setSafeHour(TimePicker timePicker, int hour) {
int base = 24;
hour = (base + hour) % base;
timePicker.setCurrentHour(hour);
}

}

And You use it by

TimePicker timePicker;
OnTimeChangedListener originalTimePickerChangeListener;

timePicker.setOnTimeChangedListener(new HourChangingListener(originalTimePickerChangeListener));

Keep in mind that HourChangingListener is not thread-safe and cannot be reusable to handle multiple TimePickers!

This works and when You add minute to 22:59 you get:



This solution needs enhancement to support client that doesn't need a delegate listener. I also consider this solution as a workaround as TimePicker should be supporting this internally.

Off-topic: Right now I think my app would gain more value if I would have done sth more 'core' than this small enhancement:)

wtorek, 1 grudnia 2009

Co było pierwsze, jajko czy kura - odpowiedź!

W odwiecznym, filozoficznym sporze nikt chyba jeszcze nie zapytał wujka google o radę.

Pierwsza była kura!

Jak widać linia dot. kury pierwsza odrywa się od osi. Dziwić może tylko data - 2004 rok. Najwidoczniej przedtem spór faktycznie nie był roztrzygnięty.

niedziela, 15 listopada 2009

Android checboxes in ListView with CheckedTextView tutorial

There is something wrong with Android doc. It is quite extensive, but missing some tutorials, or tips on how to do specific things. Especially Javadoc is too anemic. If some method/attribute is a part of some bigger design concept - it should be marked as that, and all requirements to make a basic scenario should be pointed out!

These are my conclusions after fighting with making a list on Android of elements that have a 'checkbox'! Because I couldn't find a tutorial for that on the web (I don't know how all folks cope with that kind of problems, maybe they just sit for half a day and experiment..), so here it is:

The purpose is to have a list of elements and checkboxes near them. Something like in this advanced checkboxes android tutorial.

1. To do that You have to put specific code when creating Your ListAdapter. I was using the SimpleCursorAdapter.


new SimpleCursorAdapter(getApplicationContext(),
android.R.layout.simple_list_item_multiple_choice, //important parameter
cursor, new String[] {..},
new int[] { android.R.id.text1 });//important parameter


both important parameters are elements of built-in Android layouts, but to use You have to know about them! And I have found them on some forum, not in the docs;-)

android.R.layout.simple_list_item_multiple_choice is the layout of CheckedTextView which will serve as a layout for one row in ListVew.
android.R.id.text1 is ID of this CheckedTextView - Your adapter will set text on this component.

2. You have to enable the multi choice (or single, depends on You)

getListView().setItemsCanFocus(false); //I don't know why is that, but I've seen that in all examples - it's working even without it
getListView().setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);


And it's done.

One additional step would be to use Your own predefined layout instead of android's bundled.

It's good that we see the source of that, but don't think it will be that easy to use simple layout of CheckedTextView! no, you need to

3. Ensure that undocumented parameter

android:checkMark="?android:attr/listChoiceIndicatorMultiple"

is included!
I am trying to understand why is it missing from the doc?
I asked on android developers group - and probably will have to put it into android's issues.

środa, 11 listopada 2009

Android + Metawidget = Interesting cooperation

Dzisiejszy post bedzie w jezyku angielskim, bo to, co tu pokaze nie widzialo jeszcze swiatla dziennego, zatem chcialbym sie tym podzielic latwiej z wieksza iloscia zainteresowanych.
Nie bedzie to ani wprowadzenie do Androida. Duzo materialów jest w necie, dobre wprowadzenie zapewnia strona Android Developers
Nie bedzie to tez wprowadzenie do Metawidget - dokumentacje tego narzedzia jest naprawde dobra, a autor oferuje szybkie i porzadne wsparcie na forum.
Bedzie to case study na temat wprowadzania Metawidgeta do aplikacji na Androida - dostepne dla kazdego, nawet bez znajomosci zadnej z powyzszych technologii.

Lately I've been deep into researching Android as Java development platform. As I'm not really a 'UI-guy' I thought to try to use some tool to make all the dirty (no offence) UI job done. I decided to use Metawidget, which is able to generate views in multiple technologies, with Android among them.

Full of optimism I started creating my first application (It will be in the Android Market this year).
First I wanted a View for editing my 'Sleep' object.

public class Sleep1 {

public int id;
public Date goSleep;
public Date wakeUp;

}

I can hear the voices shouting "What?! public fields!? - haven't You heard about encapsulation?" or similiar. But don't listen to them. In my case these are irrelevant.

In metawidget to make Your object shown - You have to set it for inspection. Inspectors will come and check what actually is inside Your object and create some metadata. Then there will come a WidgetBuilder which will read the metadata and generate a View - dynamically.

In Android Activity I add

final AndroidMetawidget metawidget = (AndroidMetawidget) findViewById(R.id.sleepmetawidget);
metawidget.setToInspect(sleep);

and the result is:

Not very fantastic. Yes, metawidget needs some tuning, that's sure. Let's see what we can do! The order is other than expected, and date looks quite interesting but totally not useful. We would need a kind of 'picker' for the date. The reason why metawidget shows a EditText view for date is that the Date property is not required, therefore user has to have a way of non-specifying it, and it can be achieved by EditText

We will use metawidget's @UiHidden and @UiRequired annotations first .

public class Sleep2 {

@UiHidden
public int id;

@UiRequired
public Date goSleep;

@UiRequired
@UiComesAfter("goSleep")
public Date wakeUp;
}

which results in


I decided that user doesn't need the id attribute, and set the order of attributes (Maybe we could wish that order of fields methods in class would be the same in source and in class file? This would be good for Java I think, maybe submit it to project Coin?)

The @UiRequired is telling metawidget to show a 'picker' for date object, unfortunatelly it chooses a 'DatePicker' while I would need a 'time' precision for my attribute.
I would like the date should be presented at the top of the widget, and 'Date' attributes should be presented as an hour:minute. Because Metawidget is open for extension, and promotes 'Composition over Inheritance' I had to extend it a little.

1. Create annotation to tell metawidget to treat the date object as 'time'.
 
@Retention( RetentionPolicy.RUNTIME )
@Target( { ElementType.FIELD, ElementType.METHOD } )
public @interface UiAndroidTimeStyle { }

2. Create an Inspector object that will read the annotation and add metawidget information about desired behviour.

public class DateOrTimeInspector extends BaseObjectInspector {

@Override
protected Map<String, String> inspectProperty(Property property) throws Exception {

Map<String,String> attributes = CollectionUtils.newHashMap(1);

if ( property.getType().equals(Date.class)) {
if (property.isAnnotationPresent(UiAndroidTimeStyle.class)) {
attributes.put(InspectionResultConstants.TIME_STYLE,
InspectionResultConstants.TRUE);
}
}
return attributes;
}
}

3. Create an WidgetBuilder object that would actually read the metadata and create 'TimePicker' view.

package org.bartczak.metawidget;

import static org.metawidget.inspector.InspectionResultConstants.REQUIRED;
import static org.metawidget.inspector.InspectionResultConstants.TRUE;

import java.util.Calendar;
import java.util.Date;
import java.util.Map;

import org.metawidget.android.widget.AndroidMetawidget;
import org.metawidget.android.widget.AndroidValueAccessor;
import org.metawidget.inspector.InspectionResultConstants;
import org.metawidget.util.ClassUtils;
import org.metawidget.util.CollectionUtils;
import org.metawidget.util.WidgetBuilderUtils;
import org.metawidget.widgetbuilder.impl.BaseWidgetBuilder;

import android.text.method.DateKeyListener;
import android.view.View;
import android.widget.DatePicker;
import android.widget.EditText;
import android.widget.TimePicker;
import android.widget.TimePicker.OnTimeChangedListener;

public class AndroidTimePickerWidgetBuilder extends BaseWidgetBuilder<View, AndroidMetawidget> implements AndroidValueAccessor {

@Override
protected View buildActiveWidget(
String elementName,
Map<String, String> attributes,
AndroidMetawidget metawidget) throws Exception {

String type = WidgetBuilderUtils.getActualClassOrType( attributes );

Class<?> clazz = ClassUtils.niceForName( type );

if ( Date.class.isAssignableFrom( clazz ) ) {
// Not-nullable dates can use a DatePicker or TimePicker
if ( TRUE.equals( attributes.get( REQUIRED ) ) ) {
if ( InspectionResultConstants.TRUE.equals(
attributes.get(InspectionResultConstants.TIME_STYLE))) {
TimePicker timePicker = new TimePicker(metawidget.getContext() );
timePicker.setIs24HourView(Boolean.TRUE);
timePicker.setOnTimeChangedListener(new OnTimeChangedListener() {

public void onTimeChanged(TimePicker view, int hourOfDay, int minute) {
updateValueForView(view);
}
});
return timePicker;
} else {
return new DatePicker( metawidget.getContext() );
}
}

return newEditTextField(metawidget);
}
return null;

}

private View newEditTextField(AndroidMetawidget metawidget) {
EditText editText = new EditText( metawidget.getContext() );
editText.setKeyListener( new DateKeyListener() );

return editText;
}

protected void updateValueForView(TimePicker view) {
values.put(view,getValueFromView(view));
}

Map<View,Object> values = CollectionUtils.newHashMap();

public Object getValue(View view) {
return values.get(view);
}

private Object getValueFromView(View view) {
if (view instanceof TimePicker) {
TimePicker timePicker = (TimePicker) view;
Calendar date = Calendar.getInstance();
date.set(Calendar.HOUR_OF_DAY, timePicker.getCurrentHour());
date.set(Calendar.MINUTE, timePicker.getCurrentMinute());
return date.getTime();
}
return null;
}

public boolean setValue(Object value, View view) {
if (view instanceof TimePicker) {
if (value instanceof Date) {
Calendar date = Calendar.getInstance();
date.setTime((Date) value);
TimePicker timePicker = (TimePicker) view;
timePicker.setCurrentHour(date.get(Calendar.HOUR_OF_DAY));
timePicker.setCurrentMinute(date.get(Calendar.MINUTE));
updateValueForView(timePicker);
return true;
}
}
return false;
}

}

This class is also able to handle the events on TimePicker so as it's not needed to use the 'Save' button.

4. Configure metawidget to use my object in a 'compose manner' with all previous functionality

<?xml version="1.0"?>
<metawidget xmlns="http://metawidget.org" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://metawidget.org http://metawidget.org/xsd/metawidget-1.0.xsd"
version="1.0">

<androidMetawidget xmlns="java:org.metawidget.android.widget">
<inspector>
<compositeInspector xmlns="java:org.metawidget.inspector.composite"
config="CompositeInspectorConfig">
<inspectors>
<array>
<metawidgetAnnotationInspector
xmlns="java:org.metawidget.inspector.annotation" />
<propertyTypeInspector
xmlns="java:org.metawidget.inspector.propertytype" />
<java5Inspector xmlns="java:org.metawidget.inspector.java5" />
<dateOrTimeInspector xmlns="java:org.bartczak.metawidget" />
</array>
</inspectors>
</compositeInspector>
</inspector>
<widgetBuilder>
<compositeWidgetBuilder xmlns="java:org.metawidget.widgetbuilder.composite"
config="CompositeWidgetBuilderConfig">
<widgetBuilders>
<array>
<androidTimePickerWidgetBuilder
xmlns="java:org.bartczak.metawidget" />
<androidWidgetBuilder
xmlns="java:org.metawidget.android.widget.widgetbuilder" />
</array>
</widgetBuilders>
</compositeWidgetBuilder>
</widgetBuilder>
</androidMetawidget>

</metawidget>

5. Use the metawidget

public class Sleep3 {

@UiHidden
public int id;

/**
* returns the date of sleep in the 'Day.Month' format
* @return
*/
@UiReadOnly
public String getDayOfSleep() {
return TimeUtil.inDayMonthFormat(goSleep);
}

@UiRequired
@UiAndroidTimeStyle
@UiComesAfter("dayOfSleep")
public Date goSleep;

@UiRequired
@UiAndroidTimeStyle
@UiComesAfter("goSleep")
public Date wakeUp;

}

Which results in


That looks quite OK now.

Let's apply some styles now. According to metawidget docs I configure to use my style in metawidget.xml:

<layout>
<tableLayout xmlns="java:org.metawidget.android.widget.layout"
config="LinearLayoutConfig">
<labelStyle><int>@org.bartczak.metawidgetshow:style/MyText</int></labelStyle>
<sectionStyle><int>@org.bartczak.metawidgetshow:style/MyText</int></sectionStyle>
</tableLayout>
</layout>

Where MyText is android style:

<style name="MyText">
<item name="android:textSize">26sp</item>
<item name="android:textColor">#0f0</item>
<item name="android:gravity">right|center_vertical</item>
</style>

and that is looking:


which is sort of what we could expect. Only color attribute has been used. What about others? Here I came into discussion with Richard Kennard on the metawidget forum and it was clear that it's not that easy in Android to apply a style programmatically. You can, however apply style attributes, one by one. Quick dive into metawidgets code into AndroidUtils.applyStyle, and I could see, that color is applied, so as textSize (although textSize isn't working?).
I wanted to make it done, so edited the code of applyStyle:

if ( view instanceof TextView ) {
attributes = metawidget.getContext().obtainStyledAttributes( style, new int[] { R.attr.textColor, R.attr.gravity } );
TextView textView = (TextView) view;

ColorStateList colors = attributes.getColorStateList( 0 );

if ( colors != null )
textView.setTextColor( colors );

int gravity = attributes.getInteger(1, BOGUS_DEFAULT);

if ( gravity!= BOGUS_DEFAULT) {
textView.setGravity(gravity);
}

attributes = metawidget.getContext().obtainStyledAttributes(style, new int[] {android.R.attr.textSize});

float textSize = attributes.getDimensionPixelSize(0, BOGUS_DEFAULT );
if ( textSize != BOGUS_DEFAULT )
textView.setTextSize( textSize );
}

and used my version of the class.

My style attributes are now applied! It looks like 'textSize' attribute has to be obtained in seperate call obtainStyledAttributes()! I don't know why, maybe I should create a small project and submit that bug to google.

Last thing I wanted to do was i18n. Android has support for that, but it is not integrated in metawidget. The default way for metawidget is to specify a label, and first it's trying to look for a key in resources with that name. In Android in runtime resources are identified by int keys in the magic 'R' class, so this would be very ineffective if we would have to translate the label into 'int', lookup the int value by reflection from the 'R' class, and get the real text which is represented by this int key.
But the int keys in 'R' class looks like this:

public final class R {
public static final class attr {
}
public static final class drawable {
public static final int icon=0x7f020000;
}
public static final class id {
public static final int sleepmetawidget=0x7f070000;
}
public static final class layout {
public static final int main=0x7f030000;
}
public static final class raw {
public static final int metawidget=0x7f040000;
}
public static final class string {
public static final int app_name=0x7f050000;
public static final int dayOfSleepLabel=0x7f050006;
public static final int goSleepLabel=0x7f050007;
public static final int wakeUpLabel=0x7f050008;
}
public static final class style {
public static final int MyText=0x7f060000;
}
}

These are constants compile-time, and could be used as annotation parameter. So i have prepared another extension to metawidget!

1. Annotation to specify a key for label:

@Retention( RetentionPolicy.RUNTIME )
@Target( { ElementType.FIELD, ElementType.METHOD } )
public @interface UiAndroidLabelKey {
int value();
}

2. It's usage

public class Sleep4 {

@UiHidden
public int id;

/**
* returns the date of sleep in the 'Day.Month' format
* @return
*/
@UiReadOnly
@UiAndroidLabelKey(R.string.dayOfSleepLabel)
public String getDayOfSleep() {
return TimeUtil.inDayMonthFormat(goSleep);
}

@UiRequired
@UiAndroidTimeStyle
@UiComesAfter("dayOfSleep")
@UiAndroidLabelKey(R.string.goSleepLabel)
public Date goSleep;

@UiRequired
@UiAndroidTimeStyle
@UiComesAfter("goSleep")
@UiAndroidLabelKey(R.string.wakeUpLabel)
public Date wakeUp;

}

3. Inspector to read the annotation

public class AndroidLabelKeyInspector extends BaseObjectInspector {

@Override
protected Map<String, String> inspectProperty(Property property) throws Exception {

Map<String, String> attributes = CollectionUtils.newHashMap(1);

if (property.isAnnotationPresent(UiAndroidLabelKey.class)) {
int annotationValue = property.getAnnotation(UiAndroidLabelKey.class).value();
attributes.put(AndroidMetawidget.LABEL_KEY, "" + annotationValue);
}

return attributes;
}

}

4. configure metawidget to use the inspector

<inspector>
<compositeInspector xmlns="java:org.metawidget.inspector.composite"
config="CompositeInspectorConfig">
<inspectors>
<array>
<metawidgetAnnotationInspector
xmlns="java:org.metawidget.inspector.annotation" />
<propertyTypeInspector
xmlns="java:org.metawidget.inspector.propertytype" />
<java5Inspector xmlns="java:org.metawidget.inspector.java5" />
<dateOrTimeInspector xmlns="java:org.bartczak.metawidget" />
<androidLabelKeyInspector xmlns="java:org.bartczak.metawidget" />
</array>
</inspectors>
</compositeInspector>
</inspector>

5. modification of AndroidMetawidget class
addition in getLabelString() to use new metadata

String labelIntKey = attributes.get( LABEL_KEY );
if ( labelIntKey!= null) {

Integer realKey = Integer.valueOf(labelIntKey);

return getLocalizedLabelByIntKey(realKey);

}

and simple method to obtain label from Android

public String getLocalizedLabelByIntKey( int key ) {
return getContext().getResources().getText(key).toString();
}

My strings looked like this (strings.xml)

<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Metawidget showcase</string>
<string name="dayOfSleepLabel">Dzien</string>
<string name="goSleepLabel">Zasniecie</string>
<string name="wakeUpLabel">Pobudka</string>
</resources>

and application:


Yes! That's what I wanted. Labels in polish :) However if You supply multiple languages, most suitable will be chosen in runtime by Android.

Summary: In this article I wanted to show You that it is not that hard to fix or extend Metawidget. So go on - use it, and when it can't handle something - write it! I hope my contribution will be in some parts accepted by Richard Kennard, metawidget's author.

Using metawidget is fun, with Android - even bigger. I treat it as a kind of training in developing interesting software, and in creating libraries and development tools. Creating a framework for Android is also a good way to understand Android better.

All code written here is licensed on LGPL license, same as Metawidget itself.

niedziela, 31 maja 2009

Jeszcze bardziej kontekstowe hiperłącza!

Czy hiperłącza nie powinny być jeszcze potężniejszym mechanizmem? Weźmy pod uwagę np. kawałek zdania "40% budżetu UE", które w hipertekście może wyglądać tak:


40% budżetu UE


albo tak:


40% budżetu UE


Czyż informacja nie byłaby bardziej pełna, gdyby móc wprowadzić obie metody hiperłączy w jednym hipertekście? Czasem człowiek jest zainteresowany czym jest budżet, czasem tym, czym jest UE, a czasem chce się dowiedzieć o tej kombinacji, czyli budżecie UE.

Zapewne problem był już gdzieś opisywany, ale chyba tylko naukowo (więc posiada trudną do odnalezienia nazwę), bo nic nie słyszałem, by istniała możliwość takiego linkowania w HTMLU. Przydatny dodatek? Przyjmie się?

czwartek, 28 maja 2009

Latarnik wyborczy

Wskazuje dokąd płynąć, gdy zgubiliśmy kurs ;-)

Latarnik wyborczy to serwis internetowy, w którym możemy odpowiedź na serię kluczowych pytań dotyczących poglądów na temat współczesnej europy - a otrzymamy odpowiedź na pytanie do którego komitetu nam najbliżej. Jeśli nie wiesz dokąd popłynąć 7 czerwca - sprawdź tu.

Myślałem kiedyś o stworzeniu serwisu społecznościowego podobnego do latarnika, gdzie każdy mógłby wprowadzić swoje poglądy na najważniejsze sprawy w społeczeństwie a serwis umożliwiałby szukanie osób o podobnych poglądach, dyskusję, a także pokazywałby deklarowane w kampanii oraz realizowane działania różnych osób publicznych.

A potem już tylko krok do demokracji bezpośredniej, gdzie za pośrednictwem bezpiecznego :) serwisu będziemy mogli wypowiedzieć się na te tematy w sposób wiążący.

piątek, 22 maja 2009

Idealna zakładka

Pomysł małego racjonalizatora:

Typowa zakładka do książki ma jeden minus. Po otworzeniu książki na odpowiedniej stronie - nie wiesz, w którym miejscu skończyłeś ostatnio czytać!

Więc opracowałem projekt takiej zakładki, oto on:

Strona frontowa:


Druga strona zakładki powinna być biała

Zakładka ta musi być wykonana w wymiarze niewiele większym od połowy wysokości książki, by przy żadnej konfiguracji nie wystawała zbytnio.

Oczywiście ustawiamy taką zakładkę tak, by jej strona frontowa dotykała czytanej strony, a strzałki pokazywały czytany wiersz.

Czekam na wsparcie społeczności w zakresie wykonania bardziej atrakcyjnego projektu graficznego, puszczenia tego do druku i założenia zakładkowego biznesu ;-P

PS. Nie sprawdzi się przy e-książkach. do nich automatycznie zakładki zapamiętuje np. foxit reader - lekka alternatywa do czytania pdfów.