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