4

I’m currently developing a simple project consisting of a website wrapped in a web view with minor interactions to improve interactivity between the site itself and android mobile devices.

Since the website includes a date input field for the user’s birthdate, I was looking to implement a datepicker in spinner format compatible with all devices. I tried implementing the following solution:

`<style name="MyAppTheme" parent="android:Theme.Material">
    <item name="android:dialogTheme">@style/MyDialogTheme</item>
    <item name="android:datePickerStyle">@style/MyDatePicker</item>
</style>
<style name="MyDialogTheme" parent="android:Theme.Material.Dialog">
    <item name="android:datePickerStyle">@style/MyDatePicker</item>
</style>
<style name="MyDatePicker" parent="android:Widget.Material.DatePicker">
    <item name="android:datePickerMode">spinner</item>
</style>`

As shown in: Datepicker dialog without calendar visualization in lollipop [spinner mode]?

However, as the answer points out, this solution does not work with Android 7.0 Nougat / API 24 due to the following known bug: https://issuetracker.google.com/issues/37119315

Doing some research I came across some proposed solutions such as:

https://gist.github.com/uqmessias/d9a8dc624af935f344dfa2e8928490ec

https://gist.github.com/lognaturel/232395ee1079ff9e4b1b8e7096c3afaf

DatePickerDialog Holo styling failed on Android 7 Nougat

However in my case, none of them seem to work either for devices with Android 7.0, or any other devices.

I am heavily inexperienced in android development so I was wondering what could be causing this and how could I solve it, as some of the solutions have been maintained up to recently, which pretty much discards that they could have become obsolete.

Do I need to implement something that hasn’t been pointed out in the code I posted and I am not aware of?

Is it the fact that the datepicker element is being called directly from the web view and not from the app itself that renders these solutions useless to me? And if so, is there any work around for this?

Oranakia
  • 129
  • 1
  • 10
  • 1
    Is the reason you want a spinner so that it is easier to select a year in the distant past (convenient for birthdays)? If so, you can also force the date picker to start in year selection mode first (this worked for me, since as you noted forcing the spinner in API 24 doesn't seem to work) – Tyler V Jul 20 '18 at 19:01
  • @TylerV Sounds like a good alternative. I wasn’t really aware of that as a possibility. Do you have any steps that I could follow in order to accomplish that? Also thanks for the quick response. – Oranakia Jul 20 '18 at 19:13
  • 1
    Sure, I'll post an answer to that effect. – Tyler V Jul 20 '18 at 19:17

2 Answers2

2

I ran into the same issue (users don't want to scroll back in time 40 years month by month to find their birth year and most don't know you can just click on the year in the android date picker to scroll through years). Like you, I couldn't get the spinner to work universally, so I figured out (with help from SO and Google) how to make it start in year selection mode.

The code for my DatePickerDialogFragment is pasted below.

public class DatePickerDialogFragment extends DialogFragment {

    private DatePickerDialog.OnDateSetListener listener = null;

    void setListener(DatePickerDialog.OnDateSetListener listener) {
        this.listener = listener;
    }

    private static final String START_IN_YEARS = "com.myapp.picker.START_IN_YEARS";
    private static final String YEAR = "com.myapp.picker.YEAR";
    private static final String MONTH = "com.myapp.picker.MONTH";
    private static final String DAY_OF_MONTH = "com.myapp.picker.DAY_OF_MONTH";

    public static DatePickerDialogFragment newInstance(boolean startInYears, Calendar c) {
        DatePickerDialogFragment f = new DatePickerDialogFragment();

        int year = c.get(Calendar.YEAR);
        int month = c.get(Calendar.MONTH);
        int day = c.get(Calendar.DAY_OF_MONTH);

        Bundle args = new Bundle();
        args.putBoolean(START_IN_YEARS, startInYears);
        args.putInt(YEAR, year);
        args.putInt(MONTH, month);
        args.putInt(DAY_OF_MONTH, day);

        f.setArguments(args);
        return f;
    }

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {

        Bundle args = getArguments();
        DatePickerDialog dpd = null;

        if( listener != null && args != null) {
            boolean startInYears = args.getBoolean(START_IN_YEARS);

            Context context = getActivity();
            boolean requireSpinnerMode = isBrokenSamsungDevice();
            if (requireSpinnerMode) {
                context = new ContextThemeWrapper(context, android.R.style.Theme_Holo_Light_Dialog);
            }

            int year = args.getInt(YEAR);
            int month = args.getInt(MONTH);
            int day = args.getInt(DAY_OF_MONTH);

            dpd = new DatePickerDialog(context, listener, year, month, day);

            if (startInYears && !requireSpinnerMode) {
                boolean canOpenYearView = openYearView(dpd.getDatePicker());
                if (!canOpenYearView) {
                    context = new ContextThemeWrapper(getActivity(), android.R.style.Theme_Holo_Light_Dialog);
                    dpd = new DatePickerDialog(context, listener, year, month, day);
                }
            }
        }
        else {
            setShowsDialog(false);
            dismissAllowingStateLoss();
        }

        return dpd;
    }

    private static boolean isBrokenSamsungDevice() {
        return Build.MANUFACTURER.equalsIgnoreCase("samsung") &&
                isBetweenAndroidVersions(
                        Build.VERSION_CODES.LOLLIPOP,
                        Build.VERSION_CODES.LOLLIPOP_MR1);
    }

    private static boolean isBetweenAndroidVersions(int min, int max) {
        return Build.VERSION.SDK_INT >= min && Build.VERSION.SDK_INT <= max;
    }

    private static boolean openYearView(DatePicker datePicker) {
        if( isBrokenSamsungDevice() ) {
            return false;
        }

        View v = datePicker.findViewById(Resources.getSystem().getIdentifier("date_picker_header_year", "id", "android"));
        if( v != null ) {
            v.performClick();
        }
        else {
            try {
                Field mDelegateField = datePicker.getClass().getDeclaredField("mDelegate");
                mDelegateField.setAccessible(true);
                Object delegate = mDelegateField.get(datePicker);
                Method setCurrentViewMethod = delegate.getClass().getDeclaredMethod("setCurrentView", int.class);
                setCurrentViewMethod.setAccessible(true);
                setCurrentViewMethod.invoke(delegate, 1);
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }
        return true;
    }
}

The code in the Activity (member variables and stuff in onCreate) to launch this (and preserve it on rotation) looks like this:

// Class member variables
private Calendar myCalendar = Calendar.getInstance();
private boolean birthday_is_set = false;


// this next part is in onCreate

// set the calendar date to a saved date if applicable
// and change birthday_is_set if they had saved a birthday


final DatePickerDialog.OnDateSetListener birthdayListener = new DatePickerDialog.OnDateSetListener() {

    @Override
    public void onDateSet(DatePicker view, int year, int monthOfYear,
                          int dayOfMonth) {
        // I save the date in a calendar, replace this
        // with whatever you want to do with the selected date
        myCalendar.set(Calendar.YEAR, year);
        myCalendar.set(Calendar.MONTH, monthOfYear);
        myCalendar.set(Calendar.DAY_OF_MONTH, dayOfMonth);
        birthday_is_set = true;
        updateBirthdayLabel();
    }
};

if (savedInstanceState != null) {
    DatePickerDialogFragment dpf;

    dpf = (DatePickerDialogFragment) getFragmentManager().findFragmentByTag("birthdayDatePicker");
    if (dpf != null) {
        // on rotation the listener will be referring to the old Activity,
        // so we have to reset it here to act on the current Activity
        dpf.setListener(birthdayListener);
    }
}

birthdayDatePicker.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        // Your logic may vary here. I chose not to start it in year
        // mode if they've already selected a date.
        boolean startInYears = !birthday_is_set;
        DatePickerDialogFragment dpf = DatePickerDialogFragment.newInstance(startInYears, myCalendar);
        dpf.setListener(birthdayListener);
        dpf.show(getFragmentManager(), "birthdayDatePicker");
    }
});

This includes both the hack to get it to start in year mode, and a fix for some random date picker failures on Samsung devices of a certain vintage. This version has been working without crashes or user complaints for API 15+ for a few months now.

EDIT: Updated openYearView to work on Android 10

Tyler V
  • 9,694
  • 3
  • 26
  • 52
  • I notice that you call an onClick event to set and call the fragment. Given that I use an input field from a webView, I don’t have such an event. Can I fire a call from a funcion in my WebViewInterface in this case? Or moving the contents of birthdayDatePicker.setOnClickListener will break the whole thing? – Oranakia Jul 20 '18 at 20:41
  • Also, I wasn’t aware that you could click on the year either, so worst case scenario I’d just prompt the users to do this. Which is leagues ahead of what I had. – Oranakia Jul 20 '18 at 20:41
  • You would move the stuff in `onClick` to wherever you're launching the DatePicker from. The stuff in `birthdayListener` (in the `onDateSet` method) would handle what to do once the user picks a date from the date picker. – Tyler V Jul 20 '18 at 20:42
1

For anyone who's interested in making DatePicker from WebView working properly without adding a JS interface to manually call your own DatePicker, here is a solution that can override ALL DatePickers in your app:

https://github.com/Noisyfox/DatePickerForceSpinner

This fix also applies to any DatePicker created by LayoutInflater so you don't need to worry about changing your existing layout files.

Noisyfox
  • 31
  • 4