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