Search code examples
androidandroid-layoutandroid-preferencesandroid-datepicker

DatePicker on PreferenceFragmentCompat screen


I was using an unofficial PreferenceFragment compatibility layer for Android 1.6 and up. But Google has released v7 Preference Support Library. So my idea is to migrate project library.

CURRENT IMPLEMENTATION: In the project I have created a custom setting to support dates.

public class DatePreference extends DialogPreference implements DatePicker.OnDateChangedListener {

    private String dateString;
    private String changedValueCanBeNull;
    private DatePicker datePicker;
    private Calendar maxCalendar;
    private int maxYear;
    private int maxMonth;
    private int maxDay;

    public DatePreference(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
    @Override
    protected View onCreateDialogView() {

        this.datePicker = new DatePicker(getContext());

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
           this.datePicker.setMaxDate(Calendar.getInstance().getTimeInMillis());
        } else {
            maxCalendar = Calendar.getInstance();
            maxYear = maxCalendar.get(Calendar.YEAR);
            maxMonth = maxCalendar.get(Calendar.MONTH);
            maxDay = maxCalendar.get(Calendar.DAY_OF_MONTH);
        }

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
            this.datePicker.setCalendarViewShown(false);
        }

        final Calendar calendar = getDate();
        datePicker.init(calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH), calendar.get(Calendar.DAY_OF_MONTH),
                this);
        return datePicker;
    }

    public Calendar getDate() {
        try {
            final SimpleDateFormat sfd = formatter();
            final Date date = sfd.parse(defaultValue());
            final Calendar cal = Calendar.getInstance();
            cal.setTime(date);
            return cal;
        } catch (ParseException e) {
            return defaultCalendar();
        }
    }

    public void setDate(final String dateString) {
        this.dateString = dateString;
    }

    public static SimpleDateFormat formatter() {
        return new SimpleDateFormat("yyyy.MM.dd",    Locale.getDefault());
    }

    public static SimpleDateFormat summaryFormatter() {
        return new SimpleDateFormat("MMMM dd, yyyy",    Locale.getDefault());
    }

    @Override
    protected Object onGetDefaultValue(final TypedArray a, final int index) {
        return a.getString(index);
   }

    @Override
    protected void onSetInitialValue(final boolean restoreValue, final Object def) {
        if (restoreValue) {
            this.dateString = getPersistedString(defaultValue());
            setTheDate(this.dateString);
        } else {
            final boolean wasNull = this.dateString == null;
            setDate((String) def);
            if (!wasNull) {
                persistDate(this.dateString);
            }
        }
    }

    @Override
    protected Parcelable onSaveInstanceState() {
        if (isPersistent()) {
            return super.onSaveInstanceState();
        } else {
            return new SavedState(super.onSaveInstanceState());
        }
    }

    @Override
    protected void onRestoreInstanceState(final Parcelable state) {
        if (state == null || !state.getClass().equals(SavedState.class)) {
            super.onRestoreInstanceState(state);
            try {
                if (state != null) {
                    setTheDate(((SavedState) state).dateValue);
                }
            } catch (ClassCastException e) {
                  SiempreListoLogger.INSTANCE.error(DatePreference.class.getSimpleName(), "Error casting class to date preference " + Log.getStackTraceString(e));
            }
        } else {
            final SavedState s = (SavedState) state;
            super.onRestoreInstanceState(s.getSuperState());
            setTheDate(s.dateValue);
        }
    }

    @Override
    public void onDateChanged(final DatePicker view, final int year, final int month, final int day) {
        Calendar selected = new GregorianCalendar(year, month, day);
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB && selected.after(maxCalendar)) {
            selected = new GregorianCalendar(maxYear, maxMonth, maxDay);
        }
        this.changedValueCanBeNull = formatter().format(selected.getTime());

    }

    @Override
    protected void onDialogClosed(final boolean shouldSave) {
        if (shouldSave && this.changedValueCanBeNull != null) {
            setTheDate(this.changedValueCanBeNull);
            this.changedValueCanBeNull = null;
        }
    }

    private void setTheDate(final String s) {
        setDate(s);
        persistDate(s);
    }

    private void persistDate(final String s) {
        persistString(s);
        setSummary(summaryFormatter().format(getDate().getTime()));
    }

    public static Calendar defaultCalendar() {
        return new GregorianCalendar(1900, 0, 1);
    }

    public static String defaultCalendarString() {
        return formatter().format(defaultCalendar().getTime());
    }

    private String defaultValue() {
        if (this.dateString == null) {
            setDate(defaultCalendarString());
        }
        return this.dateString;
    }

    @Override
    public void onClick(final DialogInterface dialog, final int which)    {
        super.onClick(dialog, which);
        datePicker.clearFocus();
        onDateChanged(datePicker, datePicker.getYear(), datePicker.getMonth(), datePicker.getDayOfMonth());
        onDialogClosed(which == DialogInterface.BUTTON_POSITIVE); // OK?
    }

    private static class SavedState extends BaseSavedState {

        private transient String dateValue;

        @SuppressWarnings("unused")
        public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() {
            @Override
            public SavedState createFromParcel(final Parcel parcel) {
                return new SavedState(parcel);
            }

            @Override
            public SavedState[] newArray(final int size) {
                return new SavedState[size];
            }
        };

        public SavedState(final Parcel p) {
            super(p);
            dateValue = p.readString();
        }

        public SavedState(final Parcelable p) {
            super(p);
        }

        @Override
        public void writeToParcel(final Parcel parcel, final int flags) {
            super.writeToParcel(parcel, flags);
            parcel.writeString(dateValue);
        }

    }
}

