DatePickerDialog Holo styling failed on Android 7 Nougat(DatePickerDialog Holo 样式在 Android 7 Nougat 上失败)
问题描述
根据我们的客户需求,我们希望在所有 Android 操作系统版本的 DatePickerDialog 上保留 HOLO 样式,例如:Android 7 上的日期选择器-
From our customer demand we want to keep the HOLO style on the DatePickerDialog for all Android OS version, sth like: DatePicker on Android 7-
但它似乎无法在 Android 7 上正常工作:
But it seems to not to work correctly on Android 7:
Android 7 上的日期选择器
来自我的实现:
new DatePickerDialog(getContext(), AlertDialog.THEME_HOLO_LIGHT,
mCalendarPickerListener,
calendar.get(Calendar.YEAR),
calendar.get(Calendar.MONTH),
calendar.get(Calendar.DAY_OF_MONTH));
在之前的 android 7 上运行良好.有人有同样的问题吗?
It's working fine for prior android 7. Anybody has the same issue?
已发现解决方案已在 API 25 中修复https://code.google.com/u/106133255289400340786/
Edited: solution is found to be fixed in API 25 https://code.google.com/u/106133255289400340786/
推荐答案
我使用 DatePickerDialog
来提示用户生日.不幸的是,我在尝试使用 Material 主题对话框时收到了很多用户投诉,因此切换到它不是我的选择:我必须坚持使用 Holo 主题对话框.
I use a DatePickerDialog
to prompt users for their birthdays. Unfortunately, I've received a number of complaints from users about the Material-themed dialog when trying it, so switching to it is not an option for me: I have to stick to the Holo-themed dialog.
事实证明,Android 7.0 附带了一个错误:尝试在此平台上使用 Holo 主题而不是为 DatePickerDialog
使用 broken Material 主题.请参阅以下两个错误报告:
It turns out that Android 7.0 shipped with a bug: trying to use the Holo theme on this platform instead falls back to using a broken Material theme for the DatePickerDialog
. See these two bug reports:
- 问题 222808
- 问题 222208
我使用了 Jeff Lockhart 提出的这个解决方法 的修改形式,在这些错误报告中引用:
I used a modified form of this workaround by Jeff Lockhart referenced in those bug reports:
private static final class FixedHoloDatePickerDialog extends DatePickerDialog {
private FixedHoloDatePickerDialog(Context context, OnDateSetListener callBack,
int year, int monthOfYear, int dayOfMonth) {
super(context, callBack, year, monthOfYear, dayOfMonth);
// Force spinners on Android 7.0 only (SDK 24).
// Note: I'm using a naked SDK value of 24 here, because I'm
// targeting SDK 23, and Build.VERSION_CODES.N is not available yet.
// But if you target SDK >= 24, you should have it.
if (Build.VERSION.SDK_INT == 24) {
try {
final Field field = this.findField(
DatePickerDialog.class,
DatePicker.class,
"mDatePicker"
);
final DatePicker datePicker = (DatePicker) field.get(this);
final Class<?> delegateClass = Class.forName(
"android.widget.DatePicker$DatePickerDelegate"
);
final Field delegateField = this.findField(
DatePicker.class,
delegateClass,
"mDelegate"
);
final Object delegate = delegateField.get(datePicker);
final Class<?> spinnerDelegateClass = Class.forName(
"android.widget.DatePickerSpinnerDelegate"
);
if (delegate.getClass() != spinnerDelegateClass) {
delegateField.set(datePicker, null);
datePicker.removeAllViews();
final Constructor spinnerDelegateConstructor =
spinnerDelegateClass.getDeclaredConstructor(
DatePicker.class,
Context.class,
AttributeSet.class,
int.class,
int.class
);
spinnerDelegateConstructor.setAccessible(true);
final Object spinnerDelegate = spinnerDelegateConstructor.newInstance(
datePicker,
context,
null,
android.R.attr.datePickerStyle,
0
);
delegateField.set(datePicker, spinnerDelegate);
datePicker.init(year, monthOfYear, dayOfMonth, this);
datePicker.setCalendarViewShown(false);
datePicker.setSpinnersShown(true);
}
} catch (Exception e) { /* Do nothing */ }
}
}
/**
* Find Field with expectedName in objectClass. If not found, find first occurrence of
* target fieldClass in objectClass.
*/
private Field findField(Class objectClass, Class fieldClass, String expectedName) {
try {
final Field field = objectClass.getDeclaredField(expectedName);
field.setAccessible(true);
return field;
} catch (NoSuchFieldException e) { /* Ignore */ }
// Search for it if it wasn't found under the expectedName.
for (final Field field : objectClass.getDeclaredFields()) {
if (field.getType() == fieldClass) {
field.setAccessible(true);
return field;
}
}
return null;
}
}
这是做什么的:
- 获取属于此对话框的私有
DatePicker mDatePicker
字段 - 获取属于此对话框的私有
DatePickerDelegate mDelegate
字段 - 检查委托是否已经是
DatePickerSpinnerDelegate
的实例(我们想要的委托类型) - 从
DatePicker
中移除所有视图,因为它们是 Material 日历小部件 - 创建一个新的
DatePickerSpinnerDelegate
实例,并将其分配给该对话框的mDatePicker
的mDelegate
字段 - 使用日历信息和一些参数重新初始化
mDatePicker
以使其为微调器充气
- Get the private
DatePicker mDatePicker
field belonging to this dialog - Get the private
DatePickerDelegate mDelegate
field belonging to this dialog - Check that the delegate is not already an instance of
DatePickerSpinnerDelegate
(the type of delegate we want) - Remove all views from the
DatePicker
, since they are the Material calendar widgets - Create a new instance of
DatePickerSpinnerDelegate
, and assign it to themDelegate
field ofmDatePicker
of this dialog - Re-initialize
mDatePicker
with calendar info and some params to get it to inflate the spinners
为了使用这个解决方法,我在我的 Context
周围创建了一个 ContextThemeWrapper
,它允许我设置一个主题,在本例中是 Holo:
To use this workaround, I create a ContextThemeWrapper
around my Context
, which allows me to set a theme, in this case Holo:
final Context themedContext = new ContextThemeWrapper(
this.getContext(),
android.R.style.Theme_Holo_Light_Dialog
);
final DatePickerDialog dialog = new FixedHoloDatePickerDialog(
themedContext,
datePickerListener,
calender.get(Calendar.YEAR),
calendar.get(Calendar.MONTH),
calendar.get(Calendar.DAY_OF_MONTH)
);
<小时>
注意事项:
- 这使用反射来访问私有字段.通常,这不是一种稳健的方法,您不能指望它.我通过 1) 将其限制为单个 SDK 版本 v24 来降低风险;和 2) 将整个反射代码位包装在
try {...} catch (Exception e) {/* NOP *