Home | History | Annotate | Download | only in datepicker
      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 com.android.contacts.datepicker;
     18 
     19 // This is a fork of the standard Android DatePicker that additionally allows toggling the year
     20 // on/off. It uses some private API so that not everything has to be copied.
     21 
     22 import android.animation.LayoutTransition;
     23 import android.annotation.Widget;
     24 import android.content.Context;
     25 import android.content.res.TypedArray;
     26 import android.os.Parcel;
     27 import android.os.Parcelable;
     28 import android.text.format.DateFormat;
     29 import android.util.AttributeSet;
     30 import android.util.SparseArray;
     31 import android.view.LayoutInflater;
     32 import android.view.View;
     33 import android.widget.CheckBox;
     34 import android.widget.CompoundButton;
     35 import android.widget.CompoundButton.OnCheckedChangeListener;
     36 import android.widget.FrameLayout;
     37 import android.widget.LinearLayout;
     38 import android.widget.NumberPicker;
     39 import android.widget.NumberPicker.OnValueChangeListener;
     40 
     41 import com.android.contacts.R;
     42 
     43 import java.text.DateFormatSymbols;
     44 import java.text.SimpleDateFormat;
     45 import java.util.Calendar;
     46 
     47 /**
     48  * A view for selecting a month / year / day based on a calendar like layout.
     49  *
     50  * <p>See the <a href="{@docRoot}resources/tutorials/views/hello-datepicker.html">Date Picker
     51  * tutorial</a>.</p>
     52  *
     53  * For a dialog using this view, see {@link android.app.DatePickerDialog}.
     54  */
     55 @Widget
     56 public class DatePicker extends FrameLayout {
     57     /** Magic year that represents "no year" */
     58     public static int NO_YEAR = 0;
     59 
     60     private static final int DEFAULT_START_YEAR = 1900;
     61     private static final int DEFAULT_END_YEAR = 2100;
     62 
     63     /* UI Components */
     64     private final LinearLayout mPickerContainer;
     65     private final CheckBox mYearToggle;
     66     private final NumberPicker mDayPicker;
     67     private final NumberPicker mMonthPicker;
     68     private final NumberPicker mYearPicker;
     69 
     70     /**
     71      * How we notify users the date has changed.
     72      */
     73     private OnDateChangedListener mOnDateChangedListener;
     74 
     75     private int mDay;
     76     private int mMonth;
     77     private int mYear;
     78     private boolean mYearOptional;
     79     private boolean mHasYear;
     80 
     81     /**
     82      * The callback used to indicate the user changes the date.
     83      */
     84     public interface OnDateChangedListener {
     85 
     86         /**
     87          * @param view The view associated with this listener.
     88          * @param year The year that was set or {@link DatePicker#NO_YEAR} if no year was set
     89          * @param monthOfYear The month that was set (0-11) for compatibility
     90          *  with {@link java.util.Calendar}.
     91          * @param dayOfMonth The day of the month that was set.
     92          */
     93         void onDateChanged(DatePicker view, int year, int monthOfYear, int dayOfMonth);
     94     }
     95 
     96     public DatePicker(Context context) {
     97         this(context, null);
     98     }
     99 
    100     public DatePicker(Context context, AttributeSet attrs) {
    101         this(context, attrs, 0);
    102     }
    103 
    104     public DatePicker(Context context, AttributeSet attrs, int defStyle) {
    105         super(context, attrs, defStyle);
    106 
    107         LayoutInflater inflater = (LayoutInflater) context.getSystemService(
    108                 Context.LAYOUT_INFLATER_SERVICE);
    109         inflater.inflate(R.layout.date_picker, this, true);
    110 
    111         mPickerContainer = (LinearLayout) findViewById(R.id.parent);
    112         mDayPicker = (NumberPicker) findViewById(R.id.day);
    113         mDayPicker.setFormatter(NumberPicker.getTwoDigitFormatter());
    114         mDayPicker.setOnLongPressUpdateInterval(100);
    115         mDayPicker.setOnValueChangedListener(new OnValueChangeListener() {
    116             @Override
    117             public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
    118                 mDay = newVal;
    119                 notifyDateChanged();
    120             }
    121         });
    122         mMonthPicker = (NumberPicker) findViewById(R.id.month);
    123         mMonthPicker.setFormatter(NumberPicker.getTwoDigitFormatter());
    124         DateFormatSymbols dfs = new DateFormatSymbols();
    125         String[] months = dfs.getShortMonths();
    126 
    127         /*
    128          * If the user is in a locale where the month names are numeric,
    129          * use just the number instead of the "month" character for
    130          * consistency with the other fields.
    131          */
    132         if (months[0].startsWith("1")) {
    133             for (int i = 0; i < months.length; i++) {
    134                 months[i] = String.valueOf(i + 1);
    135             }
    136             mMonthPicker.setMinValue(1);
    137             mMonthPicker.setMaxValue(12);
    138         } else {
    139             mMonthPicker.setMinValue(1);
    140             mMonthPicker.setMaxValue(12);
    141             mMonthPicker.setDisplayedValues(months);
    142         }
    143 
    144         mMonthPicker.setOnLongPressUpdateInterval(200);
    145         mMonthPicker.setOnValueChangedListener(new OnValueChangeListener() {
    146             @Override
    147             public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
    148 
    149                 /* We display the month 1-12 but store it 0-11 so always
    150                  * subtract by one to ensure our internal state is always 0-11
    151                  */
    152                 mMonth = newVal - 1;
    153                 // Adjust max day of the month
    154                 adjustMaxDay();
    155                 notifyDateChanged();
    156                 updateDaySpinner();
    157             }
    158         });
    159         mYearPicker = (NumberPicker) findViewById(R.id.year);
    160         mYearPicker.setOnLongPressUpdateInterval(100);
    161         mYearPicker.setOnValueChangedListener(new OnValueChangeListener() {
    162             @Override
    163             public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
    164                 mYear = newVal;
    165                 // Adjust max day for leap years if needed
    166                 adjustMaxDay();
    167                 notifyDateChanged();
    168                 updateDaySpinner();
    169             }
    170         });
    171 
    172         mYearToggle = (CheckBox) findViewById(R.id.yearToggle);
    173         mYearToggle.setOnCheckedChangeListener(new OnCheckedChangeListener() {
    174             @Override
    175             public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
    176                 mHasYear = isChecked;
    177                 adjustMaxDay();
    178                 notifyDateChanged();
    179                 updateSpinners();
    180             }
    181         });
    182 
    183         // attributes
    184         TypedArray a = context.obtainStyledAttributes(attrs,
    185                 com.android.internal.R.styleable.DatePicker);
    186 
    187         int mStartYear =
    188                 a.getInt(com.android.internal.R.styleable.DatePicker_startYear, DEFAULT_START_YEAR);
    189         int mEndYear =
    190                 a.getInt(com.android.internal.R.styleable.DatePicker_endYear, DEFAULT_END_YEAR);
    191         mYearPicker.setMinValue(mStartYear);
    192         mYearPicker.setMaxValue(mEndYear);
    193 
    194         a.recycle();
    195 
    196         // initialize to current date
    197         Calendar cal = Calendar.getInstance();
    198         init(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH), null);
    199 
    200         // re-order the number pickers to match the current date format
    201         reorderPickers(months);
    202 
    203         mPickerContainer.setLayoutTransition(new LayoutTransition());
    204         if (!isEnabled()) {
    205             setEnabled(false);
    206         }
    207     }
    208 
    209     @Override
    210     public void setEnabled(boolean enabled) {
    211         super.setEnabled(enabled);
    212         mDayPicker.setEnabled(enabled);
    213         mMonthPicker.setEnabled(enabled);
    214         mYearPicker.setEnabled(enabled);
    215     }
    216 
    217     private void reorderPickers(String[] months) {
    218         java.text.DateFormat format;
    219         String order;
    220 
    221         /*
    222          * If the user is in a locale where the medium date format is
    223          * still numeric (Japanese and Czech, for example), respect
    224          * the date format order setting.  Otherwise, use the order
    225          * that the locale says is appropriate for a spelled-out date.
    226          */
    227 
    228         if (months[0].startsWith("1")) {
    229             format = DateFormat.getDateFormat(getContext());
    230         } else {
    231             format = DateFormat.getMediumDateFormat(getContext());
    232         }
    233 
    234         if (format instanceof SimpleDateFormat) {
    235             order = ((SimpleDateFormat) format).toPattern();
    236         } else {
    237             // Shouldn't happen, but just in case.
    238             order = new String(DateFormat.getDateFormatOrder(getContext()));
    239         }
    240 
    241         /* Remove the 3 pickers from their parent and then add them back in the
    242          * required order.
    243          */
    244         mPickerContainer.removeAllViews();
    245 
    246         boolean quoted = false;
    247         boolean didDay = false, didMonth = false, didYear = false;
    248 
    249         for (int i = 0; i < order.length(); i++) {
    250             char c = order.charAt(i);
    251 
    252             if (c == '\'') {
    253                 quoted = !quoted;
    254             }
    255 
    256             if (!quoted) {
    257                 if (c == DateFormat.DATE && !didDay) {
    258                     mPickerContainer.addView(mDayPicker);
    259                     didDay = true;
    260                 } else if ((c == DateFormat.MONTH || c == 'L') && !didMonth) {
    261                     mPickerContainer.addView(mMonthPicker);
    262                     didMonth = true;
    263                 } else if (c == DateFormat.YEAR && !didYear) {
    264                     mPickerContainer.addView (mYearPicker);
    265                     didYear = true;
    266                 }
    267             }
    268         }
    269 
    270         // Shouldn't happen, but just in case.
    271         if (!didMonth) {
    272             mPickerContainer.addView(mMonthPicker);
    273         }
    274         if (!didDay) {
    275             mPickerContainer.addView(mDayPicker);
    276         }
    277         if (!didYear) {
    278             mPickerContainer.addView(mYearPicker);
    279         }
    280     }
    281 
    282     public void updateDate(int year, int monthOfYear, int dayOfMonth) {
    283         if (mYear != year || mMonth != monthOfYear || mDay != dayOfMonth) {
    284             mYear = (mYearOptional && year == NO_YEAR) ? getCurrentYear() : year;
    285             mMonth = monthOfYear;
    286             mDay = dayOfMonth;
    287             updateSpinners();
    288             reorderPickers(new DateFormatSymbols().getShortMonths());
    289             notifyDateChanged();
    290         }
    291     }
    292 
    293     private int getCurrentYear() {
    294         return Calendar.getInstance().get(Calendar.YEAR);
    295     }
    296 
    297     private static class SavedState extends BaseSavedState {
    298 
    299         private final int mYear;
    300         private final int mMonth;
    301         private final int mDay;
    302         private final boolean mHasYear;
    303         private final boolean mYearOptional;
    304 
    305         /**
    306          * Constructor called from {@link DatePicker#onSaveInstanceState()}
    307          */
    308         private SavedState(Parcelable superState, int year, int month, int day, boolean hasYear,
    309                 boolean yearOptional) {
    310             super(superState);
    311             mYear = year;
    312             mMonth = month;
    313             mDay = day;
    314             mHasYear = hasYear;
    315             mYearOptional = yearOptional;
    316         }
    317 
    318         /**
    319          * Constructor called from {@link #CREATOR}
    320          */
    321         private SavedState(Parcel in) {
    322             super(in);
    323             mYear = in.readInt();
    324             mMonth = in.readInt();
    325             mDay = in.readInt();
    326             mHasYear = in.readInt() != 0;
    327             mYearOptional = in.readInt() != 0;
    328         }
    329 
    330         public int getYear() {
    331             return mYear;
    332         }
    333 
    334         public int getMonth() {
    335             return mMonth;
    336         }
    337 
    338         public int getDay() {
    339             return mDay;
    340         }
    341 
    342         public boolean hasYear() {
    343             return mHasYear;
    344         }
    345 
    346         public boolean isYearOptional() {
    347             return mYearOptional;
    348         }
    349 
    350         @Override
    351         public void writeToParcel(Parcel dest, int flags) {
    352             super.writeToParcel(dest, flags);
    353             dest.writeInt(mYear);
    354             dest.writeInt(mMonth);
    355             dest.writeInt(mDay);
    356             dest.writeInt(mHasYear ? 1 : 0);
    357             dest.writeInt(mYearOptional ? 1 : 0);
    358         }
    359 
    360         @SuppressWarnings("unused")
    361         public static final Parcelable.Creator<SavedState> CREATOR =
    362                 new Creator<SavedState>() {
    363 
    364                     @Override
    365                     public SavedState createFromParcel(Parcel in) {
    366                         return new SavedState(in);
    367                     }
    368 
    369                     @Override
    370                     public SavedState[] newArray(int size) {
    371                         return new SavedState[size];
    372                     }
    373                 };
    374     }
    375 
    376 
    377     /**
    378      * Override so we are in complete control of save / restore for this widget.
    379      */
    380     @Override
    381     protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
    382         dispatchThawSelfOnly(container);
    383     }
    384 
    385     @Override
    386     protected Parcelable onSaveInstanceState() {
    387         Parcelable superState = super.onSaveInstanceState();
    388 
    389         return new SavedState(superState, mYear, mMonth, mDay, mHasYear, mYearOptional);
    390     }
    391 
    392     @Override
    393     protected void onRestoreInstanceState(Parcelable state) {
    394         SavedState ss = (SavedState) state;
    395         super.onRestoreInstanceState(ss.getSuperState());
    396         mYear = ss.getYear();
    397         mMonth = ss.getMonth();
    398         mDay = ss.getDay();
    399         mHasYear = ss.hasYear();
    400         mYearOptional = ss.isYearOptional();
    401         updateSpinners();
    402     }
    403 
    404     /**
    405      * Initialize the state.
    406      * @param year The initial year.
    407      * @param monthOfYear The initial month.
    408      * @param dayOfMonth The initial day of the month.
    409      * @param onDateChangedListener How user is notified date is changed by user, can be null.
    410      */
    411     public void init(int year, int monthOfYear, int dayOfMonth,
    412             OnDateChangedListener onDateChangedListener) {
    413         init(year, monthOfYear, dayOfMonth, false, onDateChangedListener);
    414     }
    415 
    416     /**
    417      * Initialize the state.
    418      * @param year The initial year or {@link #NO_YEAR} if no year has been specified
    419      * @param monthOfYear The initial month.
    420      * @param dayOfMonth The initial day of the month.
    421      * @param yearOptional True if the user can toggle the year
    422      * @param onDateChangedListener How user is notified date is changed by user, can be null.
    423      */
    424     public void init(int year, int monthOfYear, int dayOfMonth, boolean yearOptional,
    425             OnDateChangedListener onDateChangedListener) {
    426         mYear = (yearOptional && year == NO_YEAR) ? getCurrentYear() : year;
    427         mMonth = monthOfYear;
    428         mDay = dayOfMonth;
    429         mYearOptional = yearOptional;
    430         mHasYear = yearOptional ? (year != NO_YEAR) : true;
    431         mOnDateChangedListener = onDateChangedListener;
    432         updateSpinners();
    433     }
    434 
    435     private void updateSpinners() {
    436         updateDaySpinner();
    437         mYearToggle.setChecked(mHasYear);
    438         mYearToggle.setVisibility(mYearOptional ? View.VISIBLE : View.GONE);
    439         mYearPicker.setValue(mYear);
    440         mYearPicker.setVisibility(mHasYear ? View.VISIBLE : View.GONE);
    441 
    442         /* The month display uses 1-12 but our internal state stores it
    443          * 0-11 so add one when setting the display.
    444          */
    445         mMonthPicker.setValue(mMonth + 1);
    446     }
    447 
    448     private void updateDaySpinner() {
    449         Calendar cal = Calendar.getInstance();
    450         // if year was not set, use 2000 as it was a leap year
    451         cal.set(mHasYear ? mYear : 2000, mMonth, 1);
    452         int max = cal.getActualMaximum(Calendar.DAY_OF_MONTH);
    453         mDayPicker.setMinValue(1);
    454         mDayPicker.setMaxValue(max);
    455         mDayPicker.setValue(mDay);
    456     }
    457 
    458     public int getYear() {
    459         return (mYearOptional && !mHasYear) ? NO_YEAR : mYear;
    460     }
    461 
    462     public boolean isYearOptional() {
    463         return mYearOptional;
    464     }
    465 
    466     public int getMonth() {
    467         return mMonth;
    468     }
    469 
    470     public int getDayOfMonth() {
    471         return mDay;
    472     }
    473 
    474     private void adjustMaxDay(){
    475         Calendar cal = Calendar.getInstance();
    476         // if year was not set, use 2000 as it was a leap year
    477         cal.set(Calendar.YEAR, mHasYear ? mYear : 2000);
    478         cal.set(Calendar.MONTH, mMonth);
    479         int max = cal.getActualMaximum(Calendar.DAY_OF_MONTH);
    480         if (mDay > max) {
    481             mDay = max;
    482         }
    483     }
    484 
    485     private void notifyDateChanged() {
    486         if (mOnDateChangedListener != null) {
    487             int year = (mYearOptional && !mHasYear) ? NO_YEAR : mYear;
    488             mOnDateChangedListener.onDateChanged(DatePicker.this, year, mMonth, mDay);
    489         }
    490     }
    491 }
    492