DatePickerCalendarDelegate.java revision 518ff0de95e64116ecb07706fc564d4c19197ca7
1/*
2 * Copyright (C) 2014 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 android.widget;
18
19import android.animation.Keyframe;
20import android.animation.ObjectAnimator;
21import android.animation.PropertyValuesHolder;
22import android.content.Context;
23import android.content.res.ColorStateList;
24import android.content.res.Configuration;
25import android.content.res.Resources;
26import android.content.res.TypedArray;
27import android.graphics.Color;
28import android.os.Parcel;
29import android.os.Parcelable;
30import android.text.format.DateFormat;
31import android.text.format.DateUtils;
32import android.util.AttributeSet;
33import android.util.SparseArray;
34import android.view.HapticFeedbackConstants;
35import android.view.LayoutInflater;
36import android.view.View;
37import android.view.accessibility.AccessibilityEvent;
38import android.view.accessibility.AccessibilityNodeInfo;
39import android.view.animation.AlphaAnimation;
40import android.view.animation.Animation;
41
42import com.android.internal.R;
43import com.android.internal.widget.AccessibleDateAnimator;
44
45import java.text.SimpleDateFormat;
46import java.util.Calendar;
47import java.util.HashSet;
48import java.util.Locale;
49
50/**
51 * A delegate for picking up a date (day / month / year).
52 */
53class DatePickerCalendarDelegate extends DatePicker.AbstractDatePickerDelegate implements
54        View.OnClickListener, DatePickerController {
55
56    private static final int UNINITIALIZED = -1;
57    private static final int MONTH_AND_DAY_VIEW = 0;
58    private static final int YEAR_VIEW = 1;
59
60    private static final int DEFAULT_START_YEAR = 1900;
61    private static final int DEFAULT_END_YEAR = 2100;
62
63    private static final int PULSE_ANIMATOR_DURATION = 544;
64
65    private static final int ANIMATION_DURATION = 300;
66    private static final int ANIMATION_DELAY = 650;
67
68    private static final int MONTH_INDEX = 0;
69    private static final int DAY_INDEX = 1;
70    private static final int YEAR_INDEX = 2;
71
72    private SimpleDateFormat mYearFormat = new SimpleDateFormat("y", Locale.getDefault());
73    private SimpleDateFormat mDayFormat = new SimpleDateFormat("d", Locale.getDefault());
74
75    private TextView mDayOfWeekView;
76    private LinearLayout mDateLayout;
77    private LinearLayout mMonthAndDayLayout;
78    private TextView mHeaderMonthTextView;
79    private TextView mHeaderDayOfMonthTextView;
80    private TextView mHeaderYearTextView;
81    private DayPickerView mDayPickerView;
82    private YearPickerView mYearPickerView;
83
84    private boolean mIsEnabled = true;
85
86    // Accessibility strings.
87    private String mDayPickerDescription;
88    private String mSelectDay;
89    private String mYearPickerDescription;
90    private String mSelectYear;
91
92    private AccessibleDateAnimator mAnimator;
93
94    private DatePicker.OnDateChangedListener mDateChangedListener;
95
96    private boolean mDelayAnimation = true;
97
98    private int mCurrentView = UNINITIALIZED;
99
100    private Calendar mCurrentDate;
101    private Calendar mTempDate;
102    private Calendar mMinDate;
103    private Calendar mMaxDate;
104
105    private HashSet<OnDateChangedListener> mListeners = new HashSet<OnDateChangedListener>();
106
107    public DatePickerCalendarDelegate(DatePicker delegator, Context context, AttributeSet attrs,
108            int defStyleAttr, int defStyleRes) {
109        super(delegator, context);
110
111        final Locale locale = Locale.getDefault();
112        mMinDate = getCalendarForLocale(mMinDate, locale);
113        mMaxDate = getCalendarForLocale(mMaxDate, locale);
114        mTempDate = getCalendarForLocale(mMaxDate, locale);
115
116        mCurrentDate = getCalendarForLocale(mCurrentDate, locale);
117
118        mMinDate.set(DEFAULT_START_YEAR, 1, 1);
119        mMaxDate.set(DEFAULT_END_YEAR, 12, 31);
120
121        final Resources res = mDelegator.getResources();
122        final TypedArray a = mContext.obtainStyledAttributes(attrs,
123                R.styleable.DatePicker, defStyleAttr, defStyleRes);
124        final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
125                Context.LAYOUT_INFLATER_SERVICE);
126        final int layoutResourceId = a.getResourceId(
127                R.styleable.DatePicker_internalLayout, R.layout.date_picker_holo);
128        final View mainView = inflater.inflate(layoutResourceId, null);
129        mDelegator.addView(mainView);
130
131        mDayOfWeekView = (TextView) mainView.findViewById(R.id.date_picker_header);
132        mDateLayout = (LinearLayout) mainView.findViewById(R.id.day_picker_selector_layout);
133        mMonthAndDayLayout = (LinearLayout) mainView.findViewById(
134                R.id.date_picker_month_and_day_layout);
135        mMonthAndDayLayout.setOnClickListener(this);
136        mHeaderMonthTextView = (TextView) mainView.findViewById(R.id.date_picker_month);
137        mHeaderDayOfMonthTextView = (TextView) mainView.findViewById(R.id.date_picker_day);
138        mHeaderYearTextView = (TextView) mainView.findViewById(R.id.date_picker_year);
139        mHeaderYearTextView.setOnClickListener(this);
140
141        // Obtain default highlight color from the theme.
142        final int defaultHighlightColor = mHeaderYearTextView.getHighlightColor();
143
144        // Use Theme attributes if possible
145        final int dayOfWeekTextAppearanceResId = a.getResourceId(
146                R.styleable.DatePicker_dayOfWeekTextAppearance, -1);
147        if (dayOfWeekTextAppearanceResId != -1) {
148            mDayOfWeekView.setTextAppearance(context, dayOfWeekTextAppearanceResId);
149        }
150
151        final int dayOfWeekBackgroundColor = a.getColor(
152                R.styleable.DatePicker_dayOfWeekBackgroundColor, Color.TRANSPARENT);
153        mDayOfWeekView.setBackgroundColor(dayOfWeekBackgroundColor);
154
155        final int headerSelectedTextColor = a.getColor(
156                R.styleable.DatePicker_headerSelectedTextColor, defaultHighlightColor);
157        final int headerBackgroundColor = a.getColor(R.styleable.DatePicker_headerBackgroundColor,
158                Color.TRANSPARENT);
159        mDateLayout.setBackgroundColor(headerBackgroundColor);
160
161        final int monthTextAppearanceResId = a.getResourceId(
162                R.styleable.DatePicker_headerMonthTextAppearance, -1);
163        if (monthTextAppearanceResId != -1) {
164            mHeaderMonthTextView.setTextAppearance(context, monthTextAppearanceResId);
165        }
166        mHeaderMonthTextView.setTextColor(ColorStateList.addFirstIfMissing(
167                mHeaderMonthTextView.getTextColors(), R.attr.state_selected,
168                headerSelectedTextColor));
169
170        final int dayOfMonthTextAppearanceResId = a.getResourceId(
171                R.styleable.DatePicker_headerDayOfMonthTextAppearance, -1);
172        if (dayOfMonthTextAppearanceResId != -1) {
173            mHeaderDayOfMonthTextView.setTextAppearance(context, dayOfMonthTextAppearanceResId);
174        }
175        mHeaderDayOfMonthTextView.setTextColor(ColorStateList.addFirstIfMissing(
176                mHeaderDayOfMonthTextView.getTextColors(), R.attr.state_selected,
177                headerSelectedTextColor));
178
179        final int yearTextAppearanceResId = a.getResourceId(
180                R.styleable.DatePicker_headerYearTextAppearance, -1);
181        if (yearTextAppearanceResId != -1) {
182            mHeaderYearTextView.setTextAppearance(context, yearTextAppearanceResId);
183        }
184        mHeaderYearTextView.setTextColor(ColorStateList.addFirstIfMissing(
185                mHeaderYearTextView.getTextColors(), R.attr.state_selected,
186                headerSelectedTextColor));
187
188        mDayPickerView = new DayPickerView(mContext, this);
189        mYearPickerView = new YearPickerView(mContext);
190        mYearPickerView.init(this);
191
192        final ColorStateList calendarTextColor = a.getColorStateList(
193                R.styleable.DatePicker_calendarTextColor);
194        final int calendarSelectedTextColor = a.getColor(
195                R.styleable.DatePicker_calendarSelectedTextColor, defaultHighlightColor);
196        mDayPickerView.setCalendarTextColor(ColorStateList.addFirstIfMissing(
197                calendarTextColor, R.attr.state_selected, calendarSelectedTextColor));
198
199        mDayPickerDescription = res.getString(R.string.day_picker_description);
200        mSelectDay = res.getString(R.string.select_day);
201        mYearPickerDescription = res.getString(R.string.year_picker_description);
202        mSelectYear = res.getString(R.string.select_year);
203
204        mAnimator = (AccessibleDateAnimator) mainView.findViewById(R.id.animator);
205        mAnimator.addView(mDayPickerView);
206        mAnimator.addView(mYearPickerView);
207        mAnimator.setDateMillis(mCurrentDate.getTimeInMillis());
208        Animation animation = new AlphaAnimation(0.0f, 1.0f);
209        animation.setDuration(ANIMATION_DURATION);
210        mAnimator.setInAnimation(animation);
211        Animation animation2 = new AlphaAnimation(1.0f, 0.0f);
212        animation2.setDuration(ANIMATION_DURATION);
213        mAnimator.setOutAnimation(animation2);
214
215        updateDisplay(false);
216        setCurrentView(MONTH_AND_DAY_VIEW);
217    }
218
219    /**
220     * Gets a calendar for locale bootstrapped with the value of a given calendar.
221     *
222     * @param oldCalendar The old calendar.
223     * @param locale The locale.
224     */
225    private Calendar getCalendarForLocale(Calendar oldCalendar, Locale locale) {
226        if (oldCalendar == null) {
227            return Calendar.getInstance(locale);
228        } else {
229            final long currentTimeMillis = oldCalendar.getTimeInMillis();
230            Calendar newCalendar = Calendar.getInstance(locale);
231            newCalendar.setTimeInMillis(currentTimeMillis);
232            return newCalendar;
233        }
234    }
235
236    /**
237     * Compute the array representing the order of Month / Day / Year views in their layout.
238     * Will be used for I18N purpose as the order of them depends on the Locale.
239     */
240    private int[] getMonthDayYearIndexes(String pattern) {
241        int[] result = new int[3];
242
243        final String filteredPattern = pattern.replaceAll("'.*?'", "");
244
245        final int dayIndex = filteredPattern.indexOf('d');
246        final int monthMIndex = filteredPattern.indexOf("M");
247        final int monthIndex = (monthMIndex != -1) ? monthMIndex : filteredPattern.indexOf("L");
248        final int yearIndex = filteredPattern.indexOf("y");
249
250        if (yearIndex < monthIndex) {
251            result[YEAR_INDEX] = 0;
252
253            if (monthIndex < dayIndex) {
254                result[MONTH_INDEX] = 1;
255                result[DAY_INDEX] = 2;
256            } else {
257                result[MONTH_INDEX] = 2;
258                result[DAY_INDEX] = 1;
259            }
260        } else {
261            result[YEAR_INDEX] = 2;
262
263            if (monthIndex < dayIndex) {
264                result[MONTH_INDEX] = 0;
265                result[DAY_INDEX] = 1;
266            } else {
267                result[MONTH_INDEX] = 1;
268                result[DAY_INDEX] = 0;
269            }
270        }
271        return result;
272    }
273
274    private void updateDisplay(boolean announce) {
275        if (mDayOfWeekView != null) {
276            mDayOfWeekView.setText(mCurrentDate.getDisplayName(Calendar.DAY_OF_WEEK, Calendar.LONG,
277                    Locale.getDefault()));
278        }
279        final String bestDateTimePattern =
280                DateFormat.getBestDateTimePattern(mCurrentLocale, "yMMMd");
281
282        // Compute indices of Month, Day and Year views
283        int[] viewIndices = getMonthDayYearIndexes(bestDateTimePattern);
284
285        // Restart from a clean state
286        mMonthAndDayLayout.removeAllViews();
287        mDateLayout.removeView(mHeaderYearTextView);
288
289        // Position the Year View at the correct location
290        if (viewIndices[YEAR_INDEX] == 0) {
291            mDateLayout.addView(mHeaderYearTextView, 1);
292        } else {
293            mDateLayout.addView(mHeaderYearTextView, 2);
294        }
295
296        // Position Day and Month Views
297        if (viewIndices[MONTH_INDEX] > viewIndices[DAY_INDEX]) {
298            // Day View is first
299            mMonthAndDayLayout.addView(mHeaderDayOfMonthTextView);
300            mMonthAndDayLayout.addView(mHeaderMonthTextView);
301        } else {
302            // Month View is first
303            mMonthAndDayLayout.addView(mHeaderMonthTextView);
304            mMonthAndDayLayout.addView(mHeaderDayOfMonthTextView);
305        }
306
307        mHeaderMonthTextView.setText(mCurrentDate.getDisplayName(Calendar.MONTH, Calendar.SHORT,
308                Locale.getDefault()).toUpperCase(Locale.getDefault()));
309        mHeaderDayOfMonthTextView.setText(mDayFormat.format(mCurrentDate.getTime()));
310        mHeaderYearTextView.setText(mYearFormat.format(mCurrentDate.getTime()));
311
312        // Accessibility.
313        long millis = mCurrentDate.getTimeInMillis();
314        mAnimator.setDateMillis(millis);
315        int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NO_YEAR;
316        String monthAndDayText = DateUtils.formatDateTime(mContext, millis, flags);
317        mMonthAndDayLayout.setContentDescription(monthAndDayText);
318
319        if (announce) {
320            flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR;
321            String fullDateText = DateUtils.formatDateTime(mContext, millis, flags);
322            mAnimator.announceForAccessibility(fullDateText);
323        }
324        updatePickers();
325    }
326
327    private void setCurrentView(final int viewIndex) {
328        long millis = mCurrentDate.getTimeInMillis();
329
330        switch (viewIndex) {
331            case MONTH_AND_DAY_VIEW:
332                ObjectAnimator pulseAnimator = getPulseAnimator(mMonthAndDayLayout, 0.9f,
333                        1.05f);
334                if (mDelayAnimation) {
335                    pulseAnimator.setStartDelay(ANIMATION_DELAY);
336                    mDelayAnimation = false;
337                }
338                mDayPickerView.onDateChanged();
339                if (mCurrentView != viewIndex) {
340                    mMonthAndDayLayout.setSelected(true);
341                    mHeaderYearTextView.setSelected(false);
342                    mAnimator.setDisplayedChild(MONTH_AND_DAY_VIEW);
343                    mCurrentView = viewIndex;
344                }
345                pulseAnimator.start();
346
347                int flags = DateUtils.FORMAT_SHOW_DATE;
348                String dayString = DateUtils.formatDateTime(mContext, millis, flags);
349                mAnimator.setContentDescription(mDayPickerDescription + ": " + dayString);
350                mAnimator.announceForAccessibility(mSelectDay);
351                break;
352            case YEAR_VIEW:
353                pulseAnimator = getPulseAnimator(mHeaderYearTextView, 0.85f, 1.1f);
354                if (mDelayAnimation) {
355                    pulseAnimator.setStartDelay(ANIMATION_DELAY);
356                    mDelayAnimation = false;
357                }
358                mYearPickerView.onDateChanged();
359                if (mCurrentView != viewIndex) {
360                    mMonthAndDayLayout.setSelected(false);
361                    mHeaderYearTextView.setSelected(true);
362                    mAnimator.setDisplayedChild(YEAR_VIEW);
363                    mCurrentView = viewIndex;
364                }
365                pulseAnimator.start();
366
367                CharSequence yearString = mYearFormat.format(millis);
368                mAnimator.setContentDescription(mYearPickerDescription + ": " + yearString);
369                mAnimator.announceForAccessibility(mSelectYear);
370                break;
371        }
372    }
373
374    @Override
375    public void init(int year, int monthOfYear, int dayOfMonth,
376            DatePicker.OnDateChangedListener callBack) {
377        mDateChangedListener = callBack;
378        mCurrentDate.set(Calendar.YEAR, year);
379        mCurrentDate.set(Calendar.MONTH, monthOfYear);
380        mCurrentDate.set(Calendar.DAY_OF_MONTH, dayOfMonth);
381        updateDisplay(false);
382    }
383
384    @Override
385    public void updateDate(int year, int month, int dayOfMonth) {
386        mCurrentDate.set(Calendar.YEAR, year);
387        mCurrentDate.set(Calendar.MONTH, month);
388        mCurrentDate.set(Calendar.DAY_OF_MONTH, dayOfMonth);
389        mDateChangedListener.onDateChanged(mDelegator, year, month, dayOfMonth);
390        updateDisplay(false);
391    }
392
393    @Override
394    public int getYear() {
395        return mCurrentDate.get(Calendar.YEAR);
396    }
397
398    @Override
399    public int getMonth() {
400        return mCurrentDate.get(Calendar.MONTH);
401    }
402
403    @Override
404    public int getDayOfMonth() {
405        return mCurrentDate.get(Calendar.DAY_OF_MONTH);
406    }
407
408    @Override
409    public void setMinDate(long minDate) {
410        mTempDate.setTimeInMillis(minDate);
411        if (mTempDate.get(Calendar.YEAR) == mMinDate.get(Calendar.YEAR)
412                && mTempDate.get(Calendar.DAY_OF_YEAR) != mMinDate.get(Calendar.DAY_OF_YEAR)) {
413            return;
414        }
415        if (mCurrentDate.before(mTempDate)) {
416            mCurrentDate.setTimeInMillis(minDate);
417            updatePickers();
418            updateDisplay(false);
419        }
420        mMinDate.setTimeInMillis(minDate);
421        mDayPickerView.goTo(getSelectedDay(), false, true, true);
422    }
423
424    @Override
425    public Calendar getMinDate() {
426        return mMinDate;
427    }
428
429    @Override
430    public void setMaxDate(long maxDate) {
431        mTempDate.setTimeInMillis(maxDate);
432        if (mTempDate.get(Calendar.YEAR) == mMaxDate.get(Calendar.YEAR)
433                && mTempDate.get(Calendar.DAY_OF_YEAR) != mMaxDate.get(Calendar.DAY_OF_YEAR)) {
434            return;
435        }
436        if (mCurrentDate.after(mTempDate)) {
437            mCurrentDate.setTimeInMillis(maxDate);
438            updatePickers();
439            updateDisplay(false);
440        }
441        mMaxDate.setTimeInMillis(maxDate);
442        mDayPickerView.goTo(getSelectedDay(), false, true, true);
443    }
444
445    @Override
446    public Calendar getMaxDate() {
447        return mMaxDate;
448    }
449
450    @Override
451    public int getFirstDayOfWeek() {
452        return mCurrentDate.getFirstDayOfWeek();
453    }
454
455    @Override
456    public int getMinYear() {
457        return mMinDate.get(Calendar.YEAR);
458    }
459
460    @Override
461    public int getMaxYear() {
462        return mMaxDate.get(Calendar.YEAR);
463    }
464
465    @Override
466    public int getMinMonth() {
467        return mMinDate.get(Calendar.MONTH);
468    }
469
470    @Override
471    public int getMaxMonth() {
472        return mMaxDate.get(Calendar.MONTH);
473    }
474
475    @Override
476    public int getMinDay() {
477        return mMinDate.get(Calendar.DAY_OF_MONTH);
478    }
479
480    @Override
481    public int getMaxDay() {
482        return mMaxDate.get(Calendar.DAY_OF_MONTH);
483    }
484
485    @Override
486    public void setEnabled(boolean enabled) {
487        mMonthAndDayLayout.setEnabled(enabled);
488        mHeaderYearTextView.setEnabled(enabled);
489        mAnimator.setEnabled(enabled);
490        mIsEnabled = enabled;
491    }
492
493    @Override
494    public boolean isEnabled() {
495        return mIsEnabled;
496    }
497
498    @Override
499    public CalendarView getCalendarView() {
500        throw new UnsupportedOperationException(
501                "CalendarView does not exists for the new DatePicker");
502    }
503
504    @Override
505    public void setCalendarViewShown(boolean shown) {
506        // No-op for compatibility with the old DatePicker.
507    }
508
509    @Override
510    public boolean getCalendarViewShown() {
511        return false;
512    }
513
514    @Override
515    public void setSpinnersShown(boolean shown) {
516        // No-op for compatibility with the old DatePicker.
517    }
518
519    @Override
520    public boolean getSpinnersShown() {
521        return false;
522    }
523
524    @Override
525    public void onConfigurationChanged(Configuration newConfig) {
526        mYearFormat = new SimpleDateFormat("y", newConfig.locale);
527        mDayFormat = new SimpleDateFormat("d", newConfig.locale);
528    }
529
530    @Override
531    public void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
532        // Nothing to do
533    }
534
535    @Override
536    public Parcelable onSaveInstanceState(Parcelable superState) {
537        final int year = mCurrentDate.get(Calendar.YEAR);
538        final int month = mCurrentDate.get(Calendar.MONTH);
539        final int day = mCurrentDate.get(Calendar.DAY_OF_MONTH);
540
541        int listPosition = -1;
542        int listPositionOffset = -1;
543
544        if (mCurrentView == MONTH_AND_DAY_VIEW) {
545            listPosition = mDayPickerView.getMostVisiblePosition();
546        } else if (mCurrentView == YEAR_VIEW) {
547            listPosition = mYearPickerView.getFirstVisiblePosition();
548            listPositionOffset = mYearPickerView.getFirstPositionOffset();
549        }
550
551        return new SavedState(superState, year, month, day, mMinDate.getTimeInMillis(),
552                mMaxDate.getTimeInMillis(), mCurrentView, listPosition, listPositionOffset);
553    }
554
555    @Override
556    public void onRestoreInstanceState(Parcelable state) {
557        SavedState ss = (SavedState) state;
558
559        mCurrentDate.set(ss.getSelectedDay(), ss.getSelectedMonth(), ss.getSelectedYear());
560        mCurrentView = ss.getCurrentView();
561        mMinDate.setTimeInMillis(ss.getMinDate());
562        mMaxDate.setTimeInMillis(ss.getMaxDate());
563
564        updateDisplay(false);
565        setCurrentView(mCurrentView);
566
567        final int listPosition = ss.getListPosition();
568        if (listPosition != -1) {
569            if (mCurrentView == MONTH_AND_DAY_VIEW) {
570                mDayPickerView.postSetSelection(listPosition);
571            } else if (mCurrentView == YEAR_VIEW) {
572                mYearPickerView.postSetSelectionFromTop(listPosition, ss.getListPositionOffset());
573            }
574        }
575    }
576
577    @Override
578    public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
579        onPopulateAccessibilityEvent(event);
580        return true;
581    }
582
583    @Override
584    public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
585        event.getText().add(mCurrentDate.getTime().toString());
586    }
587
588    @Override
589    public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
590        event.setClassName(DatePicker.class.getName());
591    }
592
593    @Override
594    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
595        info.setClassName(DatePicker.class.getName());
596    }
597
598    @Override
599    public void onYearSelected(int year) {
600        adjustDayInMonthIfNeeded(mCurrentDate.get(Calendar.MONTH), year);
601        mCurrentDate.set(Calendar.YEAR, year);
602        updatePickers();
603        setCurrentView(MONTH_AND_DAY_VIEW);
604        updateDisplay(true);
605    }
606
607    // If the newly selected month / year does not contain the currently selected day number,
608    // change the selected day number to the last day of the selected month or year.
609    //      e.g. Switching from Mar to Apr when Mar 31 is selected -> Apr 30
610    //      e.g. Switching from 2012 to 2013 when Feb 29, 2012 is selected -> Feb 28, 2013
611    private void adjustDayInMonthIfNeeded(int month, int year) {
612        int day = mCurrentDate.get(Calendar.DAY_OF_MONTH);
613        int daysInMonth = getDaysInMonth(month, year);
614        if (day > daysInMonth) {
615            mCurrentDate.set(Calendar.DAY_OF_MONTH, daysInMonth);
616        }
617    }
618
619    public static int getDaysInMonth(int month, int year) {
620        switch (month) {
621            case Calendar.JANUARY:
622            case Calendar.MARCH:
623            case Calendar.MAY:
624            case Calendar.JULY:
625            case Calendar.AUGUST:
626            case Calendar.OCTOBER:
627            case Calendar.DECEMBER:
628                return 31;
629            case Calendar.APRIL:
630            case Calendar.JUNE:
631            case Calendar.SEPTEMBER:
632            case Calendar.NOVEMBER:
633                return 30;
634            case Calendar.FEBRUARY:
635                return (year % 4 == 0) ? 29 : 28;
636            default:
637                throw new IllegalArgumentException("Invalid Month");
638        }
639    }
640
641    @Override
642    public void onDayOfMonthSelected(int year, int month, int day) {
643        mCurrentDate.set(Calendar.YEAR, year);
644        mCurrentDate.set(Calendar.MONTH, month);
645        mCurrentDate.set(Calendar.DAY_OF_MONTH, day);
646        updatePickers();
647        updateDisplay(true);
648    }
649
650    private void updatePickers() {
651        for (OnDateChangedListener listener : mListeners) {
652            listener.onDateChanged();
653        }
654    }
655
656    @Override
657    public void registerOnDateChangedListener(OnDateChangedListener listener) {
658        mListeners.add(listener);
659    }
660
661    @Override
662    public void unregisterOnDateChangedListener(OnDateChangedListener listener) {
663        mListeners.remove(listener);
664    }
665
666    @Override
667    public Calendar getSelectedDay() {
668        return mCurrentDate;
669    }
670
671    @Override
672    public void tryVibrate() {
673        mDelegator.performHapticFeedback(HapticFeedbackConstants.CALENDAR_DATE);
674    }
675
676    @Override
677    public void onClick(View v) {
678        tryVibrate();
679        if (v.getId() == R.id.date_picker_year) {
680            setCurrentView(YEAR_VIEW);
681        } else if (v.getId() == R.id.date_picker_month_and_day_layout) {
682            setCurrentView(MONTH_AND_DAY_VIEW);
683        }
684    }
685
686    /**
687     * Class for managing state storing/restoring.
688     */
689    private static class SavedState extends View.BaseSavedState {
690
691        private final int mSelectedYear;
692        private final int mSelectedMonth;
693        private final int mSelectedDay;
694        private final long mMinDate;
695        private final long mMaxDate;
696        private final int mCurrentView;
697        private final int mListPosition;
698        private final int mListPositionOffset;
699
700        /**
701         * Constructor called from {@link DatePicker#onSaveInstanceState()}
702         */
703        private SavedState(Parcelable superState, int year, int month, int day,
704                long minDate, long maxDate, int currentView, int listPosition,
705                int listPositionOffset) {
706            super(superState);
707            mSelectedYear = year;
708            mSelectedMonth = month;
709            mSelectedDay = day;
710            mMinDate = minDate;
711            mMaxDate = maxDate;
712            mCurrentView = currentView;
713            mListPosition = listPosition;
714            mListPositionOffset = listPositionOffset;
715        }
716
717        /**
718         * Constructor called from {@link #CREATOR}
719         */
720        private SavedState(Parcel in) {
721            super(in);
722            mSelectedYear = in.readInt();
723            mSelectedMonth = in.readInt();
724            mSelectedDay = in.readInt();
725            mMinDate = in.readLong();
726            mMaxDate = in.readLong();
727            mCurrentView = in.readInt();
728            mListPosition = in.readInt();
729            mListPositionOffset = in.readInt();
730        }
731
732        @Override
733        public void writeToParcel(Parcel dest, int flags) {
734            super.writeToParcel(dest, flags);
735            dest.writeInt(mSelectedYear);
736            dest.writeInt(mSelectedMonth);
737            dest.writeInt(mSelectedDay);
738            dest.writeLong(mMinDate);
739            dest.writeLong(mMaxDate);
740            dest.writeInt(mCurrentView);
741            dest.writeInt(mListPosition);
742            dest.writeInt(mListPositionOffset);
743        }
744
745        public int getSelectedDay() {
746            return mSelectedDay;
747        }
748
749        public int getSelectedMonth() {
750            return mSelectedMonth;
751        }
752
753        public int getSelectedYear() {
754            return mSelectedYear;
755        }
756
757        public long getMinDate() {
758            return mMinDate;
759        }
760
761        public long getMaxDate() {
762            return mMaxDate;
763        }
764
765        public int getCurrentView() {
766            return mCurrentView;
767        }
768
769        public int getListPosition() {
770            return mListPosition;
771        }
772
773        public int getListPositionOffset() {
774            return mListPositionOffset;
775        }
776
777        @SuppressWarnings("all")
778        // suppress unused and hiding
779        public static final Parcelable.Creator<SavedState> CREATOR = new Creator<SavedState>() {
780
781            public SavedState createFromParcel(Parcel in) {
782                return new SavedState(in);
783            }
784
785            public SavedState[] newArray(int size) {
786                return new SavedState[size];
787            }
788        };
789    }
790
791    /**
792     * Render an animator to pulsate a view in place.
793     * @param labelToAnimate the view to pulsate.
794     * @return The animator object. Use .start() to begin.
795     */
796    public static ObjectAnimator getPulseAnimator(View labelToAnimate, float decreaseRatio,
797                                                  float increaseRatio) {
798        Keyframe k0 = Keyframe.ofFloat(0f, 1f);
799        Keyframe k1 = Keyframe.ofFloat(0.275f, decreaseRatio);
800        Keyframe k2 = Keyframe.ofFloat(0.69f, increaseRatio);
801        Keyframe k3 = Keyframe.ofFloat(1f, 1f);
802
803        PropertyValuesHolder scaleX = PropertyValuesHolder.ofKeyframe(View.SCALE_X, k0, k1, k2, k3);
804        PropertyValuesHolder scaleY = PropertyValuesHolder.ofKeyframe(View.SCALE_Y, k0, k1, k2, k3);
805        ObjectAnimator pulseAnimator =
806                ObjectAnimator.ofPropertyValuesHolder(labelToAnimate, scaleX, scaleY);
807        pulseAnimator.setDuration(PULSE_ANIMATOR_DURATION);
808
809        return pulseAnimator;
810    }
811}
812