• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package android.widget;
18 
19 import android.annotation.Nullable;
20 import android.content.Context;
21 import android.content.res.ColorStateList;
22 import android.content.res.Configuration;
23 import android.content.res.Resources;
24 import android.content.res.TypedArray;
25 import android.icu.text.DisplayContext;
26 import android.icu.text.SimpleDateFormat;
27 import android.icu.util.Calendar;
28 import android.os.Parcelable;
29 import android.text.format.DateFormat;
30 import android.util.AttributeSet;
31 import android.util.StateSet;
32 import android.view.HapticFeedbackConstants;
33 import android.view.LayoutInflater;
34 import android.view.View;
35 import android.view.View.OnClickListener;
36 import android.view.ViewGroup;
37 import android.view.accessibility.AccessibilityEvent;
38 import android.widget.DayPickerView.OnDaySelectedListener;
39 import android.widget.YearPickerView.OnYearSelectedListener;
40 
41 import com.android.internal.R;
42 
43 import java.util.Locale;
44 
45 /**
46  * A delegate for picking up a date (day / month / year).
47  */
48 class DatePickerCalendarDelegate extends DatePicker.AbstractDatePickerDelegate {
49     private static final int USE_LOCALE = 0;
50 
51     private static final int UNINITIALIZED = -1;
52     private static final int VIEW_MONTH_DAY = 0;
53     private static final int VIEW_YEAR = 1;
54 
55     private static final int DEFAULT_START_YEAR = 1900;
56     private static final int DEFAULT_END_YEAR = 2100;
57 
58     private static final int ANIMATION_DURATION = 300;
59 
60     private static final int[] ATTRS_TEXT_COLOR = new int[] {
61             com.android.internal.R.attr.textColor};
62     private static final int[] ATTRS_DISABLED_ALPHA = new int[] {
63             com.android.internal.R.attr.disabledAlpha};
64 
65     private SimpleDateFormat mYearFormat;
66     private SimpleDateFormat mMonthDayFormat;
67 
68     // Top-level container.
69     private ViewGroup mContainer;
70 
71     // Header views.
72     private TextView mHeaderYear;
73     private TextView mHeaderMonthDay;
74 
75     // Picker views.
76     private ViewAnimator mAnimator;
77     private DayPickerView mDayPickerView;
78     private YearPickerView mYearPickerView;
79 
80     // Accessibility strings.
81     private String mSelectDay;
82     private String mSelectYear;
83 
84     private int mCurrentView = UNINITIALIZED;
85 
86     private final Calendar mTempDate;
87     private final Calendar mMinDate;
88     private final Calendar mMaxDate;
89 
90     private int mFirstDayOfWeek = USE_LOCALE;
91 
DatePickerCalendarDelegate(DatePicker delegator, Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)92     public DatePickerCalendarDelegate(DatePicker delegator, Context context, AttributeSet attrs,
93             int defStyleAttr, int defStyleRes) {
94         super(delegator, context);
95 
96         final Locale locale = mCurrentLocale;
97         mCurrentDate = Calendar.getInstance(locale);
98         mTempDate = Calendar.getInstance(locale);
99         mMinDate = Calendar.getInstance(locale);
100         mMaxDate = Calendar.getInstance(locale);
101 
102         mMinDate.set(DEFAULT_START_YEAR, Calendar.JANUARY, 1);
103         mMaxDate.set(DEFAULT_END_YEAR, Calendar.DECEMBER, 31);
104 
105         final Resources res = mDelegator.getResources();
106         final TypedArray a = mContext.obtainStyledAttributes(attrs,
107                 R.styleable.DatePicker, defStyleAttr, defStyleRes);
108         final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
109                 Context.LAYOUT_INFLATER_SERVICE);
110         final int layoutResourceId = a.getResourceId(
111                 R.styleable.DatePicker_internalLayout, R.layout.date_picker_material);
112 
113         // Set up and attach container.
114         mContainer = (ViewGroup) inflater.inflate(layoutResourceId, mDelegator, false);
115         mContainer.setSaveFromParentEnabled(false);
116         mDelegator.addView(mContainer);
117 
118         // Set up header views.
119         final ViewGroup header = mContainer.findViewById(R.id.date_picker_header);
120         mHeaderYear = header.findViewById(R.id.date_picker_header_year);
121         mHeaderYear.setOnClickListener(mOnHeaderClickListener);
122         mHeaderMonthDay = header.findViewById(R.id.date_picker_header_date);
123         mHeaderMonthDay.setOnClickListener(mOnHeaderClickListener);
124 
125         // For the sake of backwards compatibility, attempt to extract the text
126         // color from the header month text appearance. If it's set, we'll let
127         // that override the "real" header text color.
128         ColorStateList headerTextColor = null;
129 
130         @SuppressWarnings("deprecation")
131         final int monthHeaderTextAppearance = a.getResourceId(
132                 R.styleable.DatePicker_headerMonthTextAppearance, 0);
133         if (monthHeaderTextAppearance != 0) {
134             final TypedArray textAppearance = mContext.obtainStyledAttributes(null,
135                     ATTRS_TEXT_COLOR, 0, monthHeaderTextAppearance);
136             final ColorStateList legacyHeaderTextColor = textAppearance.getColorStateList(0);
137             headerTextColor = applyLegacyColorFixes(legacyHeaderTextColor);
138             textAppearance.recycle();
139         }
140 
141         if (headerTextColor == null) {
142             headerTextColor = a.getColorStateList(R.styleable.DatePicker_headerTextColor);
143         }
144 
145         if (headerTextColor != null) {
146             mHeaderYear.setTextColor(headerTextColor);
147             mHeaderMonthDay.setTextColor(headerTextColor);
148         }
149 
150         // Set up header background, if available.
151         if (a.hasValueOrEmpty(R.styleable.DatePicker_headerBackground)) {
152             header.setBackground(a.getDrawable(R.styleable.DatePicker_headerBackground));
153         }
154 
155         a.recycle();
156 
157         // Set up picker container.
158         mAnimator = mContainer.findViewById(R.id.animator);
159 
160         // Set up day picker view.
161         mDayPickerView = mAnimator.findViewById(R.id.date_picker_day_picker);
162         mDayPickerView.setFirstDayOfWeek(mFirstDayOfWeek);
163         mDayPickerView.setMinDate(mMinDate.getTimeInMillis());
164         mDayPickerView.setMaxDate(mMaxDate.getTimeInMillis());
165         mDayPickerView.setDate(mCurrentDate.getTimeInMillis());
166         mDayPickerView.setOnDaySelectedListener(mOnDaySelectedListener);
167 
168         // Set up year picker view.
169         mYearPickerView = mAnimator.findViewById(R.id.date_picker_year_picker);
170         mYearPickerView.setRange(mMinDate, mMaxDate);
171         mYearPickerView.setYear(mCurrentDate.get(Calendar.YEAR));
172         mYearPickerView.setOnYearSelectedListener(mOnYearSelectedListener);
173 
174         // Set up content descriptions.
175         mSelectDay = res.getString(R.string.select_day);
176         mSelectYear = res.getString(R.string.select_year);
177 
178         // Initialize for current locale. This also initializes the date, so no
179         // need to call onDateChanged.
180         onLocaleChanged(mCurrentLocale);
181 
182         setCurrentView(VIEW_MONTH_DAY);
183     }
184 
185     /**
186      * The legacy text color might have been poorly defined. Ensures that it
187      * has an appropriate activated state, using the selected state if one
188      * exists or modifying the default text color otherwise.
189      *
190      * @param color a legacy text color, or {@code null}
191      * @return a color state list with an appropriate activated state, or
192      *         {@code null} if a valid activated state could not be generated
193      */
194     @Nullable
applyLegacyColorFixes(@ullable ColorStateList color)195     private ColorStateList applyLegacyColorFixes(@Nullable ColorStateList color) {
196         if (color == null || color.hasState(R.attr.state_activated)) {
197             return color;
198         }
199 
200         final int activatedColor;
201         final int defaultColor;
202         if (color.hasState(R.attr.state_selected)) {
203             activatedColor = color.getColorForState(StateSet.get(
204                     StateSet.VIEW_STATE_ENABLED | StateSet.VIEW_STATE_SELECTED), 0);
205             defaultColor = color.getColorForState(StateSet.get(
206                     StateSet.VIEW_STATE_ENABLED), 0);
207         } else {
208             activatedColor = color.getDefaultColor();
209 
210             // Generate a non-activated color using the disabled alpha.
211             final TypedArray ta = mContext.obtainStyledAttributes(ATTRS_DISABLED_ALPHA);
212             final float disabledAlpha = ta.getFloat(0, 0.30f);
213             defaultColor = multiplyAlphaComponent(activatedColor, disabledAlpha);
214         }
215 
216         if (activatedColor == 0 || defaultColor == 0) {
217             // We somehow failed to obtain the colors.
218             return null;
219         }
220 
221         final int[][] stateSet = new int[][] {{ R.attr.state_activated }, {}};
222         final int[] colors = new int[] { activatedColor, defaultColor };
223         return new ColorStateList(stateSet, colors);
224     }
225 
multiplyAlphaComponent(int color, float alphaMod)226     private int multiplyAlphaComponent(int color, float alphaMod) {
227         final int srcRgb = color & 0xFFFFFF;
228         final int srcAlpha = (color >> 24) & 0xFF;
229         final int dstAlpha = (int) (srcAlpha * alphaMod + 0.5f);
230         return srcRgb | (dstAlpha << 24);
231     }
232 
233     /**
234      * Listener called when the user selects a day in the day picker view.
235      */
236     private final OnDaySelectedListener mOnDaySelectedListener = new OnDaySelectedListener() {
237         @Override
238         public void onDaySelected(DayPickerView view, Calendar day) {
239             mCurrentDate.setTimeInMillis(day.getTimeInMillis());
240             onDateChanged(true, true);
241         }
242     };
243 
244     /**
245      * Listener called when the user selects a year in the year picker view.
246      */
247     private final OnYearSelectedListener mOnYearSelectedListener = new OnYearSelectedListener() {
248         @Override
249         public void onYearChanged(YearPickerView view, int year) {
250             // If the newly selected month / year does not contain the
251             // currently selected day number, change the selected day number
252             // to the last day of the selected month or year.
253             // e.g. Switching from Mar to Apr when Mar 31 is selected -> Apr 30
254             // e.g. Switching from 2012 to 2013 when Feb 29, 2012 is selected -> Feb 28, 2013
255             final int day = mCurrentDate.get(Calendar.DAY_OF_MONTH);
256             final int month = mCurrentDate.get(Calendar.MONTH);
257             final int daysInMonth = getDaysInMonth(month, year);
258             if (day > daysInMonth) {
259                 mCurrentDate.set(Calendar.DAY_OF_MONTH, daysInMonth);
260             }
261 
262             mCurrentDate.set(Calendar.YEAR, year);
263             onDateChanged(true, true);
264 
265             // Automatically switch to day picker.
266             setCurrentView(VIEW_MONTH_DAY);
267 
268             // Switch focus back to the year text.
269             mHeaderYear.requestFocus();
270         }
271     };
272 
273     /**
274      * Listener called when the user clicks on a header item.
275      */
276     private final OnClickListener mOnHeaderClickListener = new OnClickListener() {
277         @Override
278         public void onClick(View v) {
279             tryVibrate();
280 
281             switch (v.getId()) {
282                 case R.id.date_picker_header_year:
283                     setCurrentView(VIEW_YEAR);
284                     break;
285                 case R.id.date_picker_header_date:
286                     setCurrentView(VIEW_MONTH_DAY);
287                     break;
288             }
289         }
290     };
291 
292     @Override
onLocaleChanged(Locale locale)293     protected void onLocaleChanged(Locale locale) {
294         final TextView headerYear = mHeaderYear;
295         if (headerYear == null) {
296             // Abort, we haven't initialized yet. This method will get called
297             // again later after everything has been set up.
298             return;
299         }
300 
301         // Update the date formatter.
302         final String datePattern = DateFormat.getBestDateTimePattern(locale, "EMMMd");
303         mMonthDayFormat = new SimpleDateFormat(datePattern, locale);
304         mMonthDayFormat.setContext(DisplayContext.CAPITALIZATION_FOR_STANDALONE);
305         mYearFormat = new SimpleDateFormat("y", locale);
306 
307         // Update the header text.
308         onCurrentDateChanged(false);
309     }
310 
onCurrentDateChanged(boolean announce)311     private void onCurrentDateChanged(boolean announce) {
312         if (mHeaderYear == null) {
313             // Abort, we haven't initialized yet. This method will get called
314             // again later after everything has been set up.
315             return;
316         }
317 
318         final String year = mYearFormat.format(mCurrentDate.getTime());
319         mHeaderYear.setText(year);
320 
321         final String monthDay = mMonthDayFormat.format(mCurrentDate.getTime());
322         mHeaderMonthDay.setText(monthDay);
323 
324         // TODO: This should use live regions.
325         if (announce) {
326             mAnimator.announceForAccessibility(getFormattedCurrentDate());
327         }
328     }
329 
setCurrentView(final int viewIndex)330     private void setCurrentView(final int viewIndex) {
331         switch (viewIndex) {
332             case VIEW_MONTH_DAY:
333                 mDayPickerView.setDate(mCurrentDate.getTimeInMillis());
334 
335                 if (mCurrentView != viewIndex) {
336                     mHeaderMonthDay.setActivated(true);
337                     mHeaderYear.setActivated(false);
338                     mAnimator.setDisplayedChild(VIEW_MONTH_DAY);
339                     mCurrentView = viewIndex;
340                 }
341 
342                 mAnimator.announceForAccessibility(mSelectDay);
343                 break;
344             case VIEW_YEAR:
345                 final int year = mCurrentDate.get(Calendar.YEAR);
346                 mYearPickerView.setYear(year);
347                 mYearPickerView.post(new Runnable() {
348                     @Override
349                     public void run() {
350                         mYearPickerView.requestFocus();
351                         final View selected = mYearPickerView.getSelectedView();
352                         if (selected != null) {
353                             selected.requestFocus();
354                         }
355                     }
356                 });
357 
358                 if (mCurrentView != viewIndex) {
359                     mHeaderMonthDay.setActivated(false);
360                     mHeaderYear.setActivated(true);
361                     mAnimator.setDisplayedChild(VIEW_YEAR);
362                     mCurrentView = viewIndex;
363                 }
364 
365                 mAnimator.announceForAccessibility(mSelectYear);
366                 break;
367         }
368     }
369 
370     @Override
init(int year, int monthOfYear, int dayOfMonth, DatePicker.OnDateChangedListener callBack)371     public void init(int year, int monthOfYear, int dayOfMonth,
372             DatePicker.OnDateChangedListener callBack) {
373         mCurrentDate.set(Calendar.YEAR, year);
374         mCurrentDate.set(Calendar.MONTH, monthOfYear);
375         mCurrentDate.set(Calendar.DAY_OF_MONTH, dayOfMonth);
376 
377         onDateChanged(false, false);
378 
379         mOnDateChangedListener = callBack;
380     }
381 
382     @Override
updateDate(int year, int month, int dayOfMonth)383     public void updateDate(int year, int month, int dayOfMonth) {
384         mCurrentDate.set(Calendar.YEAR, year);
385         mCurrentDate.set(Calendar.MONTH, month);
386         mCurrentDate.set(Calendar.DAY_OF_MONTH, dayOfMonth);
387 
388         onDateChanged(false, true);
389     }
390 
onDateChanged(boolean fromUser, boolean callbackToClient)391     private void onDateChanged(boolean fromUser, boolean callbackToClient) {
392         final int year = mCurrentDate.get(Calendar.YEAR);
393 
394         if (callbackToClient
395                 && (mOnDateChangedListener != null || mAutoFillChangeListener != null)) {
396             final int monthOfYear = mCurrentDate.get(Calendar.MONTH);
397             final int dayOfMonth = mCurrentDate.get(Calendar.DAY_OF_MONTH);
398             if (mOnDateChangedListener != null) {
399                 mOnDateChangedListener.onDateChanged(mDelegator, year, monthOfYear, dayOfMonth);
400             }
401             if (mAutoFillChangeListener != null) {
402                 mAutoFillChangeListener.onDateChanged(mDelegator, year, monthOfYear, dayOfMonth);
403             }
404         }
405 
406         mDayPickerView.setDate(mCurrentDate.getTimeInMillis());
407         mYearPickerView.setYear(year);
408 
409         onCurrentDateChanged(fromUser);
410 
411         if (fromUser) {
412             tryVibrate();
413         }
414     }
415 
416     @Override
getYear()417     public int getYear() {
418         return mCurrentDate.get(Calendar.YEAR);
419     }
420 
421     @Override
getMonth()422     public int getMonth() {
423         return mCurrentDate.get(Calendar.MONTH);
424     }
425 
426     @Override
getDayOfMonth()427     public int getDayOfMonth() {
428         return mCurrentDate.get(Calendar.DAY_OF_MONTH);
429     }
430 
431     @Override
setMinDate(long minDate)432     public void setMinDate(long minDate) {
433         mTempDate.setTimeInMillis(minDate);
434         if (mTempDate.get(Calendar.YEAR) == mMinDate.get(Calendar.YEAR)
435                 && mTempDate.get(Calendar.DAY_OF_YEAR) == mMinDate.get(Calendar.DAY_OF_YEAR)) {
436             // Same day, no-op.
437             return;
438         }
439         if (mCurrentDate.before(mTempDate)) {
440             mCurrentDate.setTimeInMillis(minDate);
441             onDateChanged(false, true);
442         }
443         mMinDate.setTimeInMillis(minDate);
444         mDayPickerView.setMinDate(minDate);
445         mYearPickerView.setRange(mMinDate, mMaxDate);
446     }
447 
448     @Override
getMinDate()449     public Calendar getMinDate() {
450         return mMinDate;
451     }
452 
453     @Override
setMaxDate(long maxDate)454     public void setMaxDate(long maxDate) {
455         mTempDate.setTimeInMillis(maxDate);
456         if (mTempDate.get(Calendar.YEAR) == mMaxDate.get(Calendar.YEAR)
457                 && mTempDate.get(Calendar.DAY_OF_YEAR) == mMaxDate.get(Calendar.DAY_OF_YEAR)) {
458             // Same day, no-op.
459             return;
460         }
461         if (mCurrentDate.after(mTempDate)) {
462             mCurrentDate.setTimeInMillis(maxDate);
463             onDateChanged(false, true);
464         }
465         mMaxDate.setTimeInMillis(maxDate);
466         mDayPickerView.setMaxDate(maxDate);
467         mYearPickerView.setRange(mMinDate, mMaxDate);
468     }
469 
470     @Override
getMaxDate()471     public Calendar getMaxDate() {
472         return mMaxDate;
473     }
474 
475     @Override
setFirstDayOfWeek(int firstDayOfWeek)476     public void setFirstDayOfWeek(int firstDayOfWeek) {
477         mFirstDayOfWeek = firstDayOfWeek;
478 
479         mDayPickerView.setFirstDayOfWeek(firstDayOfWeek);
480     }
481 
482     @Override
getFirstDayOfWeek()483     public int getFirstDayOfWeek() {
484         if (mFirstDayOfWeek != USE_LOCALE) {
485             return mFirstDayOfWeek;
486         }
487         return mCurrentDate.getFirstDayOfWeek();
488     }
489 
490     @Override
setEnabled(boolean enabled)491     public void setEnabled(boolean enabled) {
492         mContainer.setEnabled(enabled);
493         mDayPickerView.setEnabled(enabled);
494         mYearPickerView.setEnabled(enabled);
495         mHeaderYear.setEnabled(enabled);
496         mHeaderMonthDay.setEnabled(enabled);
497     }
498 
499     @Override
isEnabled()500     public boolean isEnabled() {
501         return mContainer.isEnabled();
502     }
503 
504     @Override
getCalendarView()505     public CalendarView getCalendarView() {
506         throw new UnsupportedOperationException("Not supported by calendar-mode DatePicker");
507     }
508 
509     @Override
setCalendarViewShown(boolean shown)510     public void setCalendarViewShown(boolean shown) {
511         // No-op for compatibility with the old DatePicker.
512     }
513 
514     @Override
getCalendarViewShown()515     public boolean getCalendarViewShown() {
516         return false;
517     }
518 
519     @Override
setSpinnersShown(boolean shown)520     public void setSpinnersShown(boolean shown) {
521         // No-op for compatibility with the old DatePicker.
522     }
523 
524     @Override
getSpinnersShown()525     public boolean getSpinnersShown() {
526         return false;
527     }
528 
529     @Override
onConfigurationChanged(Configuration newConfig)530     public void onConfigurationChanged(Configuration newConfig) {
531         setCurrentLocale(newConfig.locale);
532     }
533 
534     @Override
onSaveInstanceState(Parcelable superState)535     public Parcelable onSaveInstanceState(Parcelable superState) {
536         final int year = mCurrentDate.get(Calendar.YEAR);
537         final int month = mCurrentDate.get(Calendar.MONTH);
538         final int day = mCurrentDate.get(Calendar.DAY_OF_MONTH);
539 
540         int listPosition = -1;
541         int listPositionOffset = -1;
542 
543         if (mCurrentView == VIEW_MONTH_DAY) {
544             listPosition = mDayPickerView.getMostVisiblePosition();
545         } else if (mCurrentView == VIEW_YEAR) {
546             listPosition = mYearPickerView.getFirstVisiblePosition();
547             listPositionOffset = mYearPickerView.getFirstPositionOffset();
548         }
549 
550         return new SavedState(superState, year, month, day, mMinDate.getTimeInMillis(),
551                 mMaxDate.getTimeInMillis(), mCurrentView, listPosition, listPositionOffset);
552     }
553 
554     @Override
onRestoreInstanceState(Parcelable state)555     public void onRestoreInstanceState(Parcelable state) {
556         if (state instanceof SavedState) {
557             final SavedState ss = (SavedState) state;
558 
559             // TODO: Move instance state into DayPickerView, YearPickerView.
560             mCurrentDate.set(ss.getSelectedYear(), ss.getSelectedMonth(), ss.getSelectedDay());
561             mMinDate.setTimeInMillis(ss.getMinDate());
562             mMaxDate.setTimeInMillis(ss.getMaxDate());
563 
564             onCurrentDateChanged(false);
565 
566             final int currentView = ss.getCurrentView();
567             setCurrentView(currentView);
568 
569             final int listPosition = ss.getListPosition();
570             if (listPosition != -1) {
571                 if (currentView == VIEW_MONTH_DAY) {
572                     mDayPickerView.setPosition(listPosition);
573                 } else if (currentView == VIEW_YEAR) {
574                     final int listPositionOffset = ss.getListPositionOffset();
575                     mYearPickerView.setSelectionFromTop(listPosition, listPositionOffset);
576                 }
577             }
578         }
579     }
580 
581     @Override
dispatchPopulateAccessibilityEvent(AccessibilityEvent event)582     public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
583         onPopulateAccessibilityEvent(event);
584         return true;
585     }
586 
getAccessibilityClassName()587     public CharSequence getAccessibilityClassName() {
588         return DatePicker.class.getName();
589     }
590 
getDaysInMonth(int month, int year)591     public static int getDaysInMonth(int month, int year) {
592         switch (month) {
593             case Calendar.JANUARY:
594             case Calendar.MARCH:
595             case Calendar.MAY:
596             case Calendar.JULY:
597             case Calendar.AUGUST:
598             case Calendar.OCTOBER:
599             case Calendar.DECEMBER:
600                 return 31;
601             case Calendar.APRIL:
602             case Calendar.JUNE:
603             case Calendar.SEPTEMBER:
604             case Calendar.NOVEMBER:
605                 return 30;
606             case Calendar.FEBRUARY:
607                 return (year % 4 == 0) ? 29 : 28;
608             default:
609                 throw new IllegalArgumentException("Invalid Month");
610         }
611     }
612 
tryVibrate()613     private void tryVibrate() {
614         mDelegator.performHapticFeedback(HapticFeedbackConstants.CALENDAR_DATE);
615     }
616 }
617