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