Forked from jeffdgr8/TimePickerDialogFixedNougatSpinner.java
Last active
March 26, 2022 11:08
-
-
Save uqmessias/d9a8dc624af935f344dfa2e8928490ec to your computer and use it in GitHub Desktop.
TimePickerDialog with fixed android:timePickerMode spinner in Nougat
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package my.packagename; | |
import android.app.DatePickerDialog; | |
import android.content.Context; | |
import android.content.res.TypedArray; | |
import android.os.Build; | |
import android.util.AttributeSet; | |
import android.widget.DatePicker; | |
import java.lang.reflect.Field; | |
import java.lang.reflect.Method; | |
/** | |
* I got it from https://gist.github.com/jeffdgr8/6bc5f990bf0c13a7334ce385d482af9f and | |
* did some adjustments in order to work with `DatePicker` | |
* Workaround for this bug: https://code.google.com/p/android/issues/detail?id=222208 | |
* In Android 7.0 Nougat, spinner mode for the DatePicker in DatePickerDialog is | |
* incorrectly displayed as calendar, even when the theme specifies otherwise, such as: | |
* <p> | |
* <resources> | |
* <style name="Theme.MyApp" parent="Theme.AppCompat.Light.NoActionBar"> | |
* <item name="android:datePickerStyle">@style/Widget.MyApp.DatePicker</item> | |
* </style> | |
* <p> | |
* <style name="Widget.MyApp.DatePicker" parent="android:Widget.Material.DatePicker"> | |
* <item name="android:datePickerMode">spinner</item> | |
* </style> | |
* </resources> | |
* <p> | |
* May also pass DatePickerDialog.THEME_HOLO_LIGHT as an argument to the constructor, | |
* as this theme has the DatePickerMode set to spinner. | |
*/ | |
public class DatePickerDialogFixedNougatSpinner extends DatePickerDialog { | |
public DatePickerDialogFixedNougatSpinner(Context context, OnDateSetListener listener, int year, int month, int dayOfMonth) { | |
super(context, listener, year, month, dayOfMonth); | |
fixSpinner(context, year, month, dayOfMonth); | |
} | |
public DatePickerDialogFixedNougatSpinner(Context context, int themeResId, OnDateSetListener listener, int year, int month, int dayOfMonth) { | |
super(context, themeResId, listener, year, month, dayOfMonth); | |
fixSpinner(context, year, month, dayOfMonth); | |
} | |
private void fixSpinner(Context context, int year, int month, int dayOfMonth) { | |
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { | |
try { | |
// Get the theme's android:datePickerMode | |
final int MODE_SPINNER = 1; | |
Class<?> styleableClass = Class.forName("com.android.internal.R$styleable"); | |
Field datePickerStyleableField = styleableClass.getField("DatePicker"); | |
int[] datePickerStyleable = (int[]) datePickerStyleableField.get(null); | |
final TypedArray a = context.obtainStyledAttributes(null, datePickerStyleable, android.R.attr.datePickerStyle, 0); | |
Field datePickerModeStyleableField = styleableClass.getField("DatePicker_datePickerMode"); | |
int datePickerModeStyleable = datePickerModeStyleableField.getInt(null); | |
final int mode = a.getInt(datePickerModeStyleable, MODE_SPINNER); | |
a.recycle(); | |
if (mode == MODE_SPINNER) { | |
DatePicker datePicker = (DatePicker) findField(DatePickerDialog.class, DatePicker.class, "mDatePicker").get(this); | |
Class<?> delegateClass = Class.forName("android.widget.DatePicker$DatePickerDelegate"); | |
Field delegateField = findField(DatePicker.class, delegateClass, "mDelegate"); | |
Object delegate = delegateField.get(datePicker); | |
Class<?> spinnerDelegateClass; | |
spinnerDelegateClass = Class.forName("android.widget.DatePickerSpinnerDelegate"); | |
// In 7.0 Nougat for some reason the datePickerMode is ignored and the delegate is DatePickerClockDelegate | |
if (delegate.getClass() != spinnerDelegateClass) { | |
delegateField.set(datePicker, null); // throw out the DatePickerClockDelegate! | |
datePicker.removeAllViews(); // remove the DatePickerClockDelegate views | |
Method createSpinnerUIDelegate = DatePicker.class.getDeclaredMethod("createSpinnerUIDelegate", Context.class, AttributeSet.class, int.class, int.class); | |
createSpinnerUIDelegate.setAccessible(true); | |
// Instantiate a DatePickerSpinnerDelegate throughout createSpinnerUIDelegate method | |
delegate = createSpinnerUIDelegate.invoke(datePicker, context, null, android.R.attr.datePickerStyle, 0); | |
delegateField.set(datePicker, delegate); // set the DatePicker.mDelegate to the spinner delegate | |
// Initialize the date for the DatePicker delegate again | |
datePicker.init(year, month, dayOfMonth, this); | |
} | |
} | |
} catch (Exception e) { | |
throw new RuntimeException(e); | |
} | |
} | |
} | |
private static Field findField(Class objectClass, Class fieldClass, String expectedName) { | |
try { | |
Field field = objectClass.getDeclaredField(expectedName); | |
field.setAccessible(true); | |
return field; | |
} catch (NoSuchFieldException e) { | |
} // ignore | |
// search for it if it wasn't found under the expected ivar name | |
for (Field searchField : objectClass.getDeclaredFields()) { | |
if (searchField.getType() == fieldClass) { | |
searchField.setAccessible(true); | |
return searchField; | |
} | |
} | |
return null; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package my.packagename; | |
import android.app.TimePickerDialog; | |
import android.content.Context; | |
import android.content.res.TypedArray; | |
import android.os.Build; | |
import android.util.AttributeSet; | |
import android.widget.TimePicker; | |
import java.lang.reflect.Constructor; | |
import java.lang.reflect.Field; | |
/** | |
* Workaround for this bug: https://code.google.com/p/android/issues/detail?id=222208 | |
* In Android 7.0 Nougat, spinner mode for the TimePicker in TimePickerDialog is | |
* incorrectly displayed as clock, even when the theme specifies otherwise, such as: | |
* | |
* <resources> | |
* <style name="Theme.MyApp" parent="Theme.AppCompat.Light.NoActionBar"> | |
* <item name="android:timePickerStyle">@style/Widget.MyApp.TimePicker</item> | |
* </style> | |
* | |
* <style name="Widget.MyApp.TimePicker" parent="android:Widget.Material.TimePicker"> | |
* <item name="android:timePickerMode">spinner</item> | |
* </style> | |
* </resources> | |
* | |
* May also pass TimePickerDialog.THEME_HOLO_LIGHT as an argument to the constructor, | |
* as this theme has the TimePickerMode set to spinner. | |
*/ | |
public class TimePickerDialogFixedNougatSpinner extends TimePickerDialog { | |
/** | |
* Creates a new time picker dialog. | |
* | |
* @param context the parent context | |
* @param listener the listener to call when the time is set | |
* @param hourOfDay the initial hour | |
* @param minute the initial minute | |
* @param is24HourView whether this is a 24 hour view or AM/PM | |
*/ | |
public TimePickerDialogFixedNougatSpinner(Context context, OnTimeSetListener listener, int hourOfDay, int minute, boolean is24HourView) { | |
super(context, listener, hourOfDay, minute, is24HourView); | |
fixSpinner(context, hourOfDay, minute, is24HourView); | |
} | |
/** | |
* Creates a new time picker dialog with the specified theme. | |
* | |
* @param context the parent context | |
* @param themeResId the resource ID of the theme to apply to this dialog | |
* @param listener the listener to call when the time is set | |
* @param hourOfDay the initial hour | |
* @param minute the initial minute | |
* @param is24HourView Whether this is a 24 hour view, or AM/PM. | |
*/ | |
public TimePickerDialogFixedNougatSpinner(Context context, int themeResId, OnTimeSetListener listener, int hourOfDay, int minute, boolean is24HourView) { | |
super(context, themeResId, listener, hourOfDay, minute, is24HourView); | |
fixSpinner(context, hourOfDay, minute, is24HourView); | |
} | |
private void fixSpinner(Context context, int hourOfDay, int minute, boolean is24HourView) { | |
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { // android:timePickerMode spinner and clock began in Lollipop | |
try { | |
// Get the theme's android:timePickerMode | |
final int MODE_SPINNER = 1; | |
Class<?> styleableClass = Class.forName("com.android.internal.R$styleable"); | |
Field timePickerStyleableField = styleableClass.getField("TimePicker"); | |
int[] timePickerStyleable = (int[]) timePickerStyleableField.get(null); | |
final TypedArray a = context.obtainStyledAttributes(null, timePickerStyleable, android.R.attr.timePickerStyle, 0); | |
Field timePickerModeStyleableField = styleableClass.getField("TimePicker_timePickerMode"); | |
int timePickerModeStyleable = timePickerModeStyleableField.getInt(null); | |
final int mode = a.getInt(timePickerModeStyleable, MODE_SPINNER); | |
a.recycle(); | |
if (mode == MODE_SPINNER) { | |
TimePicker timePicker = (TimePicker) findField(TimePickerDialog.class, TimePicker.class, "mTimePicker").get(this); | |
Class<?> delegateClass = Class.forName("android.widget.TimePicker$TimePickerDelegate"); | |
Field delegateField = findField(TimePicker.class, delegateClass, "mDelegate"); | |
Object delegate = delegateField.get(timePicker); | |
Class<?> spinnerDelegateClass; | |
if (Build.VERSION.SDK_INT != Build.VERSION_CODES.LOLLIPOP) { | |
spinnerDelegateClass = Class.forName("android.widget.TimePickerSpinnerDelegate"); | |
} else { | |
// TimePickerSpinnerDelegate was initially misnamed TimePickerClockDelegate in API 21! | |
spinnerDelegateClass = Class.forName("android.widget.TimePickerClockDelegate"); | |
} | |
// In 7.0 Nougat for some reason the timePickerMode is ignored and the delegate is TimePickerClockDelegate | |
if (delegate.getClass() != spinnerDelegateClass) { | |
delegateField.set(timePicker, null); // throw out the TimePickerClockDelegate! | |
timePicker.removeAllViews(); // remove the TimePickerClockDelegate views | |
Constructor spinnerDelegateConstructor = spinnerDelegateClass.getConstructor(TimePicker.class, Context.class, AttributeSet.class, int.class, int.class); | |
spinnerDelegateConstructor.setAccessible(true); | |
// Instantiate a TimePickerSpinnerDelegate | |
delegate = spinnerDelegateConstructor.newInstance(timePicker, context, null, android.R.attr.timePickerStyle, 0); | |
delegateField.set(timePicker, delegate); // set the TimePicker.mDelegate to the spinner delegate | |
// Set up the TimePicker again, with the TimePickerSpinnerDelegate | |
timePicker.setIs24HourView(is24HourView); | |
timePicker.setCurrentHour(hourOfDay); | |
timePicker.setCurrentMinute(minute); | |
timePicker.setOnTimeChangedListener(this); | |
} | |
} | |
} catch (Exception e) { | |
throw new RuntimeException(e); | |
} | |
} | |
} | |
private static Field findField(Class objectClass, Class fieldClass, String expectedName) { | |
try { | |
Field field = objectClass.getDeclaredField(expectedName); | |
field.setAccessible(true); | |
return field; | |
} catch (NoSuchFieldException e) {} // ignore | |
// search for it if it wasn't found under the expected ivar name | |
for (Field searchField : objectClass.getDeclaredFields()) { | |
if (searchField.getType() == fieldClass) { | |
searchField.setAccessible(true); | |
return searchField; | |
} | |
} | |
return null; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment