Android Time Picker Preference

When developing applications for Google Android, there's a great way to implement your preferences using an xml, much like you would for a regular layout. The added benifit of doing it this way is that you don't have to implement how the preference values are saved. However, one of the problems is that there are only a couple of preference types implemented.

For my latest application, I needed to let the user save a specific time. Where there is a TimePicker widget, there is not TimePickerPreference that extends the Preference class. So I wrote my own.

// Please note this must be the package if you want to use XML-based preferences
package android.preference;
 
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.widget.TimePicker;
 
/**
 * A preference type that allows a user to choose a time
 */
public class TimePickerPreference extends DialogPreference implements
		TimePicker.OnTimeChangedListener {
 
	/**
	 * The validation expression for this preference
	 */
	private static final String VALIDATION_EXPRESSION = "[0-2]*[0-9]:[0-5]*[0-9]";
 
	/**
	 * The default value for this preference
	 */
	private String defaultValue;
 
	/**
	 * @param context
	 * @param attrs
	 */
	public TimePickerPreference(Context context, AttributeSet attrs) {
		super(context, attrs);
		initialize();
	}
 
	/**
	 * @param context
	 * @param attrs
	 * @param defStyle
	 */
	public TimePickerPreference(Context context, AttributeSet attrs,
			int defStyle) {
		super(context, attrs, defStyle);
		initialize();
	}
 
	/**
	 * Initialize this preference
	 */
	private void initialize() {
		setPersistent(true);
	}
 
	/*
	 * (non-Javadoc)
	 * 
	 * @see android.preference.DialogPreference#onCreateDialogView()
	 */
	@Override
	protected View onCreateDialogView() {
 
		TimePicker tp = new TimePicker(getContext());
		tp.setOnTimeChangedListener(this);
 
		int h = getHour();
		int m = getMinute();
		if (h >= 0 && m >= 0) {
			tp.setCurrentHour(h);
			tp.setCurrentMinute(m);
		}
 
		return tp;
	}
 
	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * android.widget.TimePicker.OnTimeChangedListener#onTimeChanged(android
	 * .widget.TimePicker, int, int)
	 */
	@Override
	public void onTimeChanged(TimePicker view, int hour, int minute) {
 
		persistString(hour + ":" + minute);
	}
 
	/*
	 * (non-Javadoc)
	 * 
	 * @see android.preference.Preference#setDefaultValue(java.lang.Object)
	 */
	@Override
	public void setDefaultValue(Object defaultValue) {
		// BUG this method is never called if you use the 'android:defaultValue' attribute in your XML preference file, not sure why it isn't		
 
		super.setDefaultValue(defaultValue);
 
		if (!(defaultValue instanceof String)) {
			return;
		}
 
		if (!((String) defaultValue).matches(VALIDATION_EXPRESSION)) {
			return;
		}
 
		this.defaultValue = (String) defaultValue;
	}
 
	/**
	 * Get the hour value (in 24 hour time)
	 * 
	 * @return The hour value, will be 0 to 23 (inclusive)
	 */
	private int getHour() {
		String time = getPersistedString(this.defaultValue);
		if (time == null || !time.matches(VALIDATION_EXPRESSION)) {
			return -1;
		}
 
		return Integer.valueOf(time.split(":")[0]);
	}
 
	/**
	 * Get the minute value
	 * 
	 * @return the minute value, will be 0 to 59 (inclusive)
	 */
	private int getMinute() {
		String time = getPersistedString(this.defaultValue);
		if (time == null || !time.matches(VALIDATION_EXPRESSION)) {
			return -1;
		}
 
		return Integer.valueOf(time.split(":")[1]);
	}
}

"Bug" on default value

Hi !

Since your snippet helped me, I want to return the favour by giving you the way to use the default value. Actually, it's quite simple : you just need to override the onGetDefaultValue method =) That's all !

This method is in charge of retrieving the default value according to its correct type. In example, if your default value is a String you may have :

	@Override
	protected Object onGetDefaultValue(TypedArray a, int index) {
		final String value = a.getString(index);
 
		if (value == null || !VALIDATION_EXPRESSION.matcher(value).matches()) {
			return null;
		}
 
		this.defaultValue = value;
		return value;
	}

Thus, the Override on setDefaultValue is not needed anymore.

By the way, I made some improvements on your class. Basically, those were needed because I wanted to have some negative time, but I also made some performance enhancements :
- I used a Pattern to match the regular expression because I needed to extract more than two parts from the time and that was the most efficient way according to me
- more importantly I removed the getHour and getMinute method. These two were private (thus only used in this class) and each accessed the stored preferences and then splitted the String. I prefer reading the prefs and spliting the saved String once and for all.

Here is my version :

