Categories
Programming

Building Custom TimePicker Dialog Preference in Android

In the spaced repetition app I’m building, I wanted to allow people to pick a time for the study reminder notifications to show up on. There is a built-in AndroidX Preference Library that is meant to help with this, however I found that there’s no built-in way to display a TimePicker dialog as part of the preference option. Some tinkering are still required, and here’s the solution that works for me.

Concept

Before heading to the code, here’s a quick recap of what will be needed. Think of them as the building blocks that have to connect together.

  1. A class that derives from DialogPreference. This part is for managing the Preference aspect of the dialog preference to be displayed, such as its message, buttons, as well as for saving and retrieving the value selected. The library has some built-in dialog preferences (EditTextPreference,  ListPreferenceMultiSelectListPreference), but no such thing as TimePickerPreference, so we’ll have to make ourselves here.
  2. A class that derives from PreferenceDialogFragmentCompat. This part is for managing the visual aspect of the dialog preference to be displayed. This is where we will actually create and display the TimePicker, for instance.
  3. XML layout for the main preference page. This is used to for the visual aspect of the preferences page that’s shown to the user.
  4. Lastly, a fragment that derives from PreferenceFragmentCompat, which essentially is a special type of fragment that’s meant to be used for displaying the preferences page based on the XML layout, and for wrangling various display options related to it.

1. TimePickerPreference

First thing we want to make is the TimePickerPreference. Here’s how my code looks like:

package black.old.spacedrepetitionowl
import android.content.Context
import android.util.AttributeSet
import androidx.preference.DialogPreference
// This class is used in our preference where user can pick a time for notifications to appear.
// Specifically, this class is responsible for saving/retrieving preference data.
class TimepickerPreference(context: Context?, attrs: AttributeSet?) : DialogPreference(context, attrs) {
// Get saved preference value (in minutes from midnight, so 1 AM is represented as 1*60 here
fun getPersistedMinutesFromMidnight(): Int {
return super.getPersistedInt(DEFAULT_MINUTES_FROM_MIDNIGHT)
}
// Save preference
fun persistMinutesFromMidnight(minutesFromMidnight: Int) {
super.persistInt(minutesFromMidnight)
notifyChanged()
}
override fun onSetInitialValue(defaultValue: Any?) {
super.onSetInitialValue(defaultValue)
summary = minutesFromMidnightToHourlyTime(getPersistedMinutesFromMidnight())
}
// Mostly for default values
companion object {
// By default we want notification to appear at 9 AM each time.
private const val DEFAULT_HOUR = 9
const val DEFAULT_MINUTES_FROM_MIDNIGHT = DEFAULT_HOUR * 60
}
}

Above, I override onSetInitialValue() so that I can fill the preference’s summary with the actual time. The idea is to allow people to see the currently selected time, which should look like this:

A preference with the 10:30 value written in the summary area of the preference.

With custom dialog preference like this, I found that the summary area is what’s commonly used to show the current value of that preference.

The two functions persistMinutesFromMidnight() and getPersistedMinutesFromMidnight() are to be used for saving and retrieving the value related to this preference.

2. TimePickerPreferenceDialog

After the preference aspect is created, now we set up the actual dialog. Here’s the code that I have:

package black.old.spacedrepetitionowl
import android.content.Context
import android.os.Bundle
import android.view.View
import android.widget.TimePicker
import androidx.preference.PreferenceDialogFragmentCompat
class TimePickerPreferenceDialog : PreferenceDialogFragmentCompat() {
lateinit var timepicker: TimePicker
override fun onCreateDialogView(context: Context?): View {
timepicker = TimePicker(context)
return timepicker
}
override fun onBindDialogView(view: View?) {
super.onBindDialogView(view)
val minutesAfterMidnight = (preference as TimepickerPreference)
.getPersistedMinutesFromMidnight()
timepicker.setIs24HourView(true)
timepicker.hour = minutesAfterMidnight / 60
timepicker.minute = minutesAfterMidnight % 60
}
override fun onDialogClosed(positiveResult: Boolean) {
// Save settings
if(positiveResult) {
val minutesAfterMidnight = (timepicker.hour * 60) + timepicker.minute
(preference as TimepickerPreference).persistMinutesFromMidnight(minutesAfterMidnight)
preference.summary = minutesFromMidnightToHourlyTime(minutesAfterMidnight)
}
}
companion object {
fun newInstance(key: String): TimePickerPreferenceDialog {
val fragment = TimePickerPreferenceDialog()
val bundle = Bundle(1)
bundle.putString(ARG_KEY, key)
fragment.arguments = bundle
return fragment
}
}
}

Inside onCreateDialogView(), we generate the TimePicker to be displayed.

Inside onBindDialogView(), we grab the current hour/minute preference value, and set that value into the TimePicker. The goal is to make it so that when someone first opens the TimePicker dialog, the clock already shows the current value instead of starting from zero. (preference as TimepickerPreference) is needed here because without it, the compiler does not now that the current preference is a custom TimePickerPreference, and hence will not be able to find the function we need.

Inside onDialogClosed(), we save the value selected by the user after clicking the positive button.

For more information about the NewInstance pattern, this article will be helpful.

3. XML Layout

Here’s my preference XML file:

<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<black.old.spacedrepetitionowl.TimepickerPreference
android:key="pref_notification_time"
android:title="Notification time"
android:summary="This will programmatically show current notification time."
android:defaultValue="90" />
</androidx.preference.PreferenceScreen>
view raw preference.xml hosted with ❤ by GitHub

The interesting part might be <black.old.spacedrepetitionowl.TimepickerPreference> which indeed is the class we created on step 1. During run-time, the android:summary part is programmatically modified and filled in both within TimePickerPreference (on initial opening of the preference screen), and TimePickerDialogPreference (after user picked a time).

4. PreferenceFragment

This is the regular fragment that we’ll use to display the preference screen. Here’s my file:

package black.old.spacedrepetitionowl
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
/**
* A simple [Fragment] subclass.
*/
class PreferenceFragment : PreferenceFragmentCompat() {
private val DIALOG_FRAGMENT_TAG = "TimePickerDialog"
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.preference, rootKey)
}
override fun onDisplayPreferenceDialog(preference: Preference?) {
if(preference is TimepickerPreference) {
val timepickerdialog = TimePickerPreferenceDialog.newInstance(preference.key)
timepickerdialog.setTargetFragment(this, 0)
timepickerdialog.show(parentFragmentManager, DIALOG_FRAGMENT_TAG)
}
else {
super.onDisplayPreferenceDialog(preference)
}
}
}

First, we’re pulling the XML layout inside onCreatePreferences().

Next, since we want to display our own custom dialog preference, we need to override onDisplayPreferenceDialog(). Here, we specifically want to check if the currently selected preference is our custom preference, TimePickerPreference. If it is, we instantiate a new TimePickerPreferenceDialog and display it. If it’s a different type of dialog preference (let’s say it’s one of the three built-in dialog preferences mentioned earlier), we let it be handled automatically by the superclass.

And that’s it! This is just a small example but hopefully is a good starting point if you want to create a different kind of custom dialog preference 🙂

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s