Home | History | Annotate | Download | only in widget
      1 /*
      2  * Copyright (C) 2007 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.annotation.Widget;
     21 import android.content.Context;
     22 import android.content.res.Configuration;
     23 import android.content.res.TypedArray;
     24 import android.os.Parcel;
     25 import android.os.Parcelable;
     26 import android.text.TextUtils;
     27 import android.text.InputType;
     28 import android.text.format.DateFormat;
     29 import android.text.format.DateUtils;
     30 import android.util.AttributeSet;
     31 import android.util.Log;
     32 import android.util.SparseArray;
     33 import android.view.LayoutInflater;
     34 import android.view.View;
     35 import android.view.accessibility.AccessibilityEvent;
     36 import android.view.accessibility.AccessibilityNodeInfo;
     37 import android.view.inputmethod.EditorInfo;
     38 import android.view.inputmethod.InputMethodManager;
     39 import android.widget.NumberPicker.OnValueChangeListener;
     40 
     41 import com.android.internal.R;
     42 
     43 import java.text.DateFormatSymbols;
     44 import java.text.ParseException;
     45 import java.text.SimpleDateFormat;
     46 import java.util.Arrays;
     47 import java.util.Calendar;
     48 import java.util.Locale;
     49 import java.util.TimeZone;
     50 
     51 import libcore.icu.ICU;
     52 
     53 /**
     54  * This class is a widget for selecting a date. The date can be selected by a
     55  * year, month, and day spinners or a {@link CalendarView}. The set of spinners
     56  * and the calendar view are automatically synchronized. The client can
     57  * customize whether only the spinners, or only the calendar view, or both to be
     58  * displayed. Also the minimal and maximal date from which dates to be selected
     59  * can be customized.
     60  * <p>
     61  * See the <a href="{@docRoot}guide/topics/ui/controls/pickers.html">Pickers</a>
     62  * guide.
     63  * </p>
     64  * <p>
     65  * For a dialog using this view, see {@link android.app.DatePickerDialog}.
     66  * </p>
     67  *
     68  * @attr ref android.R.styleable#DatePicker_startYear
     69  * @attr ref android.R.styleable#DatePicker_endYear
     70  * @attr ref android.R.styleable#DatePicker_maxDate
     71  * @attr ref android.R.styleable#DatePicker_minDate
     72  * @attr ref android.R.styleable#DatePicker_spinnersShown
     73  * @attr ref android.R.styleable#DatePicker_calendarViewShown
     74  * @attr ref android.R.styleable#DatePicker_dayOfWeekBackground
     75  * @attr ref android.R.styleable#DatePicker_dayOfWeekTextAppearance
     76  * @attr ref android.R.styleable#DatePicker_headerBackground
     77  * @attr ref android.R.styleable#DatePicker_headerMonthTextAppearance
     78  * @attr ref android.R.styleable#DatePicker_headerDayOfMonthTextAppearance
     79  * @attr ref android.R.styleable#DatePicker_headerYearTextAppearance
     80  * @attr ref android.R.styleable#DatePicker_yearListItemTextAppearance
     81  * @attr ref android.R.styleable#DatePicker_yearListSelectorColor
     82  * @attr ref android.R.styleable#DatePicker_calendarTextColor
     83  */
     84 @Widget
     85 public class DatePicker extends FrameLayout {
     86     private static final String LOG_TAG = DatePicker.class.getSimpleName();
     87 
     88     private static final int MODE_SPINNER = 1;
     89     private static final int MODE_CALENDAR = 2;
     90 
     91     private final DatePickerDelegate mDelegate;
     92 
     93     /**
     94      * The callback used to indicate the user changes\d the date.
     95      */
     96     public interface OnDateChangedListener {
     97 
     98         /**
     99          * Called upon a date change.
    100          *
    101          * @param view The view associated with this listener.
    102          * @param year The year that was set.
    103          * @param monthOfYear The month that was set (0-11) for compatibility
    104          *            with {@link java.util.Calendar}.
    105          * @param dayOfMonth The day of the month that was set.
    106          */
    107         void onDateChanged(DatePicker view, int year, int monthOfYear, int dayOfMonth);
    108     }
    109 
    110     public DatePicker(Context context) {
    111         this(context, null);
    112     }
    113 
    114     public DatePicker(Context context, AttributeSet attrs) {
    115         this(context, attrs, R.attr.datePickerStyle);
    116     }
    117 
    118     public DatePicker(Context context, AttributeSet attrs, int defStyleAttr) {
    119         this(context, attrs, defStyleAttr, 0);
    120     }
    121 
    122     public DatePicker(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    123         super(context, attrs, defStyleAttr, defStyleRes);
    124 
    125         final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.DatePicker,
    126                 defStyleAttr, defStyleRes);
    127         final int mode = a.getInt(R.styleable.DatePicker_datePickerMode, MODE_SPINNER);
    128         final int firstDayOfWeek = a.getInt(R.styleable.DatePicker_firstDayOfWeek, 0);
    129         a.recycle();
    130 
    131         switch (mode) {
    132             case MODE_CALENDAR:
    133                 mDelegate = createCalendarUIDelegate(context, attrs, defStyleAttr, defStyleRes);
    134                 break;
    135             case MODE_SPINNER:
    136             default:
    137                 mDelegate = createSpinnerUIDelegate(context, attrs, defStyleAttr, defStyleRes);
    138                 break;
    139         }
    140 
    141         if (firstDayOfWeek != 0) {
    142             setFirstDayOfWeek(firstDayOfWeek);
    143         }
    144     }
    145 
    146     private DatePickerDelegate createSpinnerUIDelegate(Context context, AttributeSet attrs,
    147             int defStyleAttr, int defStyleRes) {
    148         return new DatePickerSpinnerDelegate(this, context, attrs, defStyleAttr, defStyleRes);
    149     }
    150 
    151     private DatePickerDelegate createCalendarUIDelegate(Context context, AttributeSet attrs,
    152             int defStyleAttr, int defStyleRes) {
    153         return new DatePickerCalendarDelegate(this, context, attrs, defStyleAttr,
    154                 defStyleRes);
    155     }
    156 
    157     /**
    158      * Initialize the state. If the provided values designate an inconsistent
    159      * date the values are normalized before updating the spinners.
    160      *
    161      * @param year The initial year.
    162      * @param monthOfYear The initial month <strong>starting from zero</strong>.
    163      * @param dayOfMonth The initial day of the month.
    164      * @param onDateChangedListener How user is notified date is changed by
    165      *            user, can be null.
    166      */
    167     public void init(int year, int monthOfYear, int dayOfMonth,
    168                      OnDateChangedListener onDateChangedListener) {
    169         mDelegate.init(year, monthOfYear, dayOfMonth, onDateChangedListener);
    170     }
    171 
    172     /**
    173      * Update the current date.
    174      *
    175      * @param year The year.
    176      * @param month The month which is <strong>starting from zero</strong>.
    177      * @param dayOfMonth The day of the month.
    178      */
    179     public void updateDate(int year, int month, int dayOfMonth) {
    180         mDelegate.updateDate(year, month, dayOfMonth);
    181     }
    182 
    183     /**
    184      * @return The selected year.
    185      */
    186     public int getYear() {
    187         return mDelegate.getYear();
    188     }
    189 
    190     /**
    191      * @return The selected month.
    192      */
    193     public int getMonth() {
    194         return mDelegate.getMonth();
    195     }
    196 
    197     /**
    198      * @return The selected day of month.
    199      */
    200     public int getDayOfMonth() {
    201         return mDelegate.getDayOfMonth();
    202     }
    203 
    204     /**
    205      * Gets the minimal date supported by this {@link DatePicker} in
    206      * milliseconds since January 1, 1970 00:00:00 in
    207      * {@link TimeZone#getDefault()} time zone.
    208      * <p>
    209      * Note: The default minimal date is 01/01/1900.
    210      * <p>
    211      *
    212      * @return The minimal supported date.
    213      */
    214     public long getMinDate() {
    215         return mDelegate.getMinDate().getTimeInMillis();
    216     }
    217 
    218     /**
    219      * Sets the minimal date supported by this {@link NumberPicker} in
    220      * milliseconds since January 1, 1970 00:00:00 in
    221      * {@link TimeZone#getDefault()} time zone.
    222      *
    223      * @param minDate The minimal supported date.
    224      */
    225     public void setMinDate(long minDate) {
    226         mDelegate.setMinDate(minDate);
    227     }
    228 
    229     /**
    230      * Gets the maximal date supported by this {@link DatePicker} in
    231      * milliseconds since January 1, 1970 00:00:00 in
    232      * {@link TimeZone#getDefault()} time zone.
    233      * <p>
    234      * Note: The default maximal date is 12/31/2100.
    235      * <p>
    236      *
    237      * @return The maximal supported date.
    238      */
    239     public long getMaxDate() {
    240         return mDelegate.getMaxDate().getTimeInMillis();
    241     }
    242 
    243     /**
    244      * Sets the maximal date supported by this {@link DatePicker} in
    245      * milliseconds since January 1, 1970 00:00:00 in
    246      * {@link TimeZone#getDefault()} time zone.
    247      *
    248      * @param maxDate The maximal supported date.
    249      */
    250     public void setMaxDate(long maxDate) {
    251         mDelegate.setMaxDate(maxDate);
    252     }
    253 
    254     /**
    255      * Sets the callback that indicates the current date is valid.
    256      *
    257      * @param callback the callback, may be null
    258      * @hide
    259      */
    260     public void setValidationCallback(@Nullable ValidationCallback callback) {
    261         mDelegate.setValidationCallback(callback);
    262     }
    263 
    264     @Override
    265     public void setEnabled(boolean enabled) {
    266         if (mDelegate.isEnabled() == enabled) {
    267             return;
    268         }
    269         super.setEnabled(enabled);
    270         mDelegate.setEnabled(enabled);
    271     }
    272 
    273     @Override
    274     public boolean isEnabled() {
    275         return mDelegate.isEnabled();
    276     }
    277 
    278     @Override
    279     public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
    280         return mDelegate.dispatchPopulateAccessibilityEvent(event);
    281     }
    282 
    283     @Override
    284     public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
    285         super.onPopulateAccessibilityEvent(event);
    286         mDelegate.onPopulateAccessibilityEvent(event);
    287     }
    288 
    289     @Override
    290     public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
    291         super.onInitializeAccessibilityEvent(event);
    292         mDelegate.onInitializeAccessibilityEvent(event);
    293     }
    294 
    295     @Override
    296     public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
    297         super.onInitializeAccessibilityNodeInfo(info);
    298         mDelegate.onInitializeAccessibilityNodeInfo(info);
    299     }
    300 
    301     @Override
    302     protected void onConfigurationChanged(Configuration newConfig) {
    303         super.onConfigurationChanged(newConfig);
    304         mDelegate.onConfigurationChanged(newConfig);
    305     }
    306 
    307     /**
    308      * Sets the first day of week.
    309      *
    310      * @param firstDayOfWeek The first day of the week conforming to the
    311      *            {@link CalendarView} APIs.
    312      * @see Calendar#SUNDAY
    313      * @see Calendar#MONDAY
    314      * @see Calendar#TUESDAY
    315      * @see Calendar#WEDNESDAY
    316      * @see Calendar#THURSDAY
    317      * @see Calendar#FRIDAY
    318      * @see Calendar#SATURDAY
    319      *
    320      * @attr ref android.R.styleable#DatePicker_firstDayOfWeek
    321      */
    322     public void setFirstDayOfWeek(int firstDayOfWeek) {
    323         if (firstDayOfWeek < Calendar.SUNDAY || firstDayOfWeek > Calendar.SATURDAY) {
    324             throw new IllegalArgumentException("firstDayOfWeek must be between 1 and 7");
    325         }
    326         mDelegate.setFirstDayOfWeek(firstDayOfWeek);
    327     }
    328 
    329     /**
    330      * Gets the first day of week.
    331      *
    332      * @return The first day of the week conforming to the {@link CalendarView}
    333      *         APIs.
    334      * @see Calendar#SUNDAY
    335      * @see Calendar#MONDAY
    336      * @see Calendar#TUESDAY
    337      * @see Calendar#WEDNESDAY
    338      * @see Calendar#THURSDAY
    339      * @see Calendar#FRIDAY
    340      * @see Calendar#SATURDAY
    341      *
    342      * @attr ref android.R.styleable#DatePicker_firstDayOfWeek
    343      */
    344     public int getFirstDayOfWeek() {
    345         return mDelegate.getFirstDayOfWeek();
    346     }
    347 
    348     /**
    349      * Gets whether the {@link CalendarView} is shown.
    350      *
    351      * @return True if the calendar view is shown.
    352      * @see #getCalendarView()
    353      */
    354     public boolean getCalendarViewShown() {
    355         return mDelegate.getCalendarViewShown();
    356     }
    357 
    358     /**
    359      * Gets the {@link CalendarView}.
    360      *
    361      * @return The calendar view.
    362      * @see #getCalendarViewShown()
    363      */
    364     public CalendarView getCalendarView() {
    365         return mDelegate.getCalendarView();
    366     }
    367 
    368     /**
    369      * Sets whether the {@link CalendarView} is shown.
    370      *
    371      * @param shown True if the calendar view is to be shown.
    372      */
    373     public void setCalendarViewShown(boolean shown) {
    374         mDelegate.setCalendarViewShown(shown);
    375     }
    376 
    377     /**
    378      * Gets whether the spinners are shown.
    379      *
    380      * @return True if the spinners are shown.
    381      */
    382     public boolean getSpinnersShown() {
    383         return mDelegate.getSpinnersShown();
    384     }
    385 
    386     /**
    387      * Sets whether the spinners are shown.
    388      *
    389      * @param shown True if the spinners are to be shown.
    390      */
    391     public void setSpinnersShown(boolean shown) {
    392         mDelegate.setSpinnersShown(shown);
    393     }
    394 
    395     @Override
    396     protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
    397         dispatchThawSelfOnly(container);
    398     }
    399 
    400     @Override
    401     protected Parcelable onSaveInstanceState() {
    402         Parcelable superState = super.onSaveInstanceState();
    403         return mDelegate.onSaveInstanceState(superState);
    404     }
    405 
    406     @Override
    407     protected void onRestoreInstanceState(Parcelable state) {
    408         BaseSavedState ss = (BaseSavedState) state;
    409         super.onRestoreInstanceState(ss.getSuperState());
    410         mDelegate.onRestoreInstanceState(ss);
    411     }
    412 
    413     /**
    414      * A delegate interface that defined the public API of the DatePicker. Allows different
    415      * DatePicker implementations. This would need to be implemented by the DatePicker delegates
    416      * for the real behavior.
    417      *
    418      * @hide
    419      */
    420     interface DatePickerDelegate {
    421         void init(int year, int monthOfYear, int dayOfMonth,
    422                   OnDateChangedListener onDateChangedListener);
    423 
    424         void updateDate(int year, int month, int dayOfMonth);
    425 
    426         int getYear();
    427         int getMonth();
    428         int getDayOfMonth();
    429 
    430         void setFirstDayOfWeek(int firstDayOfWeek);
    431         int getFirstDayOfWeek();
    432 
    433         void setMinDate(long minDate);
    434         Calendar getMinDate();
    435 
    436         void setMaxDate(long maxDate);
    437         Calendar getMaxDate();
    438 
    439         void setEnabled(boolean enabled);
    440         boolean isEnabled();
    441 
    442         CalendarView getCalendarView();
    443 
    444         void setCalendarViewShown(boolean shown);
    445         boolean getCalendarViewShown();
    446 
    447         void setSpinnersShown(boolean shown);
    448         boolean getSpinnersShown();
    449 
    450         void setValidationCallback(ValidationCallback callback);
    451 
    452         void onConfigurationChanged(Configuration newConfig);
    453 
    454         Parcelable onSaveInstanceState(Parcelable superState);
    455         void onRestoreInstanceState(Parcelable state);
    456 
    457         boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event);
    458         void onPopulateAccessibilityEvent(AccessibilityEvent event);
    459         void onInitializeAccessibilityEvent(AccessibilityEvent event);
    460         void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info);
    461     }
    462 
    463     /**
    464      * An abstract class which can be used as a start for DatePicker implementations
    465      */
    466     abstract static class AbstractDatePickerDelegate implements DatePickerDelegate {
    467         // The delegator
    468         protected DatePicker mDelegator;
    469 
    470         // The context
    471         protected Context mContext;
    472 
    473         // The current locale
    474         protected Locale mCurrentLocale;
    475 
    476         // Callbacks
    477         protected OnDateChangedListener mOnDateChangedListener;
    478         protected ValidationCallback mValidationCallback;
    479 
    480         public AbstractDatePickerDelegate(DatePicker delegator, Context context) {
    481             mDelegator = delegator;
    482             mContext = context;
    483 
    484             // initialization based on locale
    485             setCurrentLocale(Locale.getDefault());
    486         }
    487 
    488         protected void setCurrentLocale(Locale locale) {
    489             if (locale.equals(mCurrentLocale)) {
    490                 return;
    491             }
    492             mCurrentLocale = locale;
    493         }
    494 
    495         @Override
    496         public void setValidationCallback(ValidationCallback callback) {
    497             mValidationCallback = callback;
    498         }
    499 
    500         protected void onValidationChanged(boolean valid) {
    501             if (mValidationCallback != null) {
    502                 mValidationCallback.onValidationChanged(valid);
    503             }
    504         }
    505     }
    506 
    507     /**
    508      * A callback interface for updating input validity when the date picker
    509      * when included into a dialog.
    510      *
    511      * @hide
    512      */
    513     public static interface ValidationCallback {
    514         void onValidationChanged(boolean valid);
    515     }
    516 
    517     /**
    518      * A delegate implementing the basic DatePicker
    519      */
    520     private static class DatePickerSpinnerDelegate extends AbstractDatePickerDelegate {
    521 
    522         private static final String DATE_FORMAT = "MM/dd/yyyy";
    523 
    524         private static final int DEFAULT_START_YEAR = 1900;
    525 
    526         private static final int DEFAULT_END_YEAR = 2100;
    527 
    528         private static final boolean DEFAULT_CALENDAR_VIEW_SHOWN = true;
    529 
    530         private static final boolean DEFAULT_SPINNERS_SHOWN = true;
    531 
    532         private static final boolean DEFAULT_ENABLED_STATE = true;
    533 
    534         private final LinearLayout mSpinners;
    535 
    536         private final NumberPicker mDaySpinner;
    537 
    538         private final NumberPicker mMonthSpinner;
    539 
    540         private final NumberPicker mYearSpinner;
    541 
    542         private final EditText mDaySpinnerInput;
    543 
    544         private final EditText mMonthSpinnerInput;
    545 
    546         private final EditText mYearSpinnerInput;
    547 
    548         private final CalendarView mCalendarView;
    549 
    550         private String[] mShortMonths;
    551 
    552         private final java.text.DateFormat mDateFormat = new SimpleDateFormat(DATE_FORMAT);
    553 
    554         private int mNumberOfMonths;
    555 
    556         private Calendar mTempDate;
    557 
    558         private Calendar mMinDate;
    559 
    560         private Calendar mMaxDate;
    561 
    562         private Calendar mCurrentDate;
    563 
    564         private boolean mIsEnabled = DEFAULT_ENABLED_STATE;
    565 
    566         DatePickerSpinnerDelegate(DatePicker delegator, Context context, AttributeSet attrs,
    567                 int defStyleAttr, int defStyleRes) {
    568             super(delegator, context);
    569 
    570             mDelegator = delegator;
    571             mContext = context;
    572 
    573             // initialization based on locale
    574             setCurrentLocale(Locale.getDefault());
    575 
    576             final TypedArray attributesArray = context.obtainStyledAttributes(attrs,
    577                     R.styleable.DatePicker, defStyleAttr, defStyleRes);
    578             boolean spinnersShown = attributesArray.getBoolean(R.styleable.DatePicker_spinnersShown,
    579                     DEFAULT_SPINNERS_SHOWN);
    580             boolean calendarViewShown = attributesArray.getBoolean(
    581                     R.styleable.DatePicker_calendarViewShown, DEFAULT_CALENDAR_VIEW_SHOWN);
    582             int startYear = attributesArray.getInt(R.styleable.DatePicker_startYear,
    583                     DEFAULT_START_YEAR);
    584             int endYear = attributesArray.getInt(R.styleable.DatePicker_endYear, DEFAULT_END_YEAR);
    585             String minDate = attributesArray.getString(R.styleable.DatePicker_minDate);
    586             String maxDate = attributesArray.getString(R.styleable.DatePicker_maxDate);
    587             int layoutResourceId = attributesArray.getResourceId(
    588                     R.styleable.DatePicker_legacyLayout, R.layout.date_picker_legacy);
    589             attributesArray.recycle();
    590 
    591             LayoutInflater inflater = (LayoutInflater) context
    592                     .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    593             inflater.inflate(layoutResourceId, mDelegator, true);
    594 
    595             OnValueChangeListener onChangeListener = new OnValueChangeListener() {
    596                 public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
    597                     updateInputState();
    598                     mTempDate.setTimeInMillis(mCurrentDate.getTimeInMillis());
    599                     // take care of wrapping of days and months to update greater fields
    600                     if (picker == mDaySpinner) {
    601                         int maxDayOfMonth = mTempDate.getActualMaximum(Calendar.DAY_OF_MONTH);
    602                         if (oldVal == maxDayOfMonth && newVal == 1) {
    603                             mTempDate.add(Calendar.DAY_OF_MONTH, 1);
    604                         } else if (oldVal == 1 && newVal == maxDayOfMonth) {
    605                             mTempDate.add(Calendar.DAY_OF_MONTH, -1);
    606                         } else {
    607                             mTempDate.add(Calendar.DAY_OF_MONTH, newVal - oldVal);
    608                         }
    609                     } else if (picker == mMonthSpinner) {
    610                         if (oldVal == 11 && newVal == 0) {
    611                             mTempDate.add(Calendar.MONTH, 1);
    612                         } else if (oldVal == 0 && newVal == 11) {
    613                             mTempDate.add(Calendar.MONTH, -1);
    614                         } else {
    615                             mTempDate.add(Calendar.MONTH, newVal - oldVal);
    616                         }
    617                     } else if (picker == mYearSpinner) {
    618                         mTempDate.set(Calendar.YEAR, newVal);
    619                     } else {
    620                         throw new IllegalArgumentException();
    621                     }
    622                     // now set the date to the adjusted one
    623                     setDate(mTempDate.get(Calendar.YEAR), mTempDate.get(Calendar.MONTH),
    624                             mTempDate.get(Calendar.DAY_OF_MONTH));
    625                     updateSpinners();
    626                     updateCalendarView();
    627                     notifyDateChanged();
    628                 }
    629             };
    630 
    631             mSpinners = (LinearLayout) mDelegator.findViewById(R.id.pickers);
    632 
    633             // calendar view day-picker
    634             mCalendarView = (CalendarView) mDelegator.findViewById(R.id.calendar_view);
    635             mCalendarView.setOnDateChangeListener(new CalendarView.OnDateChangeListener() {
    636                 public void onSelectedDayChange(CalendarView view, int year, int month, int monthDay) {
    637                     setDate(year, month, monthDay);
    638                     updateSpinners();
    639                     notifyDateChanged();
    640                 }
    641             });
    642 
    643             // day
    644             mDaySpinner = (NumberPicker) mDelegator.findViewById(R.id.day);
    645             mDaySpinner.setFormatter(NumberPicker.getTwoDigitFormatter());
    646             mDaySpinner.setOnLongPressUpdateInterval(100);
    647             mDaySpinner.setOnValueChangedListener(onChangeListener);
    648             mDaySpinnerInput = (EditText) mDaySpinner.findViewById(R.id.numberpicker_input);
    649 
    650             // month
    651             mMonthSpinner = (NumberPicker) mDelegator.findViewById(R.id.month);
    652             mMonthSpinner.setMinValue(0);
    653             mMonthSpinner.setMaxValue(mNumberOfMonths - 1);
    654             mMonthSpinner.setDisplayedValues(mShortMonths);
    655             mMonthSpinner.setOnLongPressUpdateInterval(200);
    656             mMonthSpinner.setOnValueChangedListener(onChangeListener);
    657             mMonthSpinnerInput = (EditText) mMonthSpinner.findViewById(R.id.numberpicker_input);
    658 
    659             // year
    660             mYearSpinner = (NumberPicker) mDelegator.findViewById(R.id.year);
    661             mYearSpinner.setOnLongPressUpdateInterval(100);
    662             mYearSpinner.setOnValueChangedListener(onChangeListener);
    663             mYearSpinnerInput = (EditText) mYearSpinner.findViewById(R.id.numberpicker_input);
    664 
    665             // show only what the user required but make sure we
    666             // show something and the spinners have higher priority
    667             if (!spinnersShown && !calendarViewShown) {
    668                 setSpinnersShown(true);
    669             } else {
    670                 setSpinnersShown(spinnersShown);
    671                 setCalendarViewShown(calendarViewShown);
    672             }
    673 
    674             // set the min date giving priority of the minDate over startYear
    675             mTempDate.clear();
    676             if (!TextUtils.isEmpty(minDate)) {
    677                 if (!parseDate(minDate, mTempDate)) {
    678                     mTempDate.set(startYear, 0, 1);
    679                 }
    680             } else {
    681                 mTempDate.set(startYear, 0, 1);
    682             }
    683             setMinDate(mTempDate.getTimeInMillis());
    684 
    685             // set the max date giving priority of the maxDate over endYear
    686             mTempDate.clear();
    687             if (!TextUtils.isEmpty(maxDate)) {
    688                 if (!parseDate(maxDate, mTempDate)) {
    689                     mTempDate.set(endYear, 11, 31);
    690                 }
    691             } else {
    692                 mTempDate.set(endYear, 11, 31);
    693             }
    694             setMaxDate(mTempDate.getTimeInMillis());
    695 
    696             // initialize to current date
    697             mCurrentDate.setTimeInMillis(System.currentTimeMillis());
    698             init(mCurrentDate.get(Calendar.YEAR), mCurrentDate.get(Calendar.MONTH), mCurrentDate
    699                     .get(Calendar.DAY_OF_MONTH), null);
    700 
    701             // re-order the number spinners to match the current date format
    702             reorderSpinners();
    703 
    704             // accessibility
    705             setContentDescriptions();
    706 
    707             // If not explicitly specified this view is important for accessibility.
    708             if (mDelegator.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
    709                 mDelegator.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
    710             }
    711         }
    712 
    713         @Override
    714         public void init(int year, int monthOfYear, int dayOfMonth,
    715                          OnDateChangedListener onDateChangedListener) {
    716             setDate(year, monthOfYear, dayOfMonth);
    717             updateSpinners();
    718             updateCalendarView();
    719             mOnDateChangedListener = onDateChangedListener;
    720         }
    721 
    722         @Override
    723         public void updateDate(int year, int month, int dayOfMonth) {
    724             if (!isNewDate(year, month, dayOfMonth)) {
    725                 return;
    726             }
    727             setDate(year, month, dayOfMonth);
    728             updateSpinners();
    729             updateCalendarView();
    730             notifyDateChanged();
    731         }
    732 
    733         @Override
    734         public int getYear() {
    735             return mCurrentDate.get(Calendar.YEAR);
    736         }
    737 
    738         @Override
    739         public int getMonth() {
    740             return mCurrentDate.get(Calendar.MONTH);
    741         }
    742 
    743         @Override
    744         public int getDayOfMonth() {
    745             return mCurrentDate.get(Calendar.DAY_OF_MONTH);
    746         }
    747 
    748         @Override
    749         public void setFirstDayOfWeek(int firstDayOfWeek) {
    750             mCalendarView.setFirstDayOfWeek(firstDayOfWeek);
    751         }
    752 
    753         @Override
    754         public int getFirstDayOfWeek() {
    755             return mCalendarView.getFirstDayOfWeek();
    756         }
    757 
    758         @Override
    759         public void setMinDate(long minDate) {
    760             mTempDate.setTimeInMillis(minDate);
    761             if (mTempDate.get(Calendar.YEAR) == mMinDate.get(Calendar.YEAR)
    762                     && mTempDate.get(Calendar.DAY_OF_YEAR) != mMinDate.get(Calendar.DAY_OF_YEAR)) {
    763                 return;
    764             }
    765             mMinDate.setTimeInMillis(minDate);
    766             mCalendarView.setMinDate(minDate);
    767             if (mCurrentDate.before(mMinDate)) {
    768                 mCurrentDate.setTimeInMillis(mMinDate.getTimeInMillis());
    769                 updateCalendarView();
    770             }
    771             updateSpinners();
    772         }
    773 
    774         @Override
    775         public Calendar getMinDate() {
    776             final Calendar minDate = Calendar.getInstance();
    777             minDate.setTimeInMillis(mCalendarView.getMinDate());
    778             return minDate;
    779         }
    780 
    781         @Override
    782         public void setMaxDate(long maxDate) {
    783             mTempDate.setTimeInMillis(maxDate);
    784             if (mTempDate.get(Calendar.YEAR) == mMaxDate.get(Calendar.YEAR)
    785                     && mTempDate.get(Calendar.DAY_OF_YEAR) != mMaxDate.get(Calendar.DAY_OF_YEAR)) {
    786                 return;
    787             }
    788             mMaxDate.setTimeInMillis(maxDate);
    789             mCalendarView.setMaxDate(maxDate);
    790             if (mCurrentDate.after(mMaxDate)) {
    791                 mCurrentDate.setTimeInMillis(mMaxDate.getTimeInMillis());
    792                 updateCalendarView();
    793             }
    794             updateSpinners();
    795         }
    796 
    797         @Override
    798         public Calendar getMaxDate() {
    799             final Calendar maxDate = Calendar.getInstance();
    800             maxDate.setTimeInMillis(mCalendarView.getMaxDate());
    801             return maxDate;
    802         }
    803 
    804         @Override
    805         public void setEnabled(boolean enabled) {
    806             mDaySpinner.setEnabled(enabled);
    807             mMonthSpinner.setEnabled(enabled);
    808             mYearSpinner.setEnabled(enabled);
    809             mCalendarView.setEnabled(enabled);
    810             mIsEnabled = enabled;
    811         }
    812 
    813         @Override
    814         public boolean isEnabled() {
    815             return mIsEnabled;
    816         }
    817 
    818         @Override
    819         public CalendarView getCalendarView() {
    820             return mCalendarView;
    821         }
    822 
    823         @Override
    824         public void setCalendarViewShown(boolean shown) {
    825             mCalendarView.setVisibility(shown ? VISIBLE : GONE);
    826         }
    827 
    828         @Override
    829         public boolean getCalendarViewShown() {
    830             return (mCalendarView.getVisibility() == View.VISIBLE);
    831         }
    832 
    833         @Override
    834         public void setSpinnersShown(boolean shown) {
    835             mSpinners.setVisibility(shown ? VISIBLE : GONE);
    836         }
    837 
    838         @Override
    839         public boolean getSpinnersShown() {
    840             return mSpinners.isShown();
    841         }
    842 
    843         @Override
    844         public void onConfigurationChanged(Configuration newConfig) {
    845             setCurrentLocale(newConfig.locale);
    846         }
    847 
    848         @Override
    849         public Parcelable onSaveInstanceState(Parcelable superState) {
    850             return new SavedState(superState, getYear(), getMonth(), getDayOfMonth());
    851         }
    852 
    853         @Override
    854         public void onRestoreInstanceState(Parcelable state) {
    855             SavedState ss = (SavedState) state;
    856             setDate(ss.mYear, ss.mMonth, ss.mDay);
    857             updateSpinners();
    858             updateCalendarView();
    859         }
    860 
    861         @Override
    862         public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
    863             onPopulateAccessibilityEvent(event);
    864             return true;
    865         }
    866 
    867         @Override
    868         public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
    869             final int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR;
    870             String selectedDateUtterance = DateUtils.formatDateTime(mContext,
    871                     mCurrentDate.getTimeInMillis(), flags);
    872             event.getText().add(selectedDateUtterance);
    873         }
    874 
    875         @Override
    876         public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
    877             event.setClassName(DatePicker.class.getName());
    878         }
    879 
    880         @Override
    881         public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
    882             info.setClassName(DatePicker.class.getName());
    883         }
    884 
    885         /**
    886          * Sets the current locale.
    887          *
    888          * @param locale The current locale.
    889          */
    890         @Override
    891         protected void setCurrentLocale(Locale locale) {
    892             super.setCurrentLocale(locale);
    893 
    894             mTempDate = getCalendarForLocale(mTempDate, locale);
    895             mMinDate = getCalendarForLocale(mMinDate, locale);
    896             mMaxDate = getCalendarForLocale(mMaxDate, locale);
    897             mCurrentDate = getCalendarForLocale(mCurrentDate, locale);
    898 
    899             mNumberOfMonths = mTempDate.getActualMaximum(Calendar.MONTH) + 1;
    900             mShortMonths = new DateFormatSymbols().getShortMonths();
    901 
    902             if (usingNumericMonths()) {
    903                 // We're in a locale where a date should either be all-numeric, or all-text.
    904                 // All-text would require custom NumberPicker formatters for day and year.
    905                 mShortMonths = new String[mNumberOfMonths];
    906                 for (int i = 0; i < mNumberOfMonths; ++i) {
    907                     mShortMonths[i] = String.format("%d", i + 1);
    908                 }
    909             }
    910         }
    911 
    912         /**
    913          * Tests whether the current locale is one where there are no real month names,
    914          * such as Chinese, Japanese, or Korean locales.
    915          */
    916         private boolean usingNumericMonths() {
    917             return Character.isDigit(mShortMonths[Calendar.JANUARY].charAt(0));
    918         }
    919 
    920         /**
    921          * Gets a calendar for locale bootstrapped with the value of a given calendar.
    922          *
    923          * @param oldCalendar The old calendar.
    924          * @param locale The locale.
    925          */
    926         private Calendar getCalendarForLocale(Calendar oldCalendar, Locale locale) {
    927             if (oldCalendar == null) {
    928                 return Calendar.getInstance(locale);
    929             } else {
    930                 final long currentTimeMillis = oldCalendar.getTimeInMillis();
    931                 Calendar newCalendar = Calendar.getInstance(locale);
    932                 newCalendar.setTimeInMillis(currentTimeMillis);
    933                 return newCalendar;
    934             }
    935         }
    936 
    937         /**
    938          * Reorders the spinners according to the date format that is
    939          * explicitly set by the user and if no such is set fall back
    940          * to the current locale's default format.
    941          */
    942         private void reorderSpinners() {
    943             mSpinners.removeAllViews();
    944             // We use numeric spinners for year and day, but textual months. Ask icu4c what
    945             // order the user's locale uses for that combination. http://b/7207103.
    946             String pattern = DateFormat.getBestDateTimePattern(Locale.getDefault(), "yyyyMMMdd");
    947             char[] order = ICU.getDateFormatOrder(pattern);
    948             final int spinnerCount = order.length;
    949             for (int i = 0; i < spinnerCount; i++) {
    950                 switch (order[i]) {
    951                     case 'd':
    952                         mSpinners.addView(mDaySpinner);
    953                         setImeOptions(mDaySpinner, spinnerCount, i);
    954                         break;
    955                     case 'M':
    956                         mSpinners.addView(mMonthSpinner);
    957                         setImeOptions(mMonthSpinner, spinnerCount, i);
    958                         break;
    959                     case 'y':
    960                         mSpinners.addView(mYearSpinner);
    961                         setImeOptions(mYearSpinner, spinnerCount, i);
    962                         break;
    963                     default:
    964                         throw new IllegalArgumentException(Arrays.toString(order));
    965                 }
    966             }
    967         }
    968 
    969         /**
    970          * Parses the given <code>date</code> and in case of success sets the result
    971          * to the <code>outDate</code>.
    972          *
    973          * @return True if the date was parsed.
    974          */
    975         private boolean parseDate(String date, Calendar outDate) {
    976             try {
    977                 outDate.setTime(mDateFormat.parse(date));
    978                 return true;
    979             } catch (ParseException e) {
    980                 Log.w(LOG_TAG, "Date: " + date + " not in format: " + DATE_FORMAT);
    981                 return false;
    982             }
    983         }
    984 
    985         private boolean isNewDate(int year, int month, int dayOfMonth) {
    986             return (mCurrentDate.get(Calendar.YEAR) != year
    987                     || mCurrentDate.get(Calendar.MONTH) != dayOfMonth
    988                     || mCurrentDate.get(Calendar.DAY_OF_MONTH) != month);
    989         }
    990 
    991         private void setDate(int year, int month, int dayOfMonth) {
    992             mCurrentDate.set(year, month, dayOfMonth);
    993             if (mCurrentDate.before(mMinDate)) {
    994                 mCurrentDate.setTimeInMillis(mMinDate.getTimeInMillis());
    995             } else if (mCurrentDate.after(mMaxDate)) {
    996                 mCurrentDate.setTimeInMillis(mMaxDate.getTimeInMillis());
    997             }
    998         }
    999 
   1000         private void updateSpinners() {
   1001             // set the spinner ranges respecting the min and max dates
   1002             if (mCurrentDate.equals(mMinDate)) {
   1003                 mDaySpinner.setMinValue(mCurrentDate.get(Calendar.DAY_OF_MONTH));
   1004                 mDaySpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.DAY_OF_MONTH));
   1005                 mDaySpinner.setWrapSelectorWheel(false);
   1006                 mMonthSpinner.setDisplayedValues(null);
   1007                 mMonthSpinner.setMinValue(mCurrentDate.get(Calendar.MONTH));
   1008                 mMonthSpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.MONTH));
   1009                 mMonthSpinner.setWrapSelectorWheel(false);
   1010             } else if (mCurrentDate.equals(mMaxDate)) {
   1011                 mDaySpinner.setMinValue(mCurrentDate.getActualMinimum(Calendar.DAY_OF_MONTH));
   1012                 mDaySpinner.setMaxValue(mCurrentDate.get(Calendar.DAY_OF_MONTH));
   1013                 mDaySpinner.setWrapSelectorWheel(false);
   1014                 mMonthSpinner.setDisplayedValues(null);
   1015                 mMonthSpinner.setMinValue(mCurrentDate.getActualMinimum(Calendar.MONTH));
   1016                 mMonthSpinner.setMaxValue(mCurrentDate.get(Calendar.MONTH));
   1017                 mMonthSpinner.setWrapSelectorWheel(false);
   1018             } else {
   1019                 mDaySpinner.setMinValue(1);
   1020                 mDaySpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.DAY_OF_MONTH));
   1021                 mDaySpinner.setWrapSelectorWheel(true);
   1022                 mMonthSpinner.setDisplayedValues(null);
   1023                 mMonthSpinner.setMinValue(0);
   1024                 mMonthSpinner.setMaxValue(11);
   1025                 mMonthSpinner.setWrapSelectorWheel(true);
   1026             }
   1027 
   1028             // make sure the month names are a zero based array
   1029             // with the months in the month spinner
   1030             String[] displayedValues = Arrays.copyOfRange(mShortMonths,
   1031                     mMonthSpinner.getMinValue(), mMonthSpinner.getMaxValue() + 1);
   1032             mMonthSpinner.setDisplayedValues(displayedValues);
   1033 
   1034             // year spinner range does not change based on the current date
   1035             mYearSpinner.setMinValue(mMinDate.get(Calendar.YEAR));
   1036             mYearSpinner.setMaxValue(mMaxDate.get(Calendar.YEAR));
   1037             mYearSpinner.setWrapSelectorWheel(false);
   1038 
   1039             // set the spinner values
   1040             mYearSpinner.setValue(mCurrentDate.get(Calendar.YEAR));
   1041             mMonthSpinner.setValue(mCurrentDate.get(Calendar.MONTH));
   1042             mDaySpinner.setValue(mCurrentDate.get(Calendar.DAY_OF_MONTH));
   1043 
   1044             if (usingNumericMonths()) {
   1045                 mMonthSpinnerInput.setRawInputType(InputType.TYPE_CLASS_NUMBER);
   1046             }
   1047         }
   1048 
   1049         /**
   1050          * Updates the calendar view with the current date.
   1051          */
   1052         private void updateCalendarView() {
   1053             mCalendarView.setDate(mCurrentDate.getTimeInMillis(), false, false);
   1054         }
   1055 
   1056 
   1057         /**
   1058          * Notifies the listener, if such, for a change in the selected date.
   1059          */
   1060         private void notifyDateChanged() {
   1061             mDelegator.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
   1062             if (mOnDateChangedListener != null) {
   1063                 mOnDateChangedListener.onDateChanged(mDelegator, getYear(), getMonth(),
   1064                         getDayOfMonth());
   1065             }
   1066         }
   1067 
   1068         /**
   1069          * Sets the IME options for a spinner based on its ordering.
   1070          *
   1071          * @param spinner The spinner.
   1072          * @param spinnerCount The total spinner count.
   1073          * @param spinnerIndex The index of the given spinner.
   1074          */
   1075         private void setImeOptions(NumberPicker spinner, int spinnerCount, int spinnerIndex) {
   1076             final int imeOptions;
   1077             if (spinnerIndex < spinnerCount - 1) {
   1078                 imeOptions = EditorInfo.IME_ACTION_NEXT;
   1079             } else {
   1080                 imeOptions = EditorInfo.IME_ACTION_DONE;
   1081             }
   1082             TextView input = (TextView) spinner.findViewById(R.id.numberpicker_input);
   1083             input.setImeOptions(imeOptions);
   1084         }
   1085 
   1086         private void setContentDescriptions() {
   1087             // Day
   1088             trySetContentDescription(mDaySpinner, R.id.increment,
   1089                     R.string.date_picker_increment_day_button);
   1090             trySetContentDescription(mDaySpinner, R.id.decrement,
   1091                     R.string.date_picker_decrement_day_button);
   1092             // Month
   1093             trySetContentDescription(mMonthSpinner, R.id.increment,
   1094                     R.string.date_picker_increment_month_button);
   1095             trySetContentDescription(mMonthSpinner, R.id.decrement,
   1096                     R.string.date_picker_decrement_month_button);
   1097             // Year
   1098             trySetContentDescription(mYearSpinner, R.id.increment,
   1099                     R.string.date_picker_increment_year_button);
   1100             trySetContentDescription(mYearSpinner, R.id.decrement,
   1101                     R.string.date_picker_decrement_year_button);
   1102         }
   1103 
   1104         private void trySetContentDescription(View root, int viewId, int contDescResId) {
   1105             View target = root.findViewById(viewId);
   1106             if (target != null) {
   1107                 target.setContentDescription(mContext.getString(contDescResId));
   1108             }
   1109         }
   1110 
   1111         private void updateInputState() {
   1112             // Make sure that if the user changes the value and the IME is active
   1113             // for one of the inputs if this widget, the IME is closed. If the user
   1114             // changed the value via the IME and there is a next input the IME will
   1115             // be shown, otherwise the user chose another means of changing the
   1116             // value and having the IME up makes no sense.
   1117             InputMethodManager inputMethodManager = InputMethodManager.peekInstance();
   1118             if (inputMethodManager != null) {
   1119                 if (inputMethodManager.isActive(mYearSpinnerInput)) {
   1120                     mYearSpinnerInput.clearFocus();
   1121                     inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0);
   1122                 } else if (inputMethodManager.isActive(mMonthSpinnerInput)) {
   1123                     mMonthSpinnerInput.clearFocus();
   1124                     inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0);
   1125                 } else if (inputMethodManager.isActive(mDaySpinnerInput)) {
   1126                     mDaySpinnerInput.clearFocus();
   1127                     inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0);
   1128                 }
   1129             }
   1130         }
   1131     }
   1132 
   1133     /**
   1134      * Class for managing state storing/restoring.
   1135      */
   1136     private static class SavedState extends BaseSavedState {
   1137 
   1138         private final int mYear;
   1139 
   1140         private final int mMonth;
   1141 
   1142         private final int mDay;
   1143 
   1144         /**
   1145          * Constructor called from {@link DatePicker#onSaveInstanceState()}
   1146          */
   1147         private SavedState(Parcelable superState, int year, int month, int day) {
   1148             super(superState);
   1149             mYear = year;
   1150             mMonth = month;
   1151             mDay = day;
   1152         }
   1153 
   1154         /**
   1155          * Constructor called from {@link #CREATOR}
   1156          */
   1157         private SavedState(Parcel in) {
   1158             super(in);
   1159             mYear = in.readInt();
   1160             mMonth = in.readInt();
   1161             mDay = in.readInt();
   1162         }
   1163 
   1164         @Override
   1165         public void writeToParcel(Parcel dest, int flags) {
   1166             super.writeToParcel(dest, flags);
   1167             dest.writeInt(mYear);
   1168             dest.writeInt(mMonth);
   1169             dest.writeInt(mDay);
   1170         }
   1171 
   1172         @SuppressWarnings("all")
   1173         // suppress unused and hiding
   1174         public static final Parcelable.Creator<SavedState> CREATOR = new Creator<SavedState>() {
   1175 
   1176             public SavedState createFromParcel(Parcel in) {
   1177                 return new SavedState(in);
   1178             }
   1179 
   1180             public SavedState[] newArray(int size) {
   1181                 return new SavedState[size];
   1182             }
   1183         };
   1184     }
   1185 }
   1186