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