2 * Copyright (C) 2007 The Android Open Source Project
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 package android.widget;
19 import com.android.internal.R;
21 import android.annotation.Widget;
22 import android.content.Context;
23 import android.content.res.TypedArray;
24 import android.os.Parcel;
25 import android.os.Parcelable;
26 import android.text.TextUtils;
27 import android.text.format.DateFormat;
28 import android.text.format.DateUtils;
29 import android.util.AttributeSet;
30 import android.util.Log;
31 import android.util.SparseArray;
32 import android.view.LayoutInflater;
33 import android.view.accessibility.AccessibilityEvent;
34 import android.widget.NumberPicker.OnValueChangeListener;
36 import java.text.ParseException;
37 import java.text.SimpleDateFormat;
38 import java.util.Arrays;
39 import java.util.Calendar;
40 import java.util.Locale;
41 import java.util.TimeZone;
44 * This class is a widget for selecting a date. The date can be selected by a
45 * year, month, and day spinners or a {@link CalendarView}. The set of spinners
46 * and the calendar view are automatically synchronized. The client can
47 * customize whether only the spinners, or only the calendar view, or both to be
48 * displayed. Also the minimal and maximal date from which dates to be selected
51 * See the <a href="{@docRoot}resources/tutorials/views/hello-datepicker.html">Date
52 * Picker tutorial</a>.
55 * For a dialog using this view, see {@link android.app.DatePickerDialog}.
58 * @attr ref android.R.styleable#DatePicker_startYear
59 * @attr ref android.R.styleable#DatePicker_endYear
60 * @attr ref android.R.styleable#DatePicker_maxDate
61 * @attr ref android.R.styleable#DatePicker_minDate
62 * @attr ref android.R.styleable#DatePicker_spinnersShown
63 * @attr ref android.R.styleable#DatePicker_calendarViewShown
66 public class DatePicker extends FrameLayout {
68 private static final String LOG_TAG = DatePicker.class.getSimpleName();
70 private static final String DATE_FORMAT = "MM/dd/yyyy";
72 private static final int DEFAULT_START_YEAR = 1900;
74 private static final int DEFAULT_END_YEAR = 2100;
76 private static final boolean DEFAULT_CALENDAR_VIEW_SHOWN = true;
78 private static final boolean DEFAULT_SPINNERS_SHOWN = true;
80 private static final boolean DEFAULT_ENABLED_STATE = true;
82 private final NumberPicker mDaySpinner;
84 private final LinearLayout mSpinners;
86 private final NumberPicker mMonthSpinner;
88 private final NumberPicker mYearSpinner;
90 private final CalendarView mCalendarView;
92 private OnDateChangedListener mOnDateChangedListener;
94 private Locale mMonthLocale;
96 private final Calendar mTempDate = Calendar.getInstance();
98 private final int mNumberOfMonths = mTempDate.getActualMaximum(Calendar.MONTH) + 1;
100 private final String[] mShortMonths = new String[mNumberOfMonths];
102 private final java.text.DateFormat mDateFormat = new SimpleDateFormat(DATE_FORMAT);
104 private final Calendar mMinDate = Calendar.getInstance();
106 private final Calendar mMaxDate = Calendar.getInstance();
108 private final Calendar mCurrentDate = Calendar.getInstance();
110 private boolean mIsEnabled = DEFAULT_ENABLED_STATE;
113 * The callback used to indicate the user changes\d the date.
115 public interface OnDateChangedListener {
118 * Called upon a date change.
120 * @param view The view associated with this listener.
121 * @param year The year that was set.
122 * @param monthOfYear The month that was set (0-11) for compatibility
123 * with {@link java.util.Calendar}.
124 * @param dayOfMonth The day of the month that was set.
126 void onDateChanged(DatePicker view, int year, int monthOfYear, int dayOfMonth);
129 public DatePicker(Context context) {
133 public DatePicker(Context context, AttributeSet attrs) {
134 this(context, attrs, R.attr.datePickerStyle);
137 public DatePicker(Context context, AttributeSet attrs, int defStyle) {
138 super(context, attrs, defStyle);
140 TypedArray attributesArray = context.obtainStyledAttributes(attrs, R.styleable.DatePicker,
142 boolean spinnersShown = attributesArray.getBoolean(R.styleable.DatePicker_spinnersShown,
143 DEFAULT_SPINNERS_SHOWN);
144 boolean calendarViewShown = attributesArray.getBoolean(
145 R.styleable.DatePicker_calendarViewShown, DEFAULT_CALENDAR_VIEW_SHOWN);
146 int startYear = attributesArray.getInt(R.styleable.DatePicker_startYear,
148 int endYear = attributesArray.getInt(R.styleable.DatePicker_endYear, DEFAULT_END_YEAR);
149 String minDate = attributesArray.getString(R.styleable.DatePicker_minDate);
150 String maxDate = attributesArray.getString(R.styleable.DatePicker_maxDate);
151 int layoutResourceId = attributesArray.getResourceId(R.styleable.DatePicker_layout,
152 R.layout.date_picker);
153 attributesArray.recycle();
155 LayoutInflater inflater = (LayoutInflater) context
156 .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
157 inflater.inflate(layoutResourceId, this, true);
159 OnValueChangeListener onChangeListener = new OnValueChangeListener() {
160 public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
161 mTempDate.setTimeInMillis(mCurrentDate.getTimeInMillis());
162 // take care of wrapping of days and months to update greater fields
163 if (picker == mDaySpinner) {
164 int maxDayOfMonth = mTempDate.getActualMaximum(Calendar.DAY_OF_MONTH);
165 if (oldVal == maxDayOfMonth && newVal == 1) {
166 mTempDate.add(Calendar.DAY_OF_MONTH, 1);
167 } else if (oldVal == 1 && newVal == maxDayOfMonth) {
168 mTempDate.add(Calendar.DAY_OF_MONTH, -1);
170 mTempDate.add(Calendar.DAY_OF_MONTH, newVal - oldVal);
172 } else if (picker == mMonthSpinner) {
173 if (oldVal == 11 && newVal == 0) {
174 mTempDate.add(Calendar.MONTH, 1);
175 } else if (oldVal == 0 && newVal == 11) {
176 mTempDate.add(Calendar.MONTH, -1);
178 mTempDate.add(Calendar.MONTH, newVal - oldVal);
180 } else if (picker == mYearSpinner) {
181 mTempDate.set(Calendar.YEAR, newVal);
183 throw new IllegalArgumentException();
185 // now set the date to the adjusted one
186 setDate(mTempDate.get(Calendar.YEAR), mTempDate.get(Calendar.MONTH),
187 mTempDate.get(Calendar.DAY_OF_MONTH));
189 updateCalendarView();
194 mSpinners = (LinearLayout) findViewById(R.id.pickers);
196 // calendar view day-picker
197 mCalendarView = (CalendarView) findViewById(R.id.calendar_view);
198 mCalendarView.setOnDateChangeListener(new CalendarView.OnDateChangeListener() {
199 public void onSelectedDayChange(CalendarView view, int year, int month, int monthDay) {
200 setDate(year, month, monthDay);
207 mDaySpinner = (NumberPicker) findViewById(R.id.day);
208 mDaySpinner.setFormatter(NumberPicker.TWO_DIGIT_FORMATTER);
209 mDaySpinner.setOnLongPressUpdateInterval(100);
210 mDaySpinner.setOnValueChangedListener(onChangeListener);
213 mMonthSpinner = (NumberPicker) findViewById(R.id.month);
214 mMonthSpinner.setMinValue(0);
215 mMonthSpinner.setMaxValue(mNumberOfMonths - 1);
216 mMonthSpinner.setDisplayedValues(getShortMonths());
217 mMonthSpinner.setOnLongPressUpdateInterval(200);
218 mMonthSpinner.setOnValueChangedListener(onChangeListener);
221 mYearSpinner = (NumberPicker) findViewById(R.id.year);
222 mYearSpinner.setOnLongPressUpdateInterval(100);
223 mYearSpinner.setOnValueChangedListener(onChangeListener);
225 // show only what the user required but make sure we
226 // show something and the spinners have higher priority
227 if (!spinnersShown && !calendarViewShown) {
228 setSpinnersShown(true);
230 setSpinnersShown(spinnersShown);
231 setCalendarViewShown(calendarViewShown);
234 // set the min date giving priority of the minDate over startYear
236 if (!TextUtils.isEmpty(minDate)) {
237 if (!parseDate(minDate, mTempDate)) {
238 mTempDate.set(startYear, 0, 1);
241 mTempDate.set(startYear, 0, 1);
243 setMinDate(mTempDate.getTimeInMillis());
245 // set the max date giving priority of the maxDate over endYear
247 if (!TextUtils.isEmpty(maxDate)) {
248 if (!parseDate(maxDate, mTempDate)) {
249 mTempDate.set(endYear, 11, 31);
252 mTempDate.set(endYear, 11, 31);
254 setMaxDate(mTempDate.getTimeInMillis());
256 // initialize to current date
257 mCurrentDate.setTimeInMillis(System.currentTimeMillis());
258 init(mCurrentDate.get(Calendar.YEAR), mCurrentDate.get(Calendar.MONTH), mCurrentDate
259 .get(Calendar.DAY_OF_MONTH), null);
261 // re-order the number spinners to match the current date format
266 * Gets the minimal date supported by this {@link DatePicker} in
267 * milliseconds since January 1, 1970 00:00:00 in
268 * {@link TimeZone#getDefault()} time zone.
270 * Note: The default minimal date is 01/01/1900.
273 * @return The minimal supported date.
275 public long getMinDate() {
276 return mCalendarView.getMinDate();
280 * Sets the minimal date supported by this {@link NumberPicker} in
281 * milliseconds since January 1, 1970 00:00:00 in
282 * {@link TimeZone#getDefault()} time zone.
284 * @param minDate The minimal supported date.
286 public void setMinDate(long minDate) {
287 mTempDate.setTimeInMillis(minDate);
288 if (mTempDate.get(Calendar.YEAR) == mMinDate.get(Calendar.YEAR)
289 && mTempDate.get(Calendar.DAY_OF_YEAR) != mMinDate.get(Calendar.DAY_OF_YEAR)) {
292 mMinDate.setTimeInMillis(minDate);
293 mCalendarView.setMinDate(minDate);
294 if (mCurrentDate.before(mMinDate)) {
295 mCurrentDate.setTimeInMillis(mMinDate.getTimeInMillis());
296 updateCalendarView();
302 * Gets the maximal date supported by this {@link DatePicker} in
303 * milliseconds since January 1, 1970 00:00:00 in
304 * {@link TimeZone#getDefault()} time zone.
306 * Note: The default maximal date is 12/31/2100.
309 * @return The maximal supported date.
311 public long getMaxDate() {
312 return mCalendarView.getMaxDate();
316 * Sets the maximal date supported by this {@link DatePicker} in
317 * milliseconds since January 1, 1970 00:00:00 in
318 * {@link TimeZone#getDefault()} time zone.
320 * @param maxDate The maximal supported date.
322 public void setMaxDate(long maxDate) {
323 mTempDate.setTimeInMillis(maxDate);
324 if (mTempDate.get(Calendar.YEAR) == mMaxDate.get(Calendar.YEAR)
325 && mTempDate.get(Calendar.DAY_OF_YEAR) != mMaxDate.get(Calendar.DAY_OF_YEAR)) {
328 mMaxDate.setTimeInMillis(maxDate);
329 mCalendarView.setMaxDate(maxDate);
330 if (mCurrentDate.after(mMaxDate)) {
331 mCurrentDate.setTimeInMillis(mMaxDate.getTimeInMillis());
332 updateCalendarView();
338 public void setEnabled(boolean enabled) {
339 if (mIsEnabled == enabled) {
342 super.setEnabled(enabled);
343 mDaySpinner.setEnabled(enabled);
344 mMonthSpinner.setEnabled(enabled);
345 mYearSpinner.setEnabled(enabled);
346 mCalendarView.setEnabled(enabled);
347 mIsEnabled = enabled;
351 public boolean isEnabled() {
356 public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
357 int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_WEEKDAY
358 | DateUtils.FORMAT_SHOW_YEAR;
359 String selectedDateUtterance = DateUtils.formatDateTime(mContext,
360 mCurrentDate.getTimeInMillis(), flags);
361 event.getText().add(selectedDateUtterance);
366 * Gets whether the {@link CalendarView} is shown.
368 * @return True if the calendar view is shown.
370 public boolean getCalendarViewShown() {
371 return mCalendarView.isShown();
375 * Sets whether the {@link CalendarView} is shown.
377 * @param shown True if the calendar view is to be shown.
379 public void setCalendarViewShown(boolean shown) {
380 mCalendarView.setVisibility(shown ? VISIBLE : GONE);
384 * Gets whether the spinners are shown.
386 * @return True if the spinners are shown.
388 public boolean getSpinnersShown() {
389 return mSpinners.isShown();
393 * Sets whether the spinners are shown.
395 * @param shown True if the spinners are to be shown.
397 public void setSpinnersShown(boolean shown) {
398 mSpinners.setVisibility(shown ? VISIBLE : GONE);
402 * Reorders the spinners according to the date format in the current
405 private void reorderSpinners() {
406 java.text.DateFormat format;
410 * If the user is in a locale where the medium date format is still
411 * numeric (Japanese and Czech, for example), respect the date format
412 * order setting. Otherwise, use the order that the locale says is
413 * appropriate for a spelled-out date.
416 if (getShortMonths()[0].startsWith("1")) {
417 format = DateFormat.getDateFormat(getContext());
419 format = DateFormat.getMediumDateFormat(getContext());
422 if (format instanceof SimpleDateFormat) {
423 order = ((SimpleDateFormat) format).toPattern();
425 // Shouldn't happen, but just in case.
426 order = new String(DateFormat.getDateFormatOrder(getContext()));
430 * Remove the 3 spinners from their parent and then add them back in the
433 LinearLayout parent = mSpinners;
434 parent.removeAllViews();
436 boolean quoted = false;
437 boolean didDay = false, didMonth = false, didYear = false;
439 for (int i = 0; i < order.length(); i++) {
440 char c = order.charAt(i);
447 if (c == DateFormat.DATE && !didDay) {
448 parent.addView(mDaySpinner);
450 } else if ((c == DateFormat.MONTH || c == 'L') && !didMonth) {
451 parent.addView(mMonthSpinner);
453 } else if (c == DateFormat.YEAR && !didYear) {
454 parent.addView(mYearSpinner);
460 // Shouldn't happen, but just in case.
462 parent.addView(mMonthSpinner);
465 parent.addView(mDaySpinner);
468 parent.addView(mYearSpinner);
473 * Updates the current date.
475 * @param year The year.
476 * @param month The month which is <strong>starting from zero</strong>.
477 * @param dayOfMonth The day of the month.
479 public void updateDate(int year, int month, int dayOfMonth) {
480 if (!isNewDate(year, month, dayOfMonth)) {
483 setDate(year, month, dayOfMonth);
485 updateCalendarView();
489 // Override so we are in complete control of save / restore for this widget.
491 protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
492 dispatchThawSelfOnly(container);
496 protected Parcelable onSaveInstanceState() {
497 Parcelable superState = super.onSaveInstanceState();
498 return new SavedState(superState, getYear(), getMonth(), getDayOfMonth());
502 protected void onRestoreInstanceState(Parcelable state) {
503 SavedState ss = (SavedState) state;
504 super.onRestoreInstanceState(ss.getSuperState());
505 setDate(ss.mYear, ss.mMonth, ss.mDay);
507 updateCalendarView();
511 * Initialize the state. If the provided values designate an inconsistent
512 * date the values are normalized before updating the spinners.
514 * @param year The initial year.
515 * @param monthOfYear The initial month <strong>starting from zero</strong>.
516 * @param dayOfMonth The initial day of the month.
517 * @param onDateChangedListener How user is notified date is changed by
520 public void init(int year, int monthOfYear, int dayOfMonth,
521 OnDateChangedListener onDateChangedListener) {
522 setDate(year, monthOfYear, dayOfMonth);
524 updateCalendarView();
525 mOnDateChangedListener = onDateChangedListener;
529 * Parses the given <code>date</code> and in case of success sets the result
530 * to the <code>outDate</code>.
532 * @return True if the date was parsed.
534 private boolean parseDate(String date, Calendar outDate) {
536 outDate.setTime(mDateFormat.parse(date));
538 } catch (ParseException e) {
539 Log.w(LOG_TAG, "Date: " + date + " not in format: " + DATE_FORMAT);
545 * @return The short month abbreviations.
547 private String[] getShortMonths() {
548 final Locale currentLocale = Locale.getDefault();
549 if (currentLocale.equals(mMonthLocale)) {
552 for (int i = 0; i < mNumberOfMonths; i++) {
553 mShortMonths[i] = DateUtils.getMonthString(Calendar.JANUARY + i,
554 DateUtils.LENGTH_MEDIUM);
556 mMonthLocale = currentLocale;
561 private boolean isNewDate(int year, int month, int dayOfMonth) {
562 return (mCurrentDate.get(Calendar.YEAR) != year
563 || mCurrentDate.get(Calendar.MONTH) != dayOfMonth
564 || mCurrentDate.get(Calendar.DAY_OF_MONTH) != month);
567 private void setDate(int year, int month, int dayOfMonth) {
568 mCurrentDate.set(year, month, dayOfMonth);
569 if (mCurrentDate.before(mMinDate)) {
570 mCurrentDate.setTimeInMillis(mMinDate.getTimeInMillis());
571 } else if (mCurrentDate.after(mMaxDate)) {
572 mCurrentDate.setTimeInMillis(mMaxDate.getTimeInMillis());
576 private void updateSpinners() {
577 // set the spinner ranges respecting the min and max dates
578 if (mCurrentDate.equals(mMinDate)) {
579 mDaySpinner.setMinValue(mCurrentDate.get(Calendar.DAY_OF_MONTH));
580 mDaySpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.DAY_OF_MONTH));
581 mDaySpinner.setWrapSelectorWheel(false);
582 mMonthSpinner.setDisplayedValues(null);
583 mMonthSpinner.setMinValue(mCurrentDate.get(Calendar.MONTH));
584 mMonthSpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.MONTH));
585 mMonthSpinner.setWrapSelectorWheel(false);
586 } else if (mCurrentDate.equals(mMaxDate)) {
587 mDaySpinner.setMinValue(mCurrentDate.getActualMinimum(Calendar.DAY_OF_MONTH));
588 mDaySpinner.setMaxValue(mCurrentDate.get(Calendar.DAY_OF_MONTH));
589 mDaySpinner.setWrapSelectorWheel(false);
590 mMonthSpinner.setDisplayedValues(null);
591 mMonthSpinner.setMinValue(mCurrentDate.getActualMinimum(Calendar.MONTH));
592 mMonthSpinner.setMaxValue(mCurrentDate.get(Calendar.MONTH));
593 mMonthSpinner.setWrapSelectorWheel(false);
595 mDaySpinner.setMinValue(1);
596 mDaySpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.DAY_OF_MONTH));
597 mDaySpinner.setWrapSelectorWheel(true);
598 mMonthSpinner.setDisplayedValues(null);
599 mMonthSpinner.setMinValue(0);
600 mMonthSpinner.setMaxValue(11);
601 mMonthSpinner.setWrapSelectorWheel(true);
604 // make sure the month names are a zero based array
605 // with the months in the month spinner
606 String[] displayedValues = Arrays.copyOfRange(getShortMonths(),
607 mMonthSpinner.getMinValue(), mMonthSpinner.getMaxValue() + 1);
608 mMonthSpinner.setDisplayedValues(displayedValues);
610 // year spinner range does not change based on the current date
611 mYearSpinner.setMinValue(mMinDate.get(Calendar.YEAR));
612 mYearSpinner.setMaxValue(mMaxDate.get(Calendar.YEAR));
613 mYearSpinner.setWrapSelectorWheel(false);
615 // set the spinner values
616 mYearSpinner.setValue(mCurrentDate.get(Calendar.YEAR));
617 mMonthSpinner.setValue(mCurrentDate.get(Calendar.MONTH));
618 mDaySpinner.setValue(mCurrentDate.get(Calendar.DAY_OF_MONTH));
622 * Updates the calendar view with the current date.
624 private void updateCalendarView() {
625 mCalendarView.setDate(mCurrentDate.getTimeInMillis(), false, false);
629 * @return The selected year.
631 public int getYear() {
632 return mCurrentDate.get(Calendar.YEAR);
636 * @return The selected month.
638 public int getMonth() {
639 return mCurrentDate.get(Calendar.MONTH);
643 * @return The selected day of month.
645 public int getDayOfMonth() {
646 return mCurrentDate.get(Calendar.DAY_OF_MONTH);
650 * Notifies the listener, if such, for a change in the selected date.
652 private void notifyDateChanged() {
653 sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
654 if (mOnDateChangedListener != null) {
655 mOnDateChangedListener.onDateChanged(this, getYear(), getMonth(), getDayOfMonth());
660 * Class for managing state storing/restoring.
662 private static class SavedState extends BaseSavedState {
664 private final int mYear;
666 private final int mMonth;
668 private final int mDay;
671 * Constructor called from {@link DatePicker#onSaveInstanceState()}
673 private SavedState(Parcelable superState, int year, int month, int day) {
681 * Constructor called from {@link #CREATOR}
683 private SavedState(Parcel in) {
685 mYear = in.readInt();
686 mMonth = in.readInt();
691 public void writeToParcel(Parcel dest, int flags) {
692 super.writeToParcel(dest, flags);
693 dest.writeInt(mYear);
694 dest.writeInt(mMonth);
698 @SuppressWarnings("all")
699 // suppress unused and hiding
700 public static final Parcelable.Creator<SavedState> CREATOR = new Creator<SavedState>() {
702 public SavedState createFromParcel(Parcel in) {
703 return new SavedState(in);
706 public SavedState[] newArray(int size) {
707 return new SavedState[size];