And to support this preference I'm using a DatePickerFragment to allow user select the date using native DatePicker.

public class DatePickerFragment extends DialogFragment {

    public static final String TAG = DatePickerFragment.class.getName();

    private static final String MINIMUM_DATE_TO_SHOW = "minimum_date_to_show";

    private static final String DATE_TO_SHOW = "date_to_show";

    private DatePickerDialog.OnDateSetListener mListener;

    public static DatePickerFragment newInstance(DatePickerDialog.OnDateSetListener listener) {
        DatePickerFragment newInstance = new DatePickerFragment();
        newInstance.mListener = listener;
        return newInstance;
    }

    public static DatePickerFragment newInstance(DatePickerDialog.OnDateSetListener listener, Date date) {
        DatePickerFragment newInstance = new DatePickerFragment();
        Bundle args = new Bundle();
        args.putSerializable(MINIMUM_DATE_TO_SHOW, date);
        args.putSerializable(DATE_TO_SHOW, date);
        newInstance.setArguments(args);
        newInstance.mListener = listener;
        return newInstance;
    }

    @Override
    @NonNull
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        // Use the current date as the default date in the picker
        final Calendar c = Calendar.getInstance();
        int minimumYear = c.get(Calendar.YEAR);
        int minimumMonth = c.get(Calendar.MONTH);
        int minimumDay = c.get(Calendar.DAY_OF_MONTH);

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

        if (getArguments() != null &&    getArguments().containsKey(MINIMUM_DATE_TO_SHOW)) {
            Date date = (Date) getArguments().getSerializable(MINIMUM_DATE_TO_SHOW);
            if (date != null) {
                c.setTime(date);
                minimumYear = c.get(Calendar.YEAR);
                minimumMonth = c.get(Calendar.MONTH);
                minimumDay = c.get(Calendar.DAY_OF_MONTH);
            }
        }

        if (getArguments() != null &&    getArguments().containsKey(DATE_TO_SHOW)) {
            Date date = (Date) getArguments().getSerializable(DATE_TO_SHOW);
            if (date != null) {
                c.setTime(date);
                year = c.get(Calendar.YEAR);
                month = c.get(Calendar.MONTH);
                day = c.get(Calendar.DAY_OF_MONTH);
            }
        }

        final Date minimumDate = Utils.getDateFromValues(minimumYear, minimumMonth, minimumDay);

        // Create a new instance of DatePickerDialog and return it
        final DatePickerDialog datePickerDialog = new DatePickerDialog(getActivity(), mListener, year, month, day);
        datePickerDialog.setButton(DialogInterface.BUTTON_POSITIVE, getString(android.R.string.ok), new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                DatePicker datePicker = datePickerDialog.getDatePicker();
                datePicker.setMinDate(minimumDate.getTime());
                mListener.onDateSet(datePicker, datePicker.getYear(), datePicker.getMonth(), datePicker.getDayOfMonth());
            }
        });

        datePickerDialog.setButton(DialogInterface.BUTTON_NEGATIVE, getString(android.R.string.cancel), new DialogInterface.OnClickListener()    {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                DatePickerFragment.this.dismiss();
            }
        });
        return datePickerDialog;
    }

}

