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