@Override
	protected View onCreateDialogView() {
 
		final Context context = getContext();
 
		final LayoutInflater vi = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
		final View v = vi.inflate(R.layout.timepickerpreference, null);
 
		// Initialisation du bouton
		sign = (Button)v.findViewById(R.id.btn_sign);
		sign.setOnClickListener(this);
 
		// Initialisation du picker
		picker = (TimePicker)v.findViewById(R.id.pck_time);
		picker.setIs24HourView(DateFormat.is24HourFormat(context));
 
		// Initialisation du contenu des composants en fonction de la valeur de préférence actuelle.
		final String time = getPersistedString(this.defaultValue);
		if (time != null) {
			final Matcher matcher = VALIDATION_EXPRESSION.matcher(time);
			if (matcher.matches()) {
				// Initialisation du bouton
				final String sign = matcher.group(1);
				if (sign == null) {
					// Pas de signe : on considère qu'on a un nombre positif
					isNegativeTime = false;
				} else {
					// Un signe est spécifié : on regarde si c'est un -.
					// Si non, on considère qu'on a un nombre positif.
					isNegativeTime = NEGATIVE_SIGN.equals(sign);
				}
				updateSignButtonText();
 
				// Initialisation du picker
				final int hour = Integer.valueOf(matcher.group(2));
				final int minute = Integer.valueOf(matcher.group(3));
				if (hour >= 0 && minute >= 0) {
					picker.setCurrentHour(hour);
					picker.setCurrentMinute(minute);
				}
			}
		}
 
		return v;
	}

Hope this helps !

GetText()

I just did the getText() function (and used it to set the summary of the preference)

public String getText(){
int tmpHour = getHour();
int tmpMinute = getMinute();
if (DateFormat.is24HourFormat(getContext())){
return tmpHour + ":" + (tmpMinute<10?("0" + tmpMinute):tmpMinute);
}
else return tmpHour%12 + ":" + (tmpMinute<10?("0" + tmpMinute):tmpMinute) + (tmpHour>12?" PM":" AM");
}

Some modifications

Hi,

I have changed some of this code. Remove the OnTimeChangedListener and add:

private TimePicker tp; //to get this dialog from other functions

@Override
protected void onDialogClosed (boolean positiveResult){
if (positiveResult) {
tp.clearFocus();
int hour = tp.getCurrentHour();
int minute = tp.getCurrentMinute();
String result = hour + ":" + minute;
persistString(result);
callChangeListener(result);
}
}

It's working for me, and is working faster, also gets value as inserted by hand and drop them when you press Cancel.

Regards,
eM

For save a keyboard change

	@Override
	public void onDialogClosed(boolean positiveResult) {
		String result = mHour + ":" + mMinute;
		if( positiveResult ) {
			if( isPersistent() )
				persistString(result);
			callChangeListener(result);
		}
	}

Error

It doesn't save the user preference, if the user types the numbers in the box themselves ( instead of using the + / - )

For save a keyboard change

@Override
public void onClick(DialogInterface dialog, int which) {
super.getDialog().getCurrentFocus().clearFocus();
super.onClick(dialog, which);
}

Feel free to suggest updates

If you find out what event handler is called when a user manually enters a value, let me know so I can update the code here.

Thanks,
Eric

Thanks mate excellent

Thanks mate excellent implementation of a time picker

package doesn't need to be android.preference

hi, great work!
saved me a lot of time.

I just wanna say that you can change the package these preferences lie in,
if you specify entire package in XML-file:

Kris

Problem again again

Ok, I found that if I call tp.setIs24HourView(true); before tp.setOnTimeChangedListener(this); it works fine. Thanks for this great class!

Problem again

Never mind my previous post! The problem was that I added tp.setIs24HourView(true); to the onCreateDialogView method, since I'm from Europe and we use a 24 hour clock. ;)

However, that seems to mess it up. :( How can I set it to use a 24 hour clock by default?

for 12/24 time mode (system settings)

tp.setIs24HourView(DateFormat.is24HourFormat(tp.getContext()));

Problem

I've been eager to use this, but I haven't been able to get it working.

When I set a preference using this class, it gets set properly in the shared prefs xml file. However, whenever I open the preference again, the time picker is set to the current time. It seems that the onTimeChanged method is called immediately after the time picker is created, with its parameters set to the current time.

Did anyone else experience this? I don't know how to solve it ... :(

Cancel behavior

I might be missing something but this seems to always persist the value even if you cancel the TimePicker. I changed onTimeChanged to store to a local variable and then did a @Override on onDialogClosed to get the state of the dialog.

If the user said 'ok' then persist the string (In my implementation I didn't make it persist by default, hence the 'isPersistent()'). If the user canceled then I change nothing

	private int mHour = 0, mMinute = 0;
	@Override
	public void onTimeChanged(TimePicker view, int hour, int minute) {
		mHour = hour;
		mMinute = minute;
	}
	@Override
	public void onDialogClosed(boolean positiveResult) {
		if( positiveResult ) {
			if( isPersistent() )
				persistString(mHour + ":" + mMinute);
			mValueText.setText( getNiceTimestring() );
		}
	}

Update for latest Android release

From an email:

I had to modify your TimePickerPreference code a little as it did not respond to an OnPreferenceChangeListener. The change made is just this:

String result = hour + &quot;:&quot; + minute;
persistString(result);
callChangeListener(result);

DatePicker

I've been trying to implement a similar pref for DatePicker ... however I'm having a few problems. Had any luck with that?

RE: DatePicker

I haven't need to use a Date preference value, so I haven't created this kind of class for DatePicker.  If you're having problems, then I suggest posting to the Android Developers group.

Thanks

Thanks for saving me the work. Using this in my Android project now :)

Can we use your code ?

What's the license of this code ?

No license

There is no license, you can use however you want to.

layout

is there another layout or the normal if u create a new file????

This is a class, so it should

This is a class, so it should be a new file.  I'm not exactly sure what you're asking.