OSDN Git Service

Ensure that no callback is invoked while initializing DatePicker
[android-x86/frameworks-base.git] / core / java / android / widget / DatePicker.java
1 /*
2  * Copyright (C) 2007 The Android Open Source Project
3  *
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
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
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.
15  */
16
17 package android.widget;
18
19 import com.android.internal.R;
20
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.widget.NumberPicker.OnValueChangedListener;
34
35 import java.text.ParseException;
36 import java.text.SimpleDateFormat;
37 import java.util.Calendar;
38 import java.util.Locale;
39 import java.util.TimeZone;
40
41 /**
42  * This class is a widget for selecting a date. The date can be selected by a
43  * year, month, and day spinners or a {@link CalendarView}. The set of spinners
44  * and the calendar view are automatically synchronized. The client can
45  * customize whether only the spinners, or only the calendar view, or both to be
46  * displayed. Also the minimal and maximal date from which dates to be selected
47  * can be customized.
48  * <p>
49  * See the <a href="{@docRoot}
50  * resources/tutorials/views/hello-datepicker.html">Date Picker tutorial</a>.
51  * </p>
52  * <p>
53  * For a dialog using this view, see {@link android.app.DatePickerDialog}.
54  * </p>
55  *
56  * @attr ref android.R.styleable#DatePicker_startYear
57  * @attr ref android.R.styleable#DatePicker_endYear
58  * @attr ref android.R.styleable#DatePicker_maxDate
59  * @attr ref android.R.styleable#DatePicker_minDate
60  * @attr ref android.R.styleable#DatePicker_spinnersShown
61  * @attr ref android.R.styleable#DatePicker_calendarViewShown
62  */
63 @Widget
64 public class DatePicker extends FrameLayout {
65
66     private static final String LOG_TAG = DatePicker.class.getSimpleName();
67
68     private static final String DATE_FORMAT = "MM/dd/yyyy";
69
70     private static final int DEFAULT_START_YEAR = 1900;
71
72     private static final int DEFAULT_END_YEAR = 2100;
73
74     private static final boolean DEFAULT_CALENDAR_VIEW_SHOWN = true;
75
76     private static final boolean DEFAULT_SPINNERS_SHOWN = true;
77
78     private static final boolean DEFAULT_ENABLED_STATE = true;
79
80     private final NumberPicker mDaySpinner;
81
82     private final LinearLayout mSpinners;
83
84     private final NumberPicker mMonthSpinner;
85
86     private final NumberPicker mYearSpinner;
87
88     private final CalendarView mCalendarView;
89
90     private OnDateChangedListener mOnDateChangedListener;
91
92     private Locale mMonthLocale;
93
94     private final Calendar mTempDate = Calendar.getInstance();
95
96     private final int mNumberOfMonths = mTempDate.getActualMaximum(Calendar.MONTH) + 1;
97
98     private final String[] mShortMonths = new String[mNumberOfMonths];
99
100     private final java.text.DateFormat mDateFormat = new SimpleDateFormat(DATE_FORMAT);
101
102     private final Calendar mMinDate = Calendar.getInstance();
103
104     private final Calendar mMaxDate = Calendar.getInstance();
105
106     private final Calendar mCurrentDate = Calendar.getInstance();
107
108     private boolean mIsEnabled = DEFAULT_ENABLED_STATE;
109
110     /**
111      * The callback used to indicate the user changes\d the date.
112      */
113     public interface OnDateChangedListener {
114
115         /**
116          * Called upon a date change.
117          *
118          * @param view The view associated with this listener.
119          * @param year The year that was set.
120          * @param monthOfYear The month that was set (0-11) for compatibility
121          *            with {@link java.util.Calendar}.
122          * @param dayOfMonth The day of the month that was set.
123          */
124         void onDateChanged(DatePicker view, int year, int monthOfYear, int dayOfMonth);
125     }
126
127     public DatePicker(Context context) {
128         this(context, null);
129     }
130
131     public DatePicker(Context context, AttributeSet attrs) {
132         this(context, attrs, 0);
133     }
134
135     public DatePicker(Context context, AttributeSet attrs, int defStyle) {
136         super(context, attrs, defStyle);
137
138         TypedArray attributesArray = context.obtainStyledAttributes(attrs, R.styleable.DatePicker);
139         boolean spinnersShown = attributesArray.getBoolean(R.styleable.DatePicker_spinnersShown,
140                 DEFAULT_SPINNERS_SHOWN);
141         boolean calendarViewShown = attributesArray.getBoolean(
142                 R.styleable.DatePicker_calendarViewShown, DEFAULT_CALENDAR_VIEW_SHOWN);
143         int startYear = attributesArray
144                 .getInt(R.styleable.DatePicker_startYear, DEFAULT_START_YEAR);
145         int endYear = attributesArray.getInt(R.styleable.DatePicker_endYear, DEFAULT_END_YEAR);
146         String minDate = attributesArray.getString(R.styleable.DatePicker_minDate);
147         String maxDate = attributesArray.getString(R.styleable.DatePicker_maxDate);
148         attributesArray.recycle();
149
150         LayoutInflater inflater = (LayoutInflater) context
151                 .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
152         inflater.inflate(R.layout.date_picker, this, true);
153
154         OnValueChangedListener onChangeListener = new OnValueChangedListener() {
155             public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
156                 updateDate(mYearSpinner.getValue(), mMonthSpinner.getValue(), mDaySpinner
157                         .getValue());
158             }
159         };
160
161         mSpinners = (LinearLayout) findViewById(R.id.pickers);
162
163         // calendar view day-picker
164         mCalendarView = (CalendarView) findViewById(R.id.calendar_view);
165         mCalendarView.setOnDateChangeListener(new CalendarView.OnDateChangeListener() {
166             public void onSelectedDayChange(CalendarView view, int year, int month, int monthDay) {
167                 updateDate(year, month, monthDay);
168             }
169         });
170
171         // day
172         mDaySpinner = (NumberPicker) findViewById(R.id.day);
173         mDaySpinner.setFormatter(NumberPicker.TWO_DIGIT_FORMATTER);
174         mDaySpinner.setOnLongPressUpdateInterval(100);
175         mDaySpinner.setOnValueChangedListener(onChangeListener);
176
177         // month
178         mMonthSpinner = (NumberPicker) findViewById(R.id.month);
179         mMonthSpinner.setMinValue(0);
180         mMonthSpinner.setMaxValue(mNumberOfMonths - 1);
181         mMonthSpinner.setDisplayedValues(getShortMonths());
182         mMonthSpinner.setOnLongPressUpdateInterval(200);
183         mMonthSpinner.setOnValueChangedListener(onChangeListener);
184
185         // year
186         mYearSpinner = (NumberPicker) findViewById(R.id.year);
187         mYearSpinner.setOnLongPressUpdateInterval(100);
188         mYearSpinner.setOnValueChangedListener(onChangeListener);
189
190         // show only what the user required but make sure we
191         // show something and the spinners have higher priority
192         if (!spinnersShown && !calendarViewShown) {
193             setSpinnersShown(true);
194         } else {
195             setSpinnersShown(spinnersShown);
196             setCalendarViewShown(calendarViewShown);
197
198             // set the min date giving priority of the minDate over startYear
199             mTempDate.clear();
200             if (!TextUtils.isEmpty(minDate)) {
201                 if (!parseDate(minDate, mTempDate)) {
202                     mTempDate.set(startYear, 0, 1);
203                 }
204             } else {
205                 mTempDate.set(startYear, 0, 1);
206             }
207             mMinDate.clear();
208             setMinDate(mTempDate.getTimeInMillis());
209
210             // set the max date giving priority of the minDate over startYear
211             mTempDate.clear();
212             if (!TextUtils.isEmpty(maxDate)) {
213                 if (!parseDate(maxDate, mTempDate)) {
214                     mTempDate.set(endYear, 11, 31);
215                 }
216             } else {
217                 mTempDate.set(endYear, 11, 31);
218             }
219             mMaxDate.clear();
220             setMaxDate(mTempDate.getTimeInMillis());
221
222             // initialize to current date
223             mCurrentDate.setTimeInMillis(System.currentTimeMillis());
224             init(mCurrentDate.get(Calendar.YEAR), mCurrentDate.get(Calendar.MONTH), mCurrentDate
225                     .get(Calendar.DAY_OF_MONTH), null);
226         }
227
228         // re-order the number spinners to match the current date format
229         reorderSpinners();
230     }
231
232     /**
233      * Gets the minimal date supported by this {@link DatePicker} in
234      * milliseconds since January 1, 1970 00:00:00 in
235      * {@link TimeZone#getDefault()} time zone.
236      * <p>
237      * Note: The default minimal date is 01/01/1900.
238      * <p>
239      *
240      * @return The minimal supported date.
241      */
242     public long getMinDate() {
243         return mCalendarView.getMinDate();
244     }
245
246     /**
247      * Sets the minimal date supported by this {@link NumberPicker} in
248      * milliseconds since January 1, 1970 00:00:00 in
249      * {@link TimeZone#getDefault()} time zone.
250      *
251      * @param minDate The minimal supported date.
252      */
253     public void setMinDate(long minDate) {
254         mTempDate.setTimeInMillis(minDate);
255         if (mTempDate.get(Calendar.YEAR) == mMinDate.get(Calendar.YEAR)
256                 && mTempDate.get(Calendar.DAY_OF_YEAR) != mMinDate.get(Calendar.DAY_OF_YEAR)) {
257             return;
258         }
259         mMinDate.setTimeInMillis(minDate);
260         mYearSpinner.setMinValue(mMinDate.get(Calendar.YEAR));
261         mYearSpinner.setMaxValue(mMaxDate.get(Calendar.YEAR));
262         mCalendarView.setMinDate(minDate);
263         updateSpinners(mYearSpinner.getValue(), mMonthSpinner.getValue(), mDaySpinner.getValue());
264     }
265
266     /**
267      * Gets the maximal date supported by this {@link DatePicker} in
268      * milliseconds since January 1, 1970 00:00:00 in
269      * {@link TimeZone#getDefault()} time zone.
270      * <p>
271      * Note: The default maximal date is 12/31/2100.
272      * <p>
273      *
274      * @return The maximal supported date.
275      */
276     public long getMaxDate() {
277         return mCalendarView.getMaxDate();
278     }
279
280     /**
281      * Sets the maximal date supported by this {@link DatePicker} in
282      * milliseconds since January 1, 1970 00:00:00 in
283      * {@link TimeZone#getDefault()} time zone.
284      *
285      * @param maxDate The maximal supported date.
286      */
287     public void setMaxDate(long maxDate) {
288         mTempDate.setTimeInMillis(maxDate);
289         if (mTempDate.get(Calendar.YEAR) == mMaxDate.get(Calendar.YEAR)
290                 && mTempDate.get(Calendar.DAY_OF_YEAR) != mMaxDate.get(Calendar.DAY_OF_YEAR)) {
291             return;
292         }
293         mMaxDate.setTimeInMillis(maxDate);
294         mYearSpinner.setMinValue(mMinDate.get(Calendar.YEAR));
295         mYearSpinner.setMaxValue(mMaxDate.get(Calendar.YEAR));
296         mCalendarView.setMaxDate(maxDate);
297         updateSpinners(mYearSpinner.getValue(), mMonthSpinner.getValue(), mDaySpinner.getValue());
298     }
299
300     @Override
301     public void setEnabled(boolean enabled) {
302         if (mIsEnabled == enabled) {
303             return;
304         }
305         super.setEnabled(enabled);
306         mDaySpinner.setEnabled(enabled);
307         mMonthSpinner.setEnabled(enabled);
308         mYearSpinner.setEnabled(enabled);
309         mCalendarView.setEnabled(enabled);
310         mIsEnabled = enabled;
311     }
312
313     @Override
314     public boolean isEnabled() {
315         return mIsEnabled;
316     }
317
318     /**
319      * Gets whether the {@link CalendarView} is shown.
320      *
321      * @return True if the calendar view is shown.
322      */
323     public boolean getCalendarViewShown() {
324         return mCalendarView.isShown();
325     }
326
327     /**
328      * Sets whether the {@link CalendarView} is shown.
329      *
330      * @param shown True if the calendar view is to be shown.
331      */
332     public void setCalendarViewShown(boolean shown) {
333         mCalendarView.setVisibility(shown ? VISIBLE : GONE);
334     }
335
336     /**
337      * Gets whether the spinners are shown.
338      *
339      * @return True if the spinners are shown.
340      */
341     public boolean getSpinnersShown() {
342         return mSpinners.isShown();
343     }
344
345     /**
346      * Sets whether the spinners are shown.
347      *
348      * @param shown True if the spinners are to be shown.
349      */
350     public void setSpinnersShown(boolean shown) {
351         mSpinners.setVisibility(shown ? VISIBLE : GONE);
352     }
353
354     /**
355      * Reorders the spinners according to the date format in the current
356      * {@link Locale}.
357      */
358     private void reorderSpinners() {
359         java.text.DateFormat format;
360         String order;
361
362         /*
363          * If the user is in a locale where the medium date format is still
364          * numeric (Japanese and Czech, for example), respect the date format
365          * order setting. Otherwise, use the order that the locale says is
366          * appropriate for a spelled-out date.
367          */
368
369         if (getShortMonths()[0].startsWith("1")) {
370             format = DateFormat.getDateFormat(getContext());
371         } else {
372             format = DateFormat.getMediumDateFormat(getContext());
373         }
374
375         if (format instanceof SimpleDateFormat) {
376             order = ((SimpleDateFormat) format).toPattern();
377         } else {
378             // Shouldn't happen, but just in case.
379             order = new String(DateFormat.getDateFormatOrder(getContext()));
380         }
381
382         /*
383          * Remove the 3 spinners from their parent and then add them back in the
384          * required order.
385          */
386         LinearLayout parent = mSpinners;
387         parent.removeAllViews();
388
389         boolean quoted = false;
390         boolean didDay = false, didMonth = false, didYear = false;
391
392         for (int i = 0; i < order.length(); i++) {
393             char c = order.charAt(i);
394
395             if (c == '\'') {
396                 quoted = !quoted;
397             }
398
399             if (!quoted) {
400                 if (c == DateFormat.DATE && !didDay) {
401                     parent.addView(mDaySpinner);
402                     didDay = true;
403                 } else if ((c == DateFormat.MONTH || c == 'L') && !didMonth) {
404                     parent.addView(mMonthSpinner);
405                     didMonth = true;
406                 } else if (c == DateFormat.YEAR && !didYear) {
407                     parent.addView(mYearSpinner);
408                     didYear = true;
409                 }
410             }
411         }
412
413         // Shouldn't happen, but just in case.
414         if (!didMonth) {
415             parent.addView(mMonthSpinner);
416         }
417         if (!didDay) {
418             parent.addView(mDaySpinner);
419         }
420         if (!didYear) {
421             parent.addView(mYearSpinner);
422         }
423     }
424
425     /**
426      * Updates the current date.
427      *
428      * @param year The year.
429      * @param month The month which is <strong>starting from zero</strong>.
430      * @param dayOfMonth The day of the month.
431      */
432     public void updateDate(int year, int month, int dayOfMonth) {
433         if (mCurrentDate.get(Calendar.YEAR) != year
434                 || mCurrentDate.get(Calendar.MONTH) != dayOfMonth
435                 || mCurrentDate.get(Calendar.DAY_OF_MONTH) != month) {
436             updateSpinners(year, month, dayOfMonth);
437             updateCalendarView();
438             notifyDateChanged();
439         }
440     }
441
442     // Override so we are in complete control of save / restore for this widget.
443     @Override
444     protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
445         dispatchThawSelfOnly(container);
446     }
447
448     @Override
449     protected Parcelable onSaveInstanceState() {
450         Parcelable superState = super.onSaveInstanceState();
451         return new SavedState(superState, mYearSpinner.getValue(), mMonthSpinner.getValue(),
452                 mDaySpinner.getValue());
453     }
454
455     @Override
456     protected void onRestoreInstanceState(Parcelable state) {
457         SavedState ss = (SavedState) state;
458         super.onRestoreInstanceState(ss.getSuperState());
459         updateSpinners(ss.mYear, ss.mMonth, ss.mDay);
460     }
461
462     /**
463      * Initialize the state. If the provided values designate an inconsistent
464      * date the values are normalized before updating the spinners.
465      *
466      * @param year The initial year.
467      * @param monthOfYear The initial month <strong>starting from zero</strong>.
468      * @param dayOfMonth The initial day of the month.
469      * @param onDateChangedListener How user is notified date is changed by
470      *            user, can be null.
471      */
472     public void init(int year, int monthOfYear, int dayOfMonth,
473             OnDateChangedListener onDateChangedListener) {
474         // make sure there is no callback
475         mOnDateChangedListener = null;
476         updateDate(year, monthOfYear, dayOfMonth);
477         // register the callback after updating the date
478         mOnDateChangedListener = onDateChangedListener;
479     }
480
481     /**
482      * Parses the given <code>date</code> and in case of success sets the result
483      * to the <code>outDate</code>.
484      *
485      * @return True if the date was parsed.
486      */
487     private boolean parseDate(String date, Calendar outDate) {
488         try {
489             outDate.setTime(mDateFormat.parse(date));
490             return true;
491         } catch (ParseException e) {
492             Log.w(LOG_TAG, "Date: " + date + " not in format: " + DATE_FORMAT);
493             return false;
494         }
495     }
496
497     /**
498      * @return The short month abbreviations.
499      */
500     private String[] getShortMonths() {
501         final Locale currentLocale = Locale.getDefault();
502         if (currentLocale.equals(mMonthLocale)) {
503             return mShortMonths;
504         } else {
505             for (int i = 0; i < mNumberOfMonths; i++) {
506                 mShortMonths[i] = DateUtils.getMonthString(Calendar.JANUARY + i,
507                         DateUtils.LENGTH_MEDIUM);
508             }
509             mMonthLocale = currentLocale;
510             return mShortMonths;
511         }
512     }
513
514     /**
515      * Updates the spinners with the given <code>year</code>, <code>month</code>
516      * , and <code>dayOfMonth</code>. If the provided values designate an
517      * inconsistent date the values are normalized before updating the spinners.
518      */
519     private void updateSpinners(int year, int month, int dayOfMonth) {
520         // compute the deltas before modifying the current date
521         int deltaMonths = getDelataMonth(month);
522         int deltaDays = getDelataDayOfMonth(dayOfMonth);
523         mCurrentDate.set(Calendar.YEAR, year);
524         mCurrentDate.add(Calendar.MONTH, deltaMonths);
525         mCurrentDate.add(Calendar.DAY_OF_MONTH, deltaDays);
526
527         if (mCurrentDate.before(mMinDate)) {
528             mCurrentDate.setTimeInMillis(mMinDate.getTimeInMillis());
529         } else if (mCurrentDate.after(mMaxDate)) {
530             mCurrentDate.setTimeInMillis(mMaxDate.getTimeInMillis());
531         }
532
533         mYearSpinner.setValue(mCurrentDate.get(Calendar.YEAR));
534         mMonthSpinner.setValue(mCurrentDate.get(Calendar.MONTH));
535         mDaySpinner.setMinValue(1);
536         mDaySpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.DAY_OF_MONTH));
537         mDaySpinner.setValue(mCurrentDate.get(Calendar.DAY_OF_MONTH));
538     }
539
540     /**
541      * @return The delta days of moth from the current date and the given
542      *         <code>dayOfMonth</code>.
543      */
544     private int getDelataDayOfMonth(int dayOfMonth) {
545         int prevDayOfMonth = mCurrentDate.get(Calendar.DAY_OF_MONTH);
546         if (prevDayOfMonth == dayOfMonth) {
547             return 0;
548         }
549         int maxDayOfMonth = mCurrentDate.getActualMaximum(Calendar.DAY_OF_MONTH);
550         if (dayOfMonth == 1 && prevDayOfMonth == maxDayOfMonth) {
551             return 1;
552         }
553         if (dayOfMonth == maxDayOfMonth && prevDayOfMonth == 1) {
554             return -1;
555         }
556         return dayOfMonth - prevDayOfMonth;
557     }
558
559     /**
560      * @return The delta months from the current date and the given
561      *         <code>month</code>.
562      */
563     private int getDelataMonth(int month) {
564         int prevMonth = mCurrentDate.get(Calendar.MONTH);
565         if (prevMonth == month) {
566             return 0;
567         }
568         if (month == 0 && prevMonth == 11) {
569             return 1;
570         }
571         if (month == 11 && prevMonth == 0) {
572             return -1;
573         }
574         return month - prevMonth;
575     }
576
577     /**
578      * Updates the calendar view with the given year, month, and day selected by
579      * the number spinners.
580      */
581     private void updateCalendarView() {
582         mTempDate.setTimeInMillis(mCalendarView.getDate());
583         if (mTempDate.get(Calendar.YEAR) != mYearSpinner.getValue()
584                 || mTempDate.get(Calendar.MONTH) != mMonthSpinner.getValue()
585                 || mTempDate.get(Calendar.DAY_OF_MONTH) != mDaySpinner.getValue()) {
586             mTempDate.clear();
587             mTempDate.set(mYearSpinner.getValue(), mMonthSpinner.getValue(),
588                     mDaySpinner.getValue());
589             mCalendarView.setDate(mTempDate.getTimeInMillis(), false, false);
590         }
591     }
592
593     /**
594      * @return The selected year.
595      */
596     public int getYear() {
597         return mYearSpinner.getValue();
598     }
599
600     /**
601      * @return The selected month.
602      */
603     public int getMonth() {
604         return mMonthSpinner.getValue();
605     }
606
607     /**
608      * @return The selected day of month.
609      */
610     public int getDayOfMonth() {
611         return mDaySpinner.getValue();
612     }
613
614     /**
615      * Notifies the listener, if such, for a change in the selected date.
616      */
617     private void notifyDateChanged() {
618         if (mOnDateChangedListener != null) {
619             mOnDateChangedListener.onDateChanged(DatePicker.this, mYearSpinner.getValue(),
620                     mMonthSpinner.getValue(), mDaySpinner.getValue());
621         }
622     }
623
624     /**
625      * Class for managing state storing/restoring.
626      */
627     private static class SavedState extends BaseSavedState {
628
629         private final int mYear;
630
631         private final int mMonth;
632
633         private final int mDay;
634
635         /**
636          * Constructor called from {@link DatePicker#onSaveInstanceState()}
637          */
638         private SavedState(Parcelable superState, int year, int month, int day) {
639             super(superState);
640             mYear = year;
641             mMonth = month;
642             mDay = day;
643         }
644
645         /**
646          * Constructor called from {@link #CREATOR}
647          */
648         private SavedState(Parcel in) {
649             super(in);
650             mYear = in.readInt();
651             mMonth = in.readInt();
652             mDay = in.readInt();
653         }
654
655         @Override
656         public void writeToParcel(Parcel dest, int flags) {
657             super.writeToParcel(dest, flags);
658             dest.writeInt(mYear);
659             dest.writeInt(mMonth);
660             dest.writeInt(mDay);
661         }
662
663         @SuppressWarnings("all")
664         // suppress unused and hiding
665         public static final Parcelable.Creator<SavedState> CREATOR = new Creator<SavedState>() {
666
667             public SavedState createFromParcel(Parcel in) {
668                 return new SavedState(in);
669             }
670
671             public SavedState[] newArray(int size) {
672                 return new SavedState[size];
673             }
674         };
675     }
676 }