And the settings.xml:

<PreferenceCategory
    android:layout="@layout/preference_activity_bg"
    android:title="@string/pref_user_private_info">

    <com.company.project.util.DatePreference
        android:defaultValue="1980.01.01"
        android:icon="@drawable/ic_birthdate"
        android:key="prefBirthdate"
        android:title="@string/pref_user_birthdate"/>

    <Preference
        android:icon="@drawable/ic_email"
        android:key="prefEmail"
        android:summary="@string/pref_user_email"
        android:title="@string/pref_user_email"/>

    <EditTextPreference
        android:icon="@drawable/ic_ic_telefono"
        android:inputType="phone"
        android:key="prefTel"
        android:maxLength="10"
        android:summary="@string/pref_user_phone"
        android:title="@string/pref_user_phone"/>

    <Preference
        android:icon="@drawable/ic_points"
        android:key="prefBalance"
        android:title="@string/pref_user_points"/>
</PreferenceCategory>

PROBLEM: The problem states is that onCreateDialogView and onDialogClosed are not found in v7 support DialogPreference.

EDIT: I found that v7 PreferenceDialogFragmentCompat should be used with v7 DialogPreference.


Solution

  • The v7 PreferenceDialogFragmentCompat should be used in conjunction with v7 DialogPreference. So using this response code could be refactored on this way.

    Modified DatePrefence class:

    public class DatePreference extends DialogPreference {
    
        private final String TAG = getClass().getSimpleName();
    
        public String dateString;
    
        public DatePreference(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        public static String dateToString(Calendar calendar) {
            return summaryFormatter().format(calendar.getTime());
        }
    
        @Override
        protected Object onGetDefaultValue(TypedArray a, int index) {
            return a.getString(index);
        }
    
        @Override
        protected void onSetInitialValue(boolean restorePersistedValue, Object defaultValue) {
            if (restorePersistedValue) {
                if (defaultValue == null) dateString = getPersistedString(defaultValue());
                else dateString = getPersistedString(defaultValue.toString());
                setTheDate(dateString);
            } else {
                final boolean wasNull = dateString == null;
                setDate((String) defaultValue);
                if (!wasNull) {
                    persistStringValue(dateString);
                }
            }
        }
    
        @Override
        protected Parcelable onSaveInstanceState() {
            if (isPersistent()) {
                return super.onSaveInstanceState();
            } else {
                return new SavedState(super.onSaveInstanceState());
            }
        }
    
        @Override
        protected void onRestoreInstanceState(Parcelable state) {
            if (state == null || !state.getClass().equals(SavedState.class)) {
                super.onRestoreInstanceState(state);
                try {
                    if (state != null) {
                        setTheDate(((SavedState) state).dateValue);
                    }
                } catch (ClassCastException e) {
                    SiempreListoLogger.INSTANCE.error(TAG, "Error casting class to date preference " + Log.getStackTraceString(e));
                }
            } else {
                SavedState s = (SavedState) state;
                super.onRestoreInstanceState(s.getSuperState());
                setTheDate(s.dateValue);
            }
        }
    
        private String defaultValue() {
            if (this.dateString == null) {
                setDate(defaultCalendarString());
            }
            return this.dateString;
        }
    
        public static String defaultCalendarString() {
            return formatter().format(defaultCalendar().getTime());
        }
    
        public static SimpleDateFormat formatter() {
            return new SimpleDateFormat("yyyy.MM.dd", Locale.getDefault());
        }
    
        public static SimpleDateFormat summaryFormatter() {
            return new SimpleDateFormat("MMMM dd, yyyy", Locale.getDefault());
        }
    
        public static Calendar defaultCalendar() {
            return new GregorianCalendar(1900, 0, 1);
        }
    
        public void setTheDate(String s) {
            setDate(s);
            persistStringValue(s);
        }
    
        public void persistStringValue(String value) {
            persistString(value);
            setSummary(summaryFormatter().format(getDate().getTime()));
        }
    
        public void setDate(String dateString) {
            this.dateString = dateString;
        }
    
        public Calendar getDate() {
            try {
                final SimpleDateFormat sfd = formatter();
                final Date date = sfd.parse(defaultValue());
                final Calendar cal = Calendar.getInstance();
                cal.setTime(date);
                return cal;
            } catch (ParseException e) {
                return defaultCalendar();
            }
        }
    
        private static class SavedState extends Preference.BaseSavedState {
    
            private transient String dateValue;
    
            @SuppressWarnings("unused")
            public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() {
                @Override
                public SavedState createFromParcel(final Parcel parcel) {
                    return new SavedState(parcel);
                }
    
                @Override
                public SavedState[] newArray(final int size) {
                    return new SavedState[size];
                }
            };
    
            public SavedState(final Parcel p) {
                super(p);
                dateValue = p.readString();
            }
    
            public SavedState(final Parcelable p) {
                super(p);
            }
    
            @Override
            public void writeToParcel(final Parcel parcel, final int flags) {
                super.writeToParcel(parcel, flags);
                parcel.writeString(dateValue);
            }
    
        }
    
    }
    

    New DatePreferenceDialogFragmentCompat class:

    public class DatePreferenceDialogFragmentCompat extends PreferenceDialogFragmentCompat implements DialogPreference.TargetFragment, DatePicker.OnDateChangedListener {
    
        DatePicker datePicker = null;
        private String changedValueCanBeNull;
        private Calendar maxCalendar;
        private int maxYear;
        private int maxMonth;
        private int maxDay;
    
        @TargetApi(Build.VERSION_CODES.HONEYCOMB)
        @Override
        protected View onCreateDialogView(Context context) {
            datePicker = new DatePicker(context);
    
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
                datePicker.setMaxDate(Calendar.getInstance().getTimeInMillis());
            } else {
                maxCalendar = Calendar.getInstance();
                maxYear = maxCalendar.get(Calendar.YEAR);
                maxMonth = maxCalendar.get(Calendar.MONTH);
                maxDay = maxCalendar.get(Calendar.DAY_OF_MONTH);
            }
    
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
                this.datePicker.setCalendarViewShown(false);
            }
    
            DatePreference pref = (DatePreference) getPreference();
            Calendar calendar = pref.getDate();
            datePicker.init(calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH), calendar.get(Calendar.DAY_OF_MONTH), this);
            return datePicker;
        }
    
        @Override
        protected void onBindDialogView(View view) {
            super.onBindDialogView(view);
            DatePreference pref = (DatePreference) getPreference();
            String value = DatePreference.dateToString(pref.getDate());
            if (pref.callChangeListener(value)) pref.persistStringValue(value);
        }
    
        @Override
        public void onDialogClosed(boolean positiveResult) {
            if (positiveResult && changedValueCanBeNull != null) {
                DatePreference pref = (DatePreference) getPreference();
                pref.setTheDate(changedValueCanBeNull);
                changedValueCanBeNull = null;
            }
        }
    
        @Override
        public void onClick(DialogInterface dialog, int which) {
            super.onClick(dialog, which);
            datePicker.clearFocus();
            onDateChanged(datePicker, datePicker.getYear(), datePicker.getMonth(), datePicker.getDayOfMonth());
            onDialogClosed(which == DialogInterface.BUTTON_POSITIVE); // OK?
        }
    
        @Override
        public Preference findPreference(CharSequence charSequence) {
            return getPreference();
        }
    
        @Override
        public void onDateChanged(DatePicker view, int year, int monthOfYear, int dayOfMonth) {
            Calendar selected = new GregorianCalendar(year, monthOfYear, dayOfMonth);
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB && selected.after(maxCalendar)) {
                selected = new GregorianCalendar(maxYear, maxMonth, maxDay);
            }
            changedValueCanBeNull = DatePreference.formatter().format(selected.getTime());
        }
    }
    

    I also added the following code to my SettingsFragment that extends PreferenceFragmentCompat:

    @Override
    public void onDisplayPreferenceDialog(Preference preference) {
        DialogFragment dialogFragment = null;
        if (preference instanceof DatePreference) {
            dialogFragment = new DatePreferenceDialogFragmentCompat();
            Bundle bundle = new Bundle(1);
            bundle.putString("key", preference.getKey());
            dialogFragment.setArguments(bundle);
        }
    
        if (dialogFragment != null) {
            dialogFragment.setTargetFragment(this, 0);
            dialogFragment.show(this.getFragmentManager(), "android.support.v7.preference.PreferenceFragment.DIALOG");
        } else {
            super.onDisplayPreferenceDialog(preference);
        }
    }