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