/* * Copyright (C) 2007 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.widget; import com.android.internal.R; import android.annotation.Widget; import android.content.Context; import android.content.res.TypedArray; import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; import android.text.format.DateFormat; import android.text.format.DateUtils; import android.util.AttributeSet; import android.util.Log; import android.util.SparseArray; import android.view.LayoutInflater; import android.view.accessibility.AccessibilityEvent; import android.widget.NumberPicker.OnValueChangeListener; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Calendar; import java.util.Locale; import java.util.TimeZone; /** * This class is a widget for selecting a date. The date can be selected by a * year, month, and day spinners or a {@link CalendarView}. The set of spinners * and the calendar view are automatically synchronized. The client can * customize whether only the spinners, or only the calendar view, or both to be * displayed. Also the minimal and maximal date from which dates to be selected * can be customized. *
* See the Date * Picker tutorial. *
** For a dialog using this view, see {@link android.app.DatePickerDialog}. *
* * @attr ref android.R.styleable#DatePicker_startYear * @attr ref android.R.styleable#DatePicker_endYear * @attr ref android.R.styleable#DatePicker_maxDate * @attr ref android.R.styleable#DatePicker_minDate * @attr ref android.R.styleable#DatePicker_spinnersShown * @attr ref android.R.styleable#DatePicker_calendarViewShown */ @Widget public class DatePicker extends FrameLayout { private static final String LOG_TAG = DatePicker.class.getSimpleName(); private static final String DATE_FORMAT = "MM/dd/yyyy"; private static final int DEFAULT_START_YEAR = 1900; private static final int DEFAULT_END_YEAR = 2100; private static final boolean DEFAULT_CALENDAR_VIEW_SHOWN = true; private static final boolean DEFAULT_SPINNERS_SHOWN = true; private static final boolean DEFAULT_ENABLED_STATE = true; private final NumberPicker mDaySpinner; private final LinearLayout mSpinners; private final NumberPicker mMonthSpinner; private final NumberPicker mYearSpinner; private final CalendarView mCalendarView; private OnDateChangedListener mOnDateChangedListener; private Locale mMonthLocale; private final Calendar mTempDate = Calendar.getInstance(); private final int mNumberOfMonths = mTempDate.getActualMaximum(Calendar.MONTH) + 1; private final String[] mShortMonths = new String[mNumberOfMonths]; private final java.text.DateFormat mDateFormat = new SimpleDateFormat(DATE_FORMAT); private final Calendar mMinDate = Calendar.getInstance(); private final Calendar mMaxDate = Calendar.getInstance(); private final Calendar mCurrentDate = Calendar.getInstance(); private boolean mIsEnabled = DEFAULT_ENABLED_STATE; /** * The callback used to indicate the user changes\d the date. */ public interface OnDateChangedListener { /** * Called upon a date change. * * @param view The view associated with this listener. * @param year The year that was set. * @param monthOfYear The month that was set (0-11) for compatibility * with {@link java.util.Calendar}. * @param dayOfMonth The day of the month that was set. */ void onDateChanged(DatePicker view, int year, int monthOfYear, int dayOfMonth); } public DatePicker(Context context) { this(context, null); } public DatePicker(Context context, AttributeSet attrs) { this(context, attrs, R.attr.datePickerStyle); } public DatePicker(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); TypedArray attributesArray = context.obtainStyledAttributes(attrs, R.styleable.DatePicker, defStyle, 0); boolean spinnersShown = attributesArray.getBoolean(R.styleable.DatePicker_spinnersShown, DEFAULT_SPINNERS_SHOWN); boolean calendarViewShown = attributesArray.getBoolean( R.styleable.DatePicker_calendarViewShown, DEFAULT_CALENDAR_VIEW_SHOWN); int startYear = attributesArray.getInt(R.styleable.DatePicker_startYear, DEFAULT_START_YEAR); int endYear = attributesArray.getInt(R.styleable.DatePicker_endYear, DEFAULT_END_YEAR); String minDate = attributesArray.getString(R.styleable.DatePicker_minDate); String maxDate = attributesArray.getString(R.styleable.DatePicker_maxDate); int layoutResourceId = attributesArray.getResourceId(R.styleable.DatePicker_layout, R.layout.date_picker); attributesArray.recycle(); LayoutInflater inflater = (LayoutInflater) context .getSystemService(Context.LAYOUT_INFLATER_SERVICE); inflater.inflate(layoutResourceId, this, true); OnValueChangeListener onChangeListener = new OnValueChangeListener() { public void onValueChange(NumberPicker picker, int oldVal, int newVal) { mTempDate.setTimeInMillis(mCurrentDate.getTimeInMillis()); // take care of wrapping of days and months to update greater fields if (picker == mDaySpinner) { int maxDayOfMonth = mTempDate.getActualMaximum(Calendar.DAY_OF_MONTH); if (oldVal == maxDayOfMonth && newVal == 1) { mTempDate.add(Calendar.DAY_OF_MONTH, 1); } else if (oldVal == 1 && newVal == maxDayOfMonth) { mTempDate.add(Calendar.DAY_OF_MONTH, -1); } else { mTempDate.add(Calendar.DAY_OF_MONTH, newVal - oldVal); } } else if (picker == mMonthSpinner) { if (oldVal == 11 && newVal == 0) { mTempDate.add(Calendar.MONTH, 1); } else if (oldVal == 0 && newVal == 11) { mTempDate.add(Calendar.MONTH, -1); } else { mTempDate.add(Calendar.MONTH, newVal - oldVal); } } else if (picker == mYearSpinner) { mTempDate.set(Calendar.YEAR, newVal); } else { throw new IllegalArgumentException(); } // now set the date to the adjusted one setDate(mTempDate.get(Calendar.YEAR), mTempDate.get(Calendar.MONTH), mTempDate.get(Calendar.DAY_OF_MONTH)); updateSpinners(); updateCalendarView(); notifyDateChanged(); } }; mSpinners = (LinearLayout) findViewById(R.id.pickers); // calendar view day-picker mCalendarView = (CalendarView) findViewById(R.id.calendar_view); mCalendarView.setOnDateChangeListener(new CalendarView.OnDateChangeListener() { public void onSelectedDayChange(CalendarView view, int year, int month, int monthDay) { setDate(year, month, monthDay); updateSpinners(); notifyDateChanged(); } }); // day mDaySpinner = (NumberPicker) findViewById(R.id.day); mDaySpinner.setFormatter(NumberPicker.TWO_DIGIT_FORMATTER); mDaySpinner.setOnLongPressUpdateInterval(100); mDaySpinner.setOnValueChangedListener(onChangeListener); // month mMonthSpinner = (NumberPicker) findViewById(R.id.month); mMonthSpinner.setMinValue(0); mMonthSpinner.setMaxValue(mNumberOfMonths - 1); mMonthSpinner.setDisplayedValues(getShortMonths()); mMonthSpinner.setOnLongPressUpdateInterval(200); mMonthSpinner.setOnValueChangedListener(onChangeListener); // year mYearSpinner = (NumberPicker) findViewById(R.id.year); mYearSpinner.setOnLongPressUpdateInterval(100); mYearSpinner.setOnValueChangedListener(onChangeListener); // show only what the user required but make sure we // show something and the spinners have higher priority if (!spinnersShown && !calendarViewShown) { setSpinnersShown(true); } else { setSpinnersShown(spinnersShown); setCalendarViewShown(calendarViewShown); } // set the min date giving priority of the minDate over startYear mTempDate.clear(); if (!TextUtils.isEmpty(minDate)) { if (!parseDate(minDate, mTempDate)) { mTempDate.set(startYear, 0, 1); } } else { mTempDate.set(startYear, 0, 1); } setMinDate(mTempDate.getTimeInMillis()); // set the max date giving priority of the maxDate over endYear mTempDate.clear(); if (!TextUtils.isEmpty(maxDate)) { if (!parseDate(maxDate, mTempDate)) { mTempDate.set(endYear, 11, 31); } } else { mTempDate.set(endYear, 11, 31); } setMaxDate(mTempDate.getTimeInMillis()); // initialize to current date mCurrentDate.setTimeInMillis(System.currentTimeMillis()); init(mCurrentDate.get(Calendar.YEAR), mCurrentDate.get(Calendar.MONTH), mCurrentDate .get(Calendar.DAY_OF_MONTH), null); // re-order the number spinners to match the current date format reorderSpinners(); } /** * Gets the minimal date supported by this {@link DatePicker} in * milliseconds since January 1, 1970 00:00:00 in * {@link TimeZone#getDefault()} time zone. ** Note: The default minimal date is 01/01/1900. *
* * @return The minimal supported date. */ public long getMinDate() { return mCalendarView.getMinDate(); } /** * Sets the minimal date supported by this {@link NumberPicker} in * milliseconds since January 1, 1970 00:00:00 in * {@link TimeZone#getDefault()} time zone. * * @param minDate The minimal supported date. */ public void setMinDate(long minDate) { mTempDate.setTimeInMillis(minDate); if (mTempDate.get(Calendar.YEAR) == mMinDate.get(Calendar.YEAR) && mTempDate.get(Calendar.DAY_OF_YEAR) != mMinDate.get(Calendar.DAY_OF_YEAR)) { return; } mMinDate.setTimeInMillis(minDate); mCalendarView.setMinDate(minDate); if (mCurrentDate.before(mMinDate)) { mCurrentDate.setTimeInMillis(mMinDate.getTimeInMillis()); updateCalendarView(); } updateSpinners(); } /** * Gets the maximal date supported by this {@link DatePicker} in * milliseconds since January 1, 1970 00:00:00 in * {@link TimeZone#getDefault()} time zone. *
* Note: The default maximal date is 12/31/2100. *
*
* @return The maximal supported date.
*/
public long getMaxDate() {
return mCalendarView.getMaxDate();
}
/**
* Sets the maximal date supported by this {@link DatePicker} in
* milliseconds since January 1, 1970 00:00:00 in
* {@link TimeZone#getDefault()} time zone.
*
* @param maxDate The maximal supported date.
*/
public void setMaxDate(long maxDate) {
mTempDate.setTimeInMillis(maxDate);
if (mTempDate.get(Calendar.YEAR) == mMaxDate.get(Calendar.YEAR)
&& mTempDate.get(Calendar.DAY_OF_YEAR) != mMaxDate.get(Calendar.DAY_OF_YEAR)) {
return;
}
mMaxDate.setTimeInMillis(maxDate);
mCalendarView.setMaxDate(maxDate);
if (mCurrentDate.after(mMaxDate)) {
mCurrentDate.setTimeInMillis(mMaxDate.getTimeInMillis());
updateCalendarView();
}
updateSpinners();
}
@Override
public void setEnabled(boolean enabled) {
if (mIsEnabled == enabled) {
return;
}
super.setEnabled(enabled);
mDaySpinner.setEnabled(enabled);
mMonthSpinner.setEnabled(enabled);
mYearSpinner.setEnabled(enabled);
mCalendarView.setEnabled(enabled);
mIsEnabled = enabled;
}
@Override
public boolean isEnabled() {
return mIsEnabled;
}
@Override
public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_WEEKDAY
| DateUtils.FORMAT_SHOW_YEAR;
String selectedDateUtterance = DateUtils.formatDateTime(mContext,
mCurrentDate.getTimeInMillis(), flags);
event.getText().add(selectedDateUtterance);
return true;
}
/**
* Gets whether the {@link CalendarView} is shown.
*
* @return True if the calendar view is shown.
*/
public boolean getCalendarViewShown() {
return mCalendarView.isShown();
}
/**
* Sets whether the {@link CalendarView} is shown.
*
* @param shown True if the calendar view is to be shown.
*/
public void setCalendarViewShown(boolean shown) {
mCalendarView.setVisibility(shown ? VISIBLE : GONE);
}
/**
* Gets whether the spinners are shown.
*
* @return True if the spinners are shown.
*/
public boolean getSpinnersShown() {
return mSpinners.isShown();
}
/**
* Sets whether the spinners are shown.
*
* @param shown True if the spinners are to be shown.
*/
public void setSpinnersShown(boolean shown) {
mSpinners.setVisibility(shown ? VISIBLE : GONE);
}
/**
* Reorders the spinners according to the date format in the current
* {@link Locale}.
*/
private void reorderSpinners() {
java.text.DateFormat format;
String order;
/*
* If the user is in a locale where the medium date format is still
* numeric (Japanese and Czech, for example), respect the date format
* order setting. Otherwise, use the order that the locale says is
* appropriate for a spelled-out date.
*/
if (getShortMonths()[0].startsWith("1")) {
format = DateFormat.getDateFormat(getContext());
} else {
format = DateFormat.getMediumDateFormat(getContext());
}
if (format instanceof SimpleDateFormat) {
order = ((SimpleDateFormat) format).toPattern();
} else {
// Shouldn't happen, but just in case.
order = new String(DateFormat.getDateFormatOrder(getContext()));
}
/*
* Remove the 3 spinners from their parent and then add them back in the
* required order.
*/
LinearLayout parent = mSpinners;
parent.removeAllViews();
boolean quoted = false;
boolean didDay = false, didMonth = false, didYear = false;
for (int i = 0; i < order.length(); i++) {
char c = order.charAt(i);
if (c == '\'') {
quoted = !quoted;
}
if (!quoted) {
if (c == DateFormat.DATE && !didDay) {
parent.addView(mDaySpinner);
didDay = true;
} else if ((c == DateFormat.MONTH || c == 'L') && !didMonth) {
parent.addView(mMonthSpinner);
didMonth = true;
} else if (c == DateFormat.YEAR && !didYear) {
parent.addView(mYearSpinner);
didYear = true;
}
}
}
// Shouldn't happen, but just in case.
if (!didMonth) {
parent.addView(mMonthSpinner);
}
if (!didDay) {
parent.addView(mDaySpinner);
}
if (!didYear) {
parent.addView(mYearSpinner);
}
}
/**
* Updates the current date.
*
* @param year The year.
* @param month The month which is starting from zero.
* @param dayOfMonth The day of the month.
*/
public void updateDate(int year, int month, int dayOfMonth) {
if (!isNewDate(year, month, dayOfMonth)) {
return;
}
setDate(year, month, dayOfMonth);
updateSpinners();
updateCalendarView();
notifyDateChanged();
}
// Override so we are in complete control of save / restore for this widget.
@Override
protected void dispatchRestoreInstanceState(SparseArraydate
and in case of success sets the result
* to the outDate
.
*
* @return True if the date was parsed.
*/
private boolean parseDate(String date, Calendar outDate) {
try {
outDate.setTime(mDateFormat.parse(date));
return true;
} catch (ParseException e) {
Log.w(LOG_TAG, "Date: " + date + " not in format: " + DATE_FORMAT);
return false;
}
}
/**
* @return The short month abbreviations.
*/
private String[] getShortMonths() {
final Locale currentLocale = Locale.getDefault();
if (currentLocale.equals(mMonthLocale)) {
return mShortMonths;
} else {
for (int i = 0; i < mNumberOfMonths; i++) {
mShortMonths[i] = DateUtils.getMonthString(Calendar.JANUARY + i,
DateUtils.LENGTH_MEDIUM);
}
mMonthLocale = currentLocale;
return mShortMonths;
}
}
private boolean isNewDate(int year, int month, int dayOfMonth) {
return (mCurrentDate.get(Calendar.YEAR) != year
|| mCurrentDate.get(Calendar.MONTH) != dayOfMonth
|| mCurrentDate.get(Calendar.DAY_OF_MONTH) != month);
}
private void setDate(int year, int month, int dayOfMonth) {
mCurrentDate.set(year, month, dayOfMonth);
if (mCurrentDate.before(mMinDate)) {
mCurrentDate.setTimeInMillis(mMinDate.getTimeInMillis());
} else if (mCurrentDate.after(mMaxDate)) {
mCurrentDate.setTimeInMillis(mMaxDate.getTimeInMillis());
}
}
private void updateSpinners() {
// set the spinner ranges respecting the min and max dates
if (mCurrentDate.equals(mMinDate)) {
mDaySpinner.setMinValue(mCurrentDate.get(Calendar.DAY_OF_MONTH));
mDaySpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.DAY_OF_MONTH));
mDaySpinner.setWrapSelectorWheel(false);
mMonthSpinner.setDisplayedValues(null);
mMonthSpinner.setMinValue(mCurrentDate.get(Calendar.MONTH));
mMonthSpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.MONTH));
mMonthSpinner.setWrapSelectorWheel(false);
} else if (mCurrentDate.equals(mMaxDate)) {
mDaySpinner.setMinValue(mCurrentDate.getActualMinimum(Calendar.DAY_OF_MONTH));
mDaySpinner.setMaxValue(mCurrentDate.get(Calendar.DAY_OF_MONTH));
mDaySpinner.setWrapSelectorWheel(false);
mMonthSpinner.setDisplayedValues(null);
mMonthSpinner.setMinValue(mCurrentDate.getActualMinimum(Calendar.MONTH));
mMonthSpinner.setMaxValue(mCurrentDate.get(Calendar.MONTH));
mMonthSpinner.setWrapSelectorWheel(false);
} else {
mDaySpinner.setMinValue(1);
mDaySpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.DAY_OF_MONTH));
mDaySpinner.setWrapSelectorWheel(true);
mMonthSpinner.setDisplayedValues(null);
mMonthSpinner.setMinValue(0);
mMonthSpinner.setMaxValue(11);
mMonthSpinner.setWrapSelectorWheel(true);
}
// make sure the month names are a zero based array
// with the months in the month spinner
String[] displayedValues = Arrays.copyOfRange(getShortMonths(),
mMonthSpinner.getMinValue(), mMonthSpinner.getMaxValue() + 1);
mMonthSpinner.setDisplayedValues(displayedValues);
// year spinner range does not change based on the current date
mYearSpinner.setMinValue(mMinDate.get(Calendar.YEAR));
mYearSpinner.setMaxValue(mMaxDate.get(Calendar.YEAR));
mYearSpinner.setWrapSelectorWheel(false);
// set the spinner values
mYearSpinner.setValue(mCurrentDate.get(Calendar.YEAR));
mMonthSpinner.setValue(mCurrentDate.get(Calendar.MONTH));
mDaySpinner.setValue(mCurrentDate.get(Calendar.DAY_OF_MONTH));
}
/**
* Updates the calendar view with the current date.
*/
private void updateCalendarView() {
mCalendarView.setDate(mCurrentDate.getTimeInMillis(), false, false);
}
/**
* @return The selected year.
*/
public int getYear() {
return mCurrentDate.get(Calendar.YEAR);
}
/**
* @return The selected month.
*/
public int getMonth() {
return mCurrentDate.get(Calendar.MONTH);
}
/**
* @return The selected day of month.
*/
public int getDayOfMonth() {
return mCurrentDate.get(Calendar.DAY_OF_MONTH);
}
/**
* Notifies the listener, if such, for a change in the selected date.
*/
private void notifyDateChanged() {
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
if (mOnDateChangedListener != null) {
mOnDateChangedListener.onDateChanged(this, getYear(), getMonth(), getDayOfMonth());
}
}
/**
* Class for managing state storing/restoring.
*/
private static class SavedState extends BaseSavedState {
private final int mYear;
private final int mMonth;
private final int mDay;
/**
* Constructor called from {@link DatePicker#onSaveInstanceState()}
*/
private SavedState(Parcelable superState, int year, int month, int day) {
super(superState);
mYear = year;
mMonth = month;
mDay = day;
}
/**
* Constructor called from {@link #CREATOR}
*/
private SavedState(Parcel in) {
super(in);
mYear = in.readInt();
mMonth = in.readInt();
mDay = in.readInt();
}
@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeInt(mYear);
dest.writeInt(mMonth);
dest.writeInt(mDay);
}
@SuppressWarnings("all")
// suppress unused and hiding
public static final Parcelable.Creator