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