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