Fix DatePickerDialog Theme Holo on Android 7.0 Nougat (API 24)

1. Giới thiệu:

Ở bài trước, mình đã trình bày về Date/TimePickerDialog cũng như cách custom nó. Tuy nhiên Theme Holo không hoạt động trên Android 7.0 Nougat (API 24), đương nhiên dẫn đến việc không thể custom được Theme Holo. Đây là bug trên Android 7.0 và đã được fix trên Android 7.1 Đây là hình ảnh Theme Holo trên Android 7.0 (Giống với THEME_DEVICE_DEFAULT) và trên Android khác Trong bài này, mình sẽ hướng dẫn các bạn cách fix Theme Holo trong trường hợp sử dụng máy 7.0

2. Code:

Dưới đây là phần code custom DatePicker Theme Holo của bài trước, bạn có thể xem đầy đủ code bài trước tại đây

 @Override
    public TimeManagement dialogDatePicker(DatePickerDialog.OnDateSetListener onDateSetListener) {
        mDatePickerDialog =
                new DatePickerDialog(mContext, onDateSetListener, mCalendar.get(Calendar.YEAR),
                        mCalendar.get(Calendar.MONTH), mCalendar.get(Calendar.DAY_OF_MONTH));
                       
                       try {
            Field[] datePickerDialogFields = mDatePickerDialog.getClass().getDeclaredFields();
            for (Field datePickerDialogField : datePickerDialogFields) {
                if (datePickerDialogField.getName().equals("mDatePicker")) {
                    datePickerDialogField.setAccessible(true);
                    DatePicker datePicker =
                            (DatePicker) datePickerDialogField.get(mDatePickerDialog);
                    if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                       customDatePicker(mDatePicker);
                    }
                }
            }
        } catch (IllegalAccessException e) {
            Log.e(TAG, "IllegalAccessException: ", e);
        }
        return this;
    }
    
    private void customDatePicker(DatePicker datePicker) {
        int daySpinnerId = Resources.getSystem().getIdentifier(DAY_FIELD, ID, ANDROID);
        if (daySpinnerId != 0) {
            View daySpinner = datePicker.findViewById(daySpinnerId);
            if (daySpinner != null) {
                daySpinner.setVisibility(View.GONE);
            }
        }
    }
    

Đoạn code trên hoạt động ổn với các phiên bản Android khác, ngoại trừ 7.0 Nougat, nó sẽ ko hiển thị THEME HOLO như hình ảnh mình cung cấp ở đầu bài, mà thay vào đó là THEME_DEVICE_DEFAULT, để hoạt động được trên 7.0, chúng ta cần bổ sung 1 đoạn code nhỏ sau đây, Cũng tại Class TimeManagementImpl, tạo 1 inner class có tên là FixedHoloDatePickerDialog

private final class FixedHoloDatePickerDialog extends DatePickerDialog {
        private FixedHoloDatePickerDialog(Context context, OnDateSetListener callBack, int year,
                int monthOfYear, int dayOfMonth)
                throws ClassNotFoundException, IllegalAccessException, NoSuchMethodException,
                InvocationTargetException, InstantiationException {
            super(context, callBack, year, monthOfYear, dayOfMonth);

            final Field field =
                    this.findField(DatePickerDialog.class, DatePicker.class, "mDatePicker");
            assert field != null;
            try {
                mDatePicker = (DatePicker) field.get(this);
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
            final Class<?> delegateClass =
                    Class.forName("android.widget.DatePicker$DatePickerDelegate");
            final Field delegateField =
                    this.findField(DatePicker.class, delegateClass, "mDelegate");
            assert delegateField != null;
            final Object delegate = delegateField.get(mDatePicker);
            final Class<?> spinnerDelegateClass =
                    Class.forName("android.widget.DatePickerSpinnerDelegate");
            if (delegate.getClass() != spinnerDelegateClass) {
                delegateField.set(mDatePicker, null);
                mDatePicker.removeAllViews();
                final Constructor spinnerDelegateConstructor =
                        spinnerDelegateClass.getDeclaredConstructor(DatePicker.class, Context.class,
                                AttributeSet.class, int.class, int.class);
                spinnerDelegateConstructor.setAccessible(true);
                final Object spinnerDelegate =
                        spinnerDelegateConstructor.newInstance(mDatePicker, context, null,
                                android.R.attr.datePickerStyle, 0);
                delegateField.set(mDatePicker, spinnerDelegate);
                mDatePicker.init(year, monthOfYear, dayOfMonth, this);
                customDatePicker(mDatePicker);
                mDatePicker.setCalendarViewShown(false);
                mDatePicker.setSpinnersShown(true);
            }
        }

        private Field findField(Class objectClass, Class fieldClass, String expectedName) {
            try {
                final Field field = objectClass.getDeclaredField(expectedName);
                field.setAccessible(true);
                return field;
            } catch (NoSuchFieldException ignored) {
            }
            for (final Field field : objectClass.getDeclaredFields()) {
                if (field.getType() == fieldClass) {
                    field.setAccessible(true);
                    return field;
                }
            }
            return null;
        }
    }

Tại hàm dialogMonthYearPicker, thay thế

mDatePickerDialog = new DatePickerDialog(mContext, AlertDialog.THEME_HOLO_LIGHT, onDateSetListener, year, month, -1);

bằng

if (Build.VERSION.SDK_INT == 24) {    // Android 7.0 Nougat, API 24
            final Context contextThemeWrapper =
                    new ContextThemeWrapper(mContext, android.R.style.Theme_Holo_Light_Dialog);
            try {
                mDatePickerDialog =
                        new FixedHoloDatePickerDialog(contextThemeWrapper, onDateSetListener, year,
                                month, -1);
            } catch (ClassNotFoundException | IllegalAccessException | NoSuchMethodException |
                    InvocationTargetException | InstantiationException e) {
                e.printStackTrace();
            }
        } else {
            mDatePickerDialog =
                    new DatePickerDialog(mContext, AlertDialog.THEME_HOLO_LIGHT, onDateSetListener,
                            year, month, -1);
        }

Bùm, vậy là bạn đã fix thành công DatePickerDialog Theme Holo trên Android 7.0 Nougat (API 24), chúc thành công và sức khỏe. Bye bye