DatePickerDialog Holo 样式在 Android 7 Nougat 上失败

2022-01-11 00:00:00 calendar android java

根据我们的客户需求,我们希望在所有 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实例,并将其分配给该对话框的mDatePickermDelegate字段
  • 使用日历信息和一些参数重新初始化 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 the mDelegate field of mDatePicker 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 */} 块中,因此如果任何反射失败,则不会发生任何事情并且将使用(可悲的是损坏的)默认材质后备.
  • 上述错误报告称,此问题已在 Android 7.1 (SDK 25) 中得到修复.我没有对此进行测试.
  • 原始解决方法代码是针对 TimePickerDialog 的类似的问题.我已将其修改为使用 DatePickerDialog 代替,并且还将解决方案简化为不那么通用,并且更具体地针对我的确切用例.但是,您可以使用更完整的原始版本,只需将其调整为 Date 而不是 Time.
  • This uses reflection to access private fields. Generally, this is not a robust approach and you can't count on it. I'm mitigating the risk here by 1) restricting this to a single SDK version, v24; and 2) wrapping the entire bit of reflection code in a try {...} catch (Exception e) {/* NOP */} block, so if any of the reflection fails, nothing will happen and the (sadly broken) default Material fallback will be used.
  • The bug reports above claim that this issue has been fixed in Android 7.1 (SDK 25). I have not tested this.
  • The original workaround code was for TimePickerDialog that suffered from a similar problem. I've modified it to work with DatePickerDialog instead, and also simplified the solution to be less generic, and more specific to my exact use case. However, you could use the more complete original version and just tweak it for Date instead of Time.

相关文章