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
17package com.android.datetimepicker.date;
18
19import android.animation.ObjectAnimator;
20import android.app.Activity;
21import android.app.DialogFragment;
22import android.content.res.Resources;
23import android.os.Bundle;
24import android.text.format.DateUtils;
25import android.util.Log;
26import android.view.LayoutInflater;
27import android.view.View;
28import android.view.View.OnClickListener;
29import android.view.ViewGroup;
30import android.view.Window;
31import android.view.WindowManager;
32import android.view.animation.AlphaAnimation;
33import android.view.animation.Animation;
34import android.widget.Button;
35import android.widget.LinearLayout;
36import android.widget.TextView;
37
38import com.android.datetimepicker.HapticFeedbackController;
39import com.android.datetimepicker.R;
40import com.android.datetimepicker.Utils;
41import com.android.datetimepicker.date.MonthAdapter.CalendarDay;
42
43import java.text.SimpleDateFormat;
44import java.util.Calendar;
45import java.util.HashSet;
46import java.util.Iterator;
47import java.util.Locale;
48
49/**
50 * Dialog allowing users to select a date.
51 */
52public 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         */
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
133        public void onDateChanged();
134    }
135
136
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     */
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
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
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
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
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
276    public void onResume() {
277        super.onResume();
278        mHapticFeedbackController.start();
279    }
280
281    @Override
282    public void onPause() {
283        super.onPause();
284        mHapticFeedbackController.stop();
285    }
286
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
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
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
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     */
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
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     */
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
419    public Calendar getMaxDate() {
420        return mMaxDate;
421    }
422
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
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
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
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
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
467    private void updatePickers() {
468        Iterator<OnDateChangedListener> iterator = mListeners.iterator();
469        while (iterator.hasNext()) {
470            iterator.next().onDateChanged();
471        }
472    }
473
474
475    @Override
476    public CalendarDay getSelectedDay() {
477        return new CalendarDay(mCalendar);
478    }
479
480    @Override
481    public int getMinYear() {
482        return mMinYear;
483    }
484
485    @Override
486    public int getMaxYear() {
487        return mMaxYear;
488    }
489
490    @Override
491    public int getFirstDayOfWeek() {
492        return mWeekStart;
493    }
494
495    @Override
496    public void registerOnDateChangedListener(OnDateChangedListener listener) {
497        mListeners.add(listener);
498    }
499
500    @Override
501    public void unregisterOnDateChangedListener(OnDateChangedListener listener) {
502        mListeners.remove(listener);
503    }
504
505    @Override
506    public void tryVibrate() {
507        mHapticFeedbackController.tryVibrate();
508    }
509}
510