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