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