PreferenceFragmentCompat 屏幕上的 DatePicker

DatePicker on PreferenceFragmentCompat screen

我用的是 unofficial PreferenceFragment compatibility layer for Android 1.6 and up. But Google has released v7 Preference Support Library。所以我的想法是迁移项目库。

当前实施: 在项目中我创建了一个自定义设置来支持 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);
        }

    }
}

为了支持这种偏好,我使用 DatePickerFragment 来允许用户 select 使用本机 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;
    }

}

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>

问题: 问题是 onCreateDialogViewonDialogClosed 在 v7 支持中找不到 DialogPreference.

编辑: 我发现 v7 PreferenceDialogFragmentCompat 应该与 v7 DialogPreference.

一起使用

v7 PreferenceDialogFragmentCompat 应与 v7 DialogPreference 结合使用。所以使用这个 response 代码可以用这种方式重构。

已修改 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);
        }

    }

}

新建 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());
    }
}

我还在 SettingsFragment 中添加了以下代码,它扩展了 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);
    }
}