1 /*
2  * Copyright (C) 2013 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.datetimepicker.date;
18 
19 import android.animation.ObjectAnimator;
20 import android.app.Activity;
21 import android.app.DialogFragment;
22 import android.content.res.Resources;
23 import android.os.Bundle;
24 import android.text.format.DateUtils;
25 import android.util.Log;
26 import android.view.LayoutInflater;
27 import android.view.View;
28 import android.view.View.OnClickListener;
29 import android.view.ViewGroup;
30 import android.view.Window;
31 import android.view.WindowManager;
32 import android.view.animation.AlphaAnimation;
33 import android.view.animation.Animation;
34 import android.widget.Button;
35 import android.widget.LinearLayout;
36 import android.widget.TextView;
37 
38 import com.android.datetimepicker.HapticFeedbackController;
39 import com.android.datetimepicker.R;
40 import com.android.datetimepicker.Utils;
41 import com.android.datetimepicker.date.MonthAdapter.CalendarDay;
42 
43 import java.text.SimpleDateFormat;
44 import java.util.Calendar;
45 import java.util.HashSet;
46 import java.util.Iterator;
47 import java.util.Locale;
48 
49 /**
50  * Dialog allowing users to select a date.
51  */
52 public class DatePickerDialog extends DialogFragment implements
53         OnClickListener, DatePickerController {
54 
55     private static final String TAG = "DatePickerDialog";
56 
57     private static final int UNINITIALIZED = -1;
58     private static final int MONTH_AND_DAY_VIEW = 0;
59     private static final int YEAR_VIEW = 1;
60 
61     private static final String KEY_SELECTED_YEAR = "year";
62     private static final String KEY_SELECTED_MONTH = "month";
63     private static final String KEY_SELECTED_DAY = "day";
64     private static final String KEY_LIST_POSITION = "list_position";
65     private static final String KEY_WEEK_START = "week_start";
66     private static final String KEY_YEAR_START = "year_start";
67     private static final String KEY_YEAR_END = "year_end";
68     private static final String KEY_CURRENT_VIEW = "current_view";
69     private static final String KEY_LIST_POSITION_OFFSET = "list_position_offset";
70 
71     private static final int DEFAULT_START_YEAR = 1900;
72     private static final int DEFAULT_END_YEAR = 2100;
73 
74     private static final int ANIMATION_DURATION = 300;
75     private static final int ANIMATION_DELAY = 500;
76 
77     private static SimpleDateFormat YEAR_FORMAT = new SimpleDateFormat("yyyy", Locale.getDefault());
78     private static SimpleDateFormat DAY_FORMAT = new SimpleDateFormat("dd", Locale.getDefault());
79 
80     private final Calendar mCalendar = Calendar.getInstance();
81     private OnDateSetListener mCallBack;
82     private HashSet<OnDateChangedListener> mListeners = new HashSet<OnDateChangedListener>();
83 
84     private AccessibleDateAnimator mAnimator;
85 
86     private TextView mDayOfWeekView;
87     private LinearLayout mMonthAndDayView;
88     private TextView mSelectedMonthTextView;
89     private TextView mSelectedDayTextView;
90     private TextView mYearView;
91     private DayPickerView mDayPickerView;
92     private YearPickerView mYearPickerView;
93     private Button mDoneButton;
94 
95     private int mCurrentView = UNINITIALIZED;
96 
97     private int mWeekStart = mCalendar.getFirstDayOfWeek();
98     private int mMinYear = DEFAULT_START_YEAR;
99     private int mMaxYear = DEFAULT_END_YEAR;
100     private Calendar mMinDate;
101     private Calendar mMaxDate;
102 
103     private HapticFeedbackController mHapticFeedbackController;
104 
105     private boolean mDelayAnimation = true;
106 
107     // Accessibility strings.
108     private String mDayPickerDescription;
109     private String mSelectDay;
110     private String mYearPickerDescription;
111     private String mSelectYear;
112 
113     /**
114      * The callback used to indicate the user is done filling in the date.
115      */
116     public interface OnDateSetListener {
117 
118         /**
119          * @param view The view associated with this listener.
120          * @param year The year that was set.
121          * @param monthOfYear The month that was set (0-11) for compatibility
122          *            with {@link java.util.Calendar}.
123          * @param dayOfMonth The day of the month that was set.
124          */
onDateSet(DatePickerDialog dialog, int year, int monthOfYear, int dayOfMonth)125         void onDateSet(DatePickerDialog dialog, int year, int monthOfYear, int dayOfMonth);
126     }
127 
128     /**
129      * The callback used to notify other date picker components of a change in selected date.
130      */
131     public interface OnDateChangedListener {
132 
onDateChanged()133         public void onDateChanged();
134     }
135 
136 
DatePickerDialog()137     public DatePickerDialog() {
138         // Empty constructor required for dialog fragment.
139     }
140 
141     /**
142      * @param callBack How the parent is notified that the date is set.
143      * @param year The initial year of the dialog.
144      * @param monthOfYear The initial month of the dialog.
145      * @param dayOfMonth The initial day of the dialog.
146      */
newInstance(OnDateSetListener callBack, int year, int monthOfYear, int dayOfMonth)147     public static DatePickerDialog newInstance(OnDateSetListener callBack, int year,
148             int monthOfYear,
149             int dayOfMonth) {
150         DatePickerDialog ret = new DatePickerDialog();
151         ret.initialize(callBack, year, monthOfYear, dayOfMonth);
152         return ret;
153     }
154 
initialize(OnDateSetListener callBack, int year, int monthOfYear, int dayOfMonth)155     public void initialize(OnDateSetListener callBack, int year, int monthOfYear, int dayOfMonth) {
156         mCallBack = callBack;
157         mCalendar.set(Calendar.YEAR, year);
158         mCalendar.set(Calendar.MONTH, monthOfYear);
159         mCalendar.set(Calendar.DAY_OF_MONTH, dayOfMonth);
160     }
161 
162     @Override
onCreate(Bundle savedInstanceState)163     public void onCreate(Bundle savedInstanceState) {
164         super.onCreate(savedInstanceState);
165         final Activity activity = getActivity();
166         activity.getWindow().setSoftInputMode(
167                 WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN);
168         if (savedInstanceState != null) {
169             mCalendar.set(Calendar.YEAR, savedInstanceState.getInt(KEY_SELECTED_YEAR));
170             mCalendar.set(Calendar.MONTH, savedInstanceState.getInt(KEY_SELECTED_MONTH));
171             mCalendar.set(Calendar.DAY_OF_MONTH, savedInstanceState.getInt(KEY_SELECTED_DAY));
172         }
173     }
174 
175     @Override
onSaveInstanceState(Bundle outState)176     public void onSaveInstanceState(Bundle outState) {
177         super.onSaveInstanceState(outState);
178         outState.putInt(KEY_SELECTED_YEAR, mCalendar.get(Calendar.YEAR));
179         outState.putInt(KEY_SELECTED_MONTH, mCalendar.get(Calendar.MONTH));
180         outState.putInt(KEY_SELECTED_DAY, mCalendar.get(Calendar.DAY_OF_MONTH));
181         outState.putInt(KEY_WEEK_START, mWeekStart);
182         outState.putInt(KEY_YEAR_START, mMinYear);
183         outState.putInt(KEY_YEAR_END, mMaxYear);
184         outState.putInt(KEY_CURRENT_VIEW, mCurrentView);
185         int listPosition = -1;
186         if (mCurrentView == MONTH_AND_DAY_VIEW) {
187             listPosition = mDayPickerView.getMostVisiblePosition();
188         } else if (mCurrentView == YEAR_VIEW) {
189             listPosition = mYearPickerView.getFirstVisiblePosition();
190             outState.putInt(KEY_LIST_POSITION_OFFSET, mYearPickerView.getFirstPositionOffset());
191         }
192         outState.putInt(KEY_LIST_POSITION, listPosition);
193     }
194 
195     @Override
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)196     public View onCreateView(LayoutInflater inflater, ViewGroup container,
197             Bundle savedInstanceState) {
198         Log.d(TAG, "onCreateView: ");
199         getDialog().getWindow().requestFeature(Window.FEATURE_NO_TITLE);
200 
201         View view = inflater.inflate(R.layout.date_picker_dialog, null);
202 
203         mDayOfWeekView = (TextView) view.findViewById(R.id.date_picker_header);
204         mMonthAndDayView = (LinearLayout) view.findViewById(R.id.date_picker_month_and_day);
205         mMonthAndDayView.setOnClickListener(this);
206         mSelectedMonthTextView = (TextView) view.findViewById(R.id.date_picker_month);
207         mSelectedDayTextView = (TextView) view.findViewById(R.id.date_picker_day);
208         mYearView = (TextView) view.findViewById(R.id.date_picker_year);
209         mYearView.setOnClickListener(this);
210 
211         int listPosition = -1;
212         int listPositionOffset = 0;
213         int currentView = MONTH_AND_DAY_VIEW;
214         if (savedInstanceState != null) {
215             mWeekStart = savedInstanceState.getInt(KEY_WEEK_START);
216             mMinYear = savedInstanceState.getInt(KEY_YEAR_START);
217             mMaxYear = savedInstanceState.getInt(KEY_YEAR_END);
218             currentView = savedInstanceState.getInt(KEY_CURRENT_VIEW);
219             listPosition = savedInstanceState.getInt(KEY_LIST_POSITION);
220             listPositionOffset = savedInstanceState.getInt(KEY_LIST_POSITION_OFFSET);
221         }
222 
223         final Activity activity = getActivity();
224         mDayPickerView = new SimpleDayPickerView(activity, this);
225         mYearPickerView = new YearPickerView(activity, this);
226 
227         Resources res = getResources();
228         mDayPickerDescription = res.getString(R.string.day_picker_description);
229         mSelectDay = res.getString(R.string.select_day);
230         mYearPickerDescription = res.getString(R.string.year_picker_description);
231         mSelectYear = res.getString(R.string.select_year);
232 
233         mAnimator = (AccessibleDateAnimator) view.findViewById(R.id.animator);
234         mAnimator.addView(mDayPickerView);
235         mAnimator.addView(mYearPickerView);
236         mAnimator.setDateMillis(mCalendar.getTimeInMillis());
237         // TODO: Replace with animation decided upon by the design team.
238         Animation animation = new AlphaAnimation(0.0f, 1.0f);
239         animation.setDuration(ANIMATION_DURATION);
240         mAnimator.setInAnimation(animation);
241         // TODO: Replace with animation decided upon by the design team.
242         Animation animation2 = new AlphaAnimation(1.0f, 0.0f);
243         animation2.setDuration(ANIMATION_DURATION);
244         mAnimator.setOutAnimation(animation2);
245 
246         mDoneButton = (Button) view.findViewById(R.id.done);
247         mDoneButton.setOnClickListener(new OnClickListener() {
248 
249             @Override
250             public void onClick(View v) {
251                 tryVibrate();
252                 if (mCallBack != null) {
253                     mCallBack.onDateSet(DatePickerDialog.this, mCalendar.get(Calendar.YEAR),
254                             mCalendar.get(Calendar.MONTH), mCalendar.get(Calendar.DAY_OF_MONTH));
255                 }
256                 dismiss();
257             }
258         });
259 
260         updateDisplay(false);
261         setCurrentView(currentView);
262 
263         if (listPosition != -1) {
264             if (currentView == MONTH_AND_DAY_VIEW) {
265                 mDayPickerView.postSetSelection(listPosition);
266             } else if (currentView == YEAR_VIEW) {
267                 mYearPickerView.postSetSelectionFromTop(listPosition, listPositionOffset);
268             }
269         }
270 
271         mHapticFeedbackController = new HapticFeedbackController(activity);
272         return view;
273     }
274 
275     @Override
onResume()276     public void onResume() {
277         super.onResume();
278         mHapticFeedbackController.start();
279     }
280 
281     @Override
onPause()282     public void onPause() {
283         super.onPause();
284         mHapticFeedbackController.stop();
285     }
286 
setCurrentView(final int viewIndex)287     private void setCurrentView(final int viewIndex) {
288         long millis = mCalendar.getTimeInMillis();
289 
290         switch (viewIndex) {
291             case MONTH_AND_DAY_VIEW:
292                 ObjectAnimator pulseAnimator = Utils.getPulseAnimator(mMonthAndDayView, 0.9f,
293                         1.05f);
294                 if (mDelayAnimation) {
295                     pulseAnimator.setStartDelay(ANIMATION_DELAY);
296                     mDelayAnimation = false;
297                 }
298                 mDayPickerView.onDateChanged();
299                 if (mCurrentView != viewIndex) {
300                     mMonthAndDayView.setSelected(true);
301                     mYearView.setSelected(false);
302                     mAnimator.setDisplayedChild(MONTH_AND_DAY_VIEW);
303                     mCurrentView = viewIndex;
304                 }
305                 pulseAnimator.start();
306 
307                 int flags = DateUtils.FORMAT_SHOW_DATE;
308                 String dayString = DateUtils.formatDateTime(getActivity(), millis, flags);
309                 mAnimator.setContentDescription(mDayPickerDescription+": "+dayString);
310                 Utils.tryAccessibilityAnnounce(mAnimator, mSelectDay);
311                 break;
312             case YEAR_VIEW:
313                 pulseAnimator = Utils.getPulseAnimator(mYearView, 0.85f, 1.1f);
314                 if (mDelayAnimation) {
315                     pulseAnimator.setStartDelay(ANIMATION_DELAY);
316                     mDelayAnimation = false;
317                 }
318                 mYearPickerView.onDateChanged();
319                 if (mCurrentView != viewIndex) {
320                     mMonthAndDayView.setSelected(false);
321                     mYearView.setSelected(true);
322                     mAnimator.setDisplayedChild(YEAR_VIEW);
323                     mCurrentView = viewIndex;
324                 }
325                 pulseAnimator.start();
326 
327                 CharSequence yearString = YEAR_FORMAT.format(millis);
328                 mAnimator.setContentDescription(mYearPickerDescription+": "+yearString);
329                 Utils.tryAccessibilityAnnounce(mAnimator, mSelectYear);
330                 break;
331         }
332     }
333 
updateDisplay(boolean announce)334     private void updateDisplay(boolean announce) {
335         if (mDayOfWeekView != null) {
336             mDayOfWeekView.setText(mCalendar.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.LONG,
337                     Locale.getDefault()).toUpperCase(Locale.getDefault()));
338         }
339 
340         mSelectedMonthTextView.setText(mCalendar.getDisplayName(Calendar.MONTH, Calendar.SHORT,
341                 Locale.getDefault()).toUpperCase(Locale.getDefault()));
342         mSelectedDayTextView.setText(DAY_FORMAT.format(mCalendar.getTime()));
343         mYearView.setText(YEAR_FORMAT.format(mCalendar.getTime()));
344 
345         // Accessibility.
346         long millis = mCalendar.getTimeInMillis();
347         mAnimator.setDateMillis(millis);
348         int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NO_YEAR;
349         String monthAndDayText = DateUtils.formatDateTime(getActivity(), millis, flags);
350         mMonthAndDayView.setContentDescription(monthAndDayText);
351 
352         if (announce) {
353             flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR;
354             String fullDateText = DateUtils.formatDateTime(getActivity(), millis, flags);
355             Utils.tryAccessibilityAnnounce(mAnimator, fullDateText);
356         }
357     }
358 
setFirstDayOfWeek(int startOfWeek)359     public void setFirstDayOfWeek(int startOfWeek) {
360         if (startOfWeek < Calendar.SUNDAY || startOfWeek > Calendar.SATURDAY) {
361             throw new IllegalArgumentException("Value must be between Calendar.SUNDAY and " +
362                     "Calendar.SATURDAY");
363         }
364         mWeekStart = startOfWeek;
365         if (mDayPickerView != null) {
366             mDayPickerView.onChange();
367         }
368     }
369 
setYearRange(int startYear, int endYear)370     public void setYearRange(int startYear, int endYear) {
371         if (endYear <= startYear) {
372             throw new IllegalArgumentException("Year end must be larger than year start");
373         }
374         mMinYear = startYear;
375         mMaxYear = endYear;
376         if (mDayPickerView != null) {
377             mDayPickerView.onChange();
378         }
379     }
380 
381     /**
382      * Sets the minimal date supported by this DatePicker. Dates before (but not including) the
383      * specified date will be disallowed from being selected.
384      * @param calendar a Calendar object set to the year, month, day desired as the mindate.
385      */
setMinDate(Calendar calendar)386     public void setMinDate(Calendar calendar) {
387         mMinDate = calendar;
388 
389         if (mDayPickerView != null) {
390             mDayPickerView.onChange();
391         }
392     }
393 
394     /**
395      * @return The minimal date supported by this DatePicker. Null if it has not been set.
396      */
397     @Override
getMinDate()398     public Calendar getMinDate() {
399         return mMinDate;
400     }
401 
402     /**
403      * Sets the minimal date supported by this DatePicker. Dates after (but not including) the
404      * specified date will be disallowed from being selected.
405      * @param calendar a Calendar object set to the year, month, day desired as the maxdate.
406      */
setMaxDate(Calendar calendar)407     public void setMaxDate(Calendar calendar) {
408         mMaxDate = calendar;
409 
410         if (mDayPickerView != null) {
411             mDayPickerView.onChange();
412         }
413     }
414 
415     /**
416      * @return The maximal date supported by this DatePicker. Null if it has not been set.
417      */
418     @Override
getMaxDate()419     public Calendar getMaxDate() {
420         return mMaxDate;
421     }
422 
setOnDateSetListener(OnDateSetListener listener)423     public void setOnDateSetListener(OnDateSetListener listener) {
424         mCallBack = listener;
425     }
426 
427     // If the newly selected month / year does not contain the currently selected day number,
428     // change the selected day number to the last day of the selected month or year.
429     //      e.g. Switching from Mar to Apr when Mar 31 is selected -> Apr 30
430     //      e.g. Switching from 2012 to 2013 when Feb 29, 2012 is selected -> Feb 28, 2013
adjustDayInMonthIfNeeded(int month, int year)431     private void adjustDayInMonthIfNeeded(int month, int year) {
432         int day = mCalendar.get(Calendar.DAY_OF_MONTH);
433         int daysInMonth = Utils.getDaysInMonth(month, year);
434         if (day > daysInMonth) {
435             mCalendar.set(Calendar.DAY_OF_MONTH, daysInMonth);
436         }
437     }
438 
439     @Override
onClick(View v)440     public void onClick(View v) {
441         tryVibrate();
442         if (v.getId() == R.id.date_picker_year) {
443             setCurrentView(YEAR_VIEW);
444         } else if (v.getId() == R.id.date_picker_month_and_day) {
445             setCurrentView(MONTH_AND_DAY_VIEW);
446         }
447     }
448 
449     @Override
onYearSelected(int year)450     public void onYearSelected(int year) {
451         adjustDayInMonthIfNeeded(mCalendar.get(Calendar.MONTH), year);
452         mCalendar.set(Calendar.YEAR, year);
453         updatePickers();
454         setCurrentView(MONTH_AND_DAY_VIEW);
455         updateDisplay(true);
456     }
457 
458     @Override
onDayOfMonthSelected(int year, int month, int day)459     public void onDayOfMonthSelected(int year, int month, int day) {
460         mCalendar.set(Calendar.YEAR, year);
461         mCalendar.set(Calendar.MONTH, month);
462         mCalendar.set(Calendar.DAY_OF_MONTH, day);
463         updatePickers();
464         updateDisplay(true);
465     }
466 
updatePickers()467     private void updatePickers() {
468         Iterator<OnDateChangedListener> iterator = mListeners.iterator();
469         while (iterator.hasNext()) {
470             iterator.next().onDateChanged();
471         }
472     }
473 
474 
475     @Override
getSelectedDay()476     public CalendarDay getSelectedDay() {
477         return new CalendarDay(mCalendar);
478     }
479 
480     @Override
getMinYear()481     public int getMinYear() {
482         return mMinYear;
483     }
484 
485     @Override
getMaxYear()486     public int getMaxYear() {
487         return mMaxYear;
488     }
489 
490     @Override
getFirstDayOfWeek()491     public int getFirstDayOfWeek() {
492         return mWeekStart;
493     }
494 
495     @Override
registerOnDateChangedListener(OnDateChangedListener listener)496     public void registerOnDateChangedListener(OnDateChangedListener listener) {
497         mListeners.add(listener);
498     }
499 
500     @Override
unregisterOnDateChangedListener(OnDateChangedListener listener)501     public void unregisterOnDateChangedListener(OnDateChangedListener listener) {
502         mListeners.remove(listener);
503     }
504 
505     @Override
tryVibrate()506     public void tryVibrate() {
507         mHapticFeedbackController.tryVibrate();
508     }
509 }
510