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 android.annotation.Widget;
20import android.content.Context;
21import android.content.res.TypedArray;
22import android.os.Parcel;
23import android.os.Parcelable;
24import android.text.format.DateFormat;
25import android.text.format.DateUtils;
26import android.util.AttributeSet;
27import android.util.SparseArray;
28import android.view.LayoutInflater;
29import android.widget.NumberPicker;
30import android.widget.NumberPicker.OnChangedListener;
31
32import com.android.internal.R;
33
34import java.text.DateFormatSymbols;
35import java.text.SimpleDateFormat;
36import java.util.Calendar;
37import java.util.Locale;
38
39/**
40 * A view for selecting a month / year / day based on a calendar like layout.
41 *
42 * <p>See the <a href="{@docRoot}resources/tutorials/views/hello-datepicker.html">Date Picker
43 * tutorial</a>.</p>
44 *
45 * For a dialog using this view, see {@link android.app.DatePickerDialog}.
46 */
47@Widget
48public class DatePicker extends FrameLayout {
49
50    private static final int DEFAULT_START_YEAR = 1900;
51    private static final int DEFAULT_END_YEAR = 2100;
52
53    // This ignores Undecimber, but we only support real Gregorian calendars.
54    private static final int NUMBER_OF_MONTHS = 12;
55
56    /* UI Components */
57    private final NumberPicker mDayPicker;
58    private final NumberPicker mMonthPicker;
59    private final NumberPicker mYearPicker;
60
61    /**
62     * How we notify users the date has changed.
63     */
64    private OnDateChangedListener mOnDateChangedListener;
65
66    private int mDay;
67    private int mMonth;
68    private int mYear;
69
70    private Object mMonthUpdateLock = new Object();
71    private volatile Locale mMonthLocale;
72    private String[] mShortMonths;
73
74    /**
75     * The callback used to indicate the user changes the date.
76     */
77    public interface OnDateChangedListener {
78
79        /**
80         * @param view The view associated with this listener.
81         * @param year The year that was set.
82         * @param monthOfYear The month that was set (0-11) for compatibility
83         *  with {@link java.util.Calendar}.
84         * @param dayOfMonth The day of the month that was set.
85         */
86        void onDateChanged(DatePicker view, int year, int monthOfYear, int dayOfMonth);
87    }
88
89    public DatePicker(Context context) {
90        this(context, null);
91    }
92
93    public DatePicker(Context context, AttributeSet attrs) {
94        this(context, attrs, 0);
95    }
96
97    public DatePicker(Context context, AttributeSet attrs, int defStyle) {
98        super(context, attrs, defStyle);
99
100        LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
101        inflater.inflate(R.layout.date_picker, this, true);
102
103        mDayPicker = (NumberPicker) findViewById(R.id.day);
104        mDayPicker.setFormatter(NumberPicker.TWO_DIGIT_FORMATTER);
105        mDayPicker.setSpeed(100);
106        mDayPicker.setOnChangeListener(new OnChangedListener() {
107            public void onChanged(NumberPicker picker, int oldVal, int newVal) {
108                mDay = newVal;
109                notifyDateChanged();
110            }
111        });
112        mMonthPicker = (NumberPicker) findViewById(R.id.month);
113        mMonthPicker.setFormatter(NumberPicker.TWO_DIGIT_FORMATTER);
114        final String[] months = getShortMonths();
115
116        /*
117         * If the user is in a locale where the month names are numeric,
118         * use just the number instead of the "month" character for
119         * consistency with the other fields.
120         */
121        if (months[0].startsWith("1")) {
122            for (int i = 0; i < months.length; i++) {
123                months[i] = String.valueOf(i + 1);
124            }
125            mMonthPicker.setRange(1, NUMBER_OF_MONTHS);
126        } else {
127            mMonthPicker.setRange(1, NUMBER_OF_MONTHS, months);
128        }
129
130        mMonthPicker.setSpeed(200);
131        mMonthPicker.setOnChangeListener(new OnChangedListener() {
132            public void onChanged(NumberPicker picker, int oldVal, int newVal) {
133
134                /* We display the month 1-12 but store it 0-11 so always
135                 * subtract by one to ensure our internal state is always 0-11
136                 */
137                mMonth = newVal - 1;
138                // Adjust max day of the month
139                adjustMaxDay();
140                notifyDateChanged();
141                updateDaySpinner();
142            }
143        });
144        mYearPicker = (NumberPicker) findViewById(R.id.year);
145        mYearPicker.setSpeed(100);
146        mYearPicker.setOnChangeListener(new OnChangedListener() {
147            public void onChanged(NumberPicker picker, int oldVal, int newVal) {
148                mYear = newVal;
149                // Adjust max day for leap years if needed
150                adjustMaxDay();
151                notifyDateChanged();
152                updateDaySpinner();
153            }
154        });
155
156        // attributes
157        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.DatePicker);
158
159        int mStartYear = a.getInt(R.styleable.DatePicker_startYear, DEFAULT_START_YEAR);
160        int mEndYear = a.getInt(R.styleable.DatePicker_endYear, DEFAULT_END_YEAR);
161        mYearPicker.setRange(mStartYear, mEndYear);
162
163        a.recycle();
164
165        // initialize to current date
166        Calendar cal = Calendar.getInstance();
167        init(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH), null);
168
169        // re-order the number pickers to match the current date format
170        reorderPickers(months);
171
172        if (!isEnabled()) {
173            setEnabled(false);
174        }
175    }
176
177    @Override
178    public void setEnabled(boolean enabled) {
179        super.setEnabled(enabled);
180        mDayPicker.setEnabled(enabled);
181        mMonthPicker.setEnabled(enabled);
182        mYearPicker.setEnabled(enabled);
183    }
184
185    private void reorderPickers(String[] months) {
186        java.text.DateFormat format;
187        String order;
188
189        /*
190         * If the user is in a locale where the medium date format is
191         * still numeric (Japanese and Czech, for example), respect
192         * the date format order setting.  Otherwise, use the order
193         * that the locale says is appropriate for a spelled-out date.
194         */
195
196        if (months[0].startsWith("1")) {
197            format = DateFormat.getDateFormat(getContext());
198        } else {
199            format = DateFormat.getMediumDateFormat(getContext());
200        }
201
202        if (format instanceof SimpleDateFormat) {
203            order = ((SimpleDateFormat) format).toPattern();
204        } else {
205            // Shouldn't happen, but just in case.
206            order = new String(DateFormat.getDateFormatOrder(getContext()));
207        }
208
209        /* Remove the 3 pickers from their parent and then add them back in the
210         * required order.
211         */
212        LinearLayout parent = (LinearLayout) findViewById(R.id.parent);
213        parent.removeAllViews();
214
215        boolean quoted = false;
216        boolean didDay = false, didMonth = false, didYear = false;
217
218        for (int i = 0; i < order.length(); i++) {
219            char c = order.charAt(i);
220
221            if (c == '\'') {
222                quoted = !quoted;
223            }
224
225            if (!quoted) {
226                if (c == DateFormat.DATE && !didDay) {
227                    parent.addView(mDayPicker);
228                    didDay = true;
229                } else if ((c == DateFormat.MONTH || c == 'L') && !didMonth) {
230                    parent.addView(mMonthPicker);
231                    didMonth = true;
232                } else if (c == DateFormat.YEAR && !didYear) {
233                    parent.addView (mYearPicker);
234                    didYear = true;
235                }
236            }
237        }
238
239        // Shouldn't happen, but just in case.
240        if (!didMonth) {
241            parent.addView(mMonthPicker);
242        }
243        if (!didDay) {
244            parent.addView(mDayPicker);
245        }
246        if (!didYear) {
247            parent.addView(mYearPicker);
248        }
249    }
250
251    public void updateDate(int year, int monthOfYear, int dayOfMonth) {
252        if (mYear != year || mMonth != monthOfYear || mDay != dayOfMonth) {
253            mYear = year;
254            mMonth = monthOfYear;
255            mDay = dayOfMonth;
256            updateSpinners();
257            reorderPickers(getShortMonths());
258            notifyDateChanged();
259        }
260    }
261
262    private String[] getShortMonths() {
263        final Locale currentLocale = Locale.getDefault();
264        if (currentLocale.equals(mMonthLocale) && mShortMonths != null) {
265            return mShortMonths;
266        } else {
267            synchronized (mMonthUpdateLock) {
268                if (!currentLocale.equals(mMonthLocale)) {
269                    mShortMonths = new String[NUMBER_OF_MONTHS];
270                    for (int i = 0; i < NUMBER_OF_MONTHS; i++) {
271                        mShortMonths[i] = DateUtils.getMonthString(Calendar.JANUARY + i,
272                                DateUtils.LENGTH_MEDIUM);
273                    }
274                    mMonthLocale = currentLocale;
275                }
276            }
277            return mShortMonths;
278        }
279    }
280
281    private static class SavedState extends BaseSavedState {
282
283        private final int mYear;
284        private final int mMonth;
285        private final int mDay;
286
287        /**
288         * Constructor called from {@link DatePicker#onSaveInstanceState()}
289         */
290        private SavedState(Parcelable superState, int year, int month, int day) {
291            super(superState);
292            mYear = year;
293            mMonth = month;
294            mDay = day;
295        }
296
297        /**
298         * Constructor called from {@link #CREATOR}
299         */
300        private SavedState(Parcel in) {
301            super(in);
302            mYear = in.readInt();
303            mMonth = in.readInt();
304            mDay = in.readInt();
305        }
306
307        public int getYear() {
308            return mYear;
309        }
310
311        public int getMonth() {
312            return mMonth;
313        }
314
315        public int getDay() {
316            return mDay;
317        }
318
319        @Override
320        public void writeToParcel(Parcel dest, int flags) {
321            super.writeToParcel(dest, flags);
322            dest.writeInt(mYear);
323            dest.writeInt(mMonth);
324            dest.writeInt(mDay);
325        }
326
327        public static final Parcelable.Creator<SavedState> CREATOR =
328                new Creator<SavedState>() {
329
330                    public SavedState createFromParcel(Parcel in) {
331                        return new SavedState(in);
332                    }
333
334                    public SavedState[] newArray(int size) {
335                        return new SavedState[size];
336                    }
337                };
338    }
339
340
341    /**
342     * Override so we are in complete control of save / restore for this widget.
343     */
344    @Override
345    protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
346        dispatchThawSelfOnly(container);
347    }
348
349    @Override
350    protected Parcelable onSaveInstanceState() {
351        Parcelable superState = super.onSaveInstanceState();
352
353        return new SavedState(superState, mYear, mMonth, mDay);
354    }
355
356    @Override
357    protected void onRestoreInstanceState(Parcelable state) {
358        SavedState ss = (SavedState) state;
359        super.onRestoreInstanceState(ss.getSuperState());
360        mYear = ss.getYear();
361        mMonth = ss.getMonth();
362        mDay = ss.getDay();
363        updateSpinners();
364    }
365
366    /**
367     * Initialize the state.
368     * @param year The initial year.
369     * @param monthOfYear The initial month.
370     * @param dayOfMonth The initial day of the month.
371     * @param onDateChangedListener How user is notified date is changed by user, can be null.
372     */
373    public void init(int year, int monthOfYear, int dayOfMonth,
374            OnDateChangedListener onDateChangedListener) {
375        mYear = year;
376        mMonth = monthOfYear;
377        mDay = dayOfMonth;
378        mOnDateChangedListener = onDateChangedListener;
379        updateSpinners();
380    }
381
382    private void updateSpinners() {
383        updateDaySpinner();
384        mYearPicker.setCurrent(mYear);
385
386        /* The month display uses 1-12 but our internal state stores it
387         * 0-11 so add one when setting the display.
388         */
389        mMonthPicker.setCurrent(mMonth + 1);
390    }
391
392    private void updateDaySpinner() {
393        Calendar cal = Calendar.getInstance();
394        cal.set(mYear, mMonth, mDay);
395        int max = cal.getActualMaximum(Calendar.DAY_OF_MONTH);
396        mDayPicker.setRange(1, max);
397        mDayPicker.setCurrent(mDay);
398    }
399
400    public int getYear() {
401        return mYear;
402    }
403
404    public int getMonth() {
405        return mMonth;
406    }
407
408    public int getDayOfMonth() {
409        return mDay;
410    }
411
412    private void adjustMaxDay(){
413        Calendar cal = Calendar.getInstance();
414        cal.set(Calendar.YEAR, mYear);
415        cal.set(Calendar.MONTH, mMonth);
416        int max = cal.getActualMaximum(Calendar.DAY_OF_MONTH);
417        if (mDay > max) {
418            mDay = max;
419        }
420    }
421
422    private void notifyDateChanged() {
423        if (mOnDateChangedListener != null) {
424            mOnDateChangedListener.onDateChanged(DatePicker.this, mYear, mMonth, mDay);
425        }
426    }
427}
428