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 import android.animation.LayoutTransition;
20 import android.content.Context;
21 import android.os.Parcel;
22 import android.os.Parcelable;
23 import android.text.format.DateFormat;
24 import android.util.AttributeSet;
25 import android.util.SparseArray;
26 import android.view.LayoutInflater;
27 import android.view.View;
28 import android.widget.CheckBox;
29 import android.widget.CompoundButton;
30 import android.widget.CompoundButton.OnCheckedChangeListener;
31 import android.widget.FrameLayout;
32 import android.widget.LinearLayout;
33 import android.widget.NumberPicker;
34 import android.widget.NumberPicker.OnValueChangeListener;
35 
36 import com.android.contacts.R;
37 
38 import java.text.DateFormatSymbols;
39 import java.util.Calendar;
40 import 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  */
53 public 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          */
onDateChanged(DatePicker view, int year, int monthOfYear, int dayOfMonth)91         void onDateChanged(DatePicker view, int year, int monthOfYear, int dayOfMonth);
92     }
93 
DatePicker(Context context)94     public DatePicker(Context context) {
95         this(context, null);
96     }
97 
DatePicker(Context context, AttributeSet attrs)98     public DatePicker(Context context, AttributeSet attrs) {
99         this(context, attrs, 0);
100     }
101 
DatePicker(Context context, AttributeSet attrs, int defStyle)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
setEnabled(boolean enabled)197     public void setEnabled(boolean enabled) {
198         super.setEnabled(enabled);
199         mDayPicker.setEnabled(enabled);
200         mMonthPicker.setEnabled(enabled);
201         mYearPicker.setEnabled(enabled);
202     }
203 
reorderPickers()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 
updateDate(int year, int monthOfYear, int dayOfMonth)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 
getCurrentYear()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          */
SavedState(Parcelable superState, int year, int month, int day, boolean hasYear, boolean yearOptional)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          */
SavedState(Parcel in)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 
getYear()277         public int getYear() {
278             return mYear;
279         }
280 
getMonth()281         public int getMonth() {
282             return mMonth;
283         }
284 
getDay()285         public int getDay() {
286             return mDay;
287         }
288 
hasYear()289         public boolean hasYear() {
290             return mHasYear;
291         }
292 
isYearOptional()293         public boolean isYearOptional() {
294             return mYearOptional;
295         }
296 
297         @Override
writeToParcel(Parcel dest, int flags)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
dispatchRestoreInstanceState(SparseArray<Parcelable> container)328     protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
329         dispatchThawSelfOnly(container);
330     }
331 
332     @Override
onSaveInstanceState()333     protected Parcelable onSaveInstanceState() {
334         Parcelable superState = super.onSaveInstanceState();
335 
336         return new SavedState(superState, mYear, mMonth, mDay, mHasYear, mYearOptional);
337     }
338 
339     @Override
onRestoreInstanceState(Parcelable state)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      */
init(int year, int monthOfYear, int dayOfMonth, OnDateChangedListener onDateChangedListener)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      */
init(int year, int monthOfYear, int dayOfMonth, boolean yearOptional, OnDateChangedListener onDateChangedListener)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 
updateSpinners()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 
updateDaySpinner()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 
getYear()405     public int getYear() {
406         return (mYearOptional && !mHasYear) ? NO_YEAR : mYear;
407     }
408 
isYearOptional()409     public boolean isYearOptional() {
410         return mYearOptional;
411     }
412 
getMonth()413     public int getMonth() {
414         return mMonth;
415     }
416 
getDayOfMonth()417     public int getDayOfMonth() {
418         return mDay;
419     }
420 
adjustMaxDay()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 
notifyDateChanged()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