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         mYearPicker.setMinValue(DEFAULT_START_YEAR);
    175         mYearPicker.setMaxValue(DEFAULT_END_YEAR);
    176 
    177         mYearToggle = (CheckBox) findViewById(R.id.yearToggle);
    178         mYearToggle.setOnCheckedChangeListener(new OnCheckedChangeListener() {
    179             @Override
    180             public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
    181                 mHasYear = isChecked;
    182                 adjustMaxDay();
    183                 notifyDateChanged();
    184                 updateSpinners();
    185             }
    186         });
    187 
    188         // initialize to current date
    189         Calendar cal = Calendar.getInstance();
    190         init(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH), null);
    191 
    192         // re-order the number pickers to match the current date format
    193         reorderPickers();
    194 
    195         mPickerContainer.setLayoutTransition(new LayoutTransition());
    196         if (!isEnabled()) {
    197             setEnabled(false);
    198         }
    199     }
    200 
    201     @Override
    202     public void setEnabled(boolean enabled) {
    203         super.setEnabled(enabled);
    204         mDayPicker.setEnabled(enabled);
    205         mMonthPicker.setEnabled(enabled);
    206         mYearPicker.setEnabled(enabled);
    207     }
    208 
    209     private void reorderPickers() {
    210         // We use numeric spinners for year and day, but textual months. Ask icu4c what
    211         // order the user's locale uses for that combination. http://b/7207103.
    212         String skeleton = mHasYear ? "yyyyMMMdd" : "MMMdd";
    213         String pattern = DateFormat.getBestDateTimePattern(Locale.getDefault(), skeleton);
    214         char[] order = ICU.getDateFormatOrder(pattern);
    215 
    216         /* Remove the 3 pickers from their parent and then add them back in the
    217          * required order.
    218          */
    219         mPickerContainer.removeAllViews();
    220         for (char field : order) {
    221             if (field == 'd') {
    222                 mPickerContainer.addView(mDayPicker);
    223             } else if (field == 'M') {
    224                 mPickerContainer.addView(mMonthPicker);
    225             } else {
    226                 // Either 'y' or '\u0000' depending on whether we're showing a year.
    227                 // If we're not showing a year, it doesn't matter where we put it,
    228                 // but the rest of this class assumes that it will be present (but GONE).
    229                 mPickerContainer.addView(mYearPicker);
    230             }
    231         }
    232     }
    233 
    234     public void updateDate(int year, int monthOfYear, int dayOfMonth) {
    235         if (mYear != year || mMonth != monthOfYear || mDay != dayOfMonth) {
    236             mYear = (mYearOptional && year == NO_YEAR) ? getCurrentYear() : year;
    237             mMonth = monthOfYear;
    238             mDay = dayOfMonth;
    239             updateSpinners();
    240             reorderPickers();
    241             notifyDateChanged();
    242         }
    243     }
    244 
    245     private int getCurrentYear() {
    246         return Calendar.getInstance().get(Calendar.YEAR);
    247     }
    248 
    249     private static class SavedState extends BaseSavedState {
    250 
    251         private final int mYear;
    252         private final int mMonth;
    253         private final int mDay;
    254         private final boolean mHasYear;
    255         private final boolean mYearOptional;
    256 
    257         /**
    258          * Constructor called from {@link DatePicker#onSaveInstanceState()}
    259          */
    260         private SavedState(Parcelable superState, int year, int month, int day, boolean hasYear,
    261                 boolean yearOptional) {
    262             super(superState);
    263             mYear = year;
    264             mMonth = month;
    265             mDay = day;
    266             mHasYear = hasYear;
    267             mYearOptional = yearOptional;
    268         }
    269 
    270         /**
    271          * Constructor called from {@link #CREATOR}
    272          */
    273         private SavedState(Parcel in) {
    274             super(in);
    275             mYear = in.readInt();
    276             mMonth = in.readInt();
    277             mDay = in.readInt();
    278             mHasYear = in.readInt() != 0;
    279             mYearOptional = in.readInt() != 0;
    280         }
    281 
    282         public int getYear() {
    283             return mYear;
    284         }
    285 
    286         public int getMonth() {
    287             return mMonth;
    288         }
    289 
    290         public int getDay() {
    291             return mDay;
    292         }
    293 
    294         public boolean hasYear() {
    295             return mHasYear;
    296         }
    297 
    298         public boolean isYearOptional() {
    299             return mYearOptional;
    300         }
    301 
    302         @Override
    303         public void writeToParcel(Parcel dest, int flags) {
    304             super.writeToParcel(dest, flags);
    305             dest.writeInt(mYear);
    306             dest.writeInt(mMonth);
    307             dest.writeInt(mDay);
    308             dest.writeInt(mHasYear ? 1 : 0);
    309             dest.writeInt(mYearOptional ? 1 : 0);
    310         }
    311 
    312         @SuppressWarnings("unused")
    313         public static final Parcelable.Creator<SavedState> CREATOR =
    314                 new Creator<SavedState>() {
    315 
    316                     @Override
    317                     public SavedState createFromParcel(Parcel in) {
    318                         return new SavedState(in);
    319                     }
    320 
    321                     @Override
    322                     public SavedState[] newArray(int size) {
    323                         return new SavedState[size];
    324                     }
    325                 };
    326     }
    327 
    328 
    329     /**
    330      * Override so we are in complete control of save / restore for this widget.
    331      */
    332     @Override
    333     protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
    334         dispatchThawSelfOnly(container);
    335     }
    336 
    337     @Override
    338     protected Parcelable onSaveInstanceState() {
    339         Parcelable superState = super.onSaveInstanceState();
    340 
    341         return new SavedState(superState, mYear, mMonth, mDay, mHasYear, mYearOptional);
    342     }
    343 
    344     @Override
    345     protected void onRestoreInstanceState(Parcelable state) {
    346         SavedState ss = (SavedState) state;
    347         super.onRestoreInstanceState(ss.getSuperState());
    348         mYear = ss.getYear();
    349         mMonth = ss.getMonth();
    350         mDay = ss.getDay();
    351         mHasYear = ss.hasYear();
    352         mYearOptional = ss.isYearOptional();
    353         updateSpinners();
    354     }
    355 
    356     /**
    357      * Initialize the state.
    358      * @param year The initial year.
    359      * @param monthOfYear The initial month.
    360      * @param dayOfMonth The initial day of the month.
    361      * @param onDateChangedListener How user is notified date is changed by user, can be null.
    362      */
    363     public void init(int year, int monthOfYear, int dayOfMonth,
    364             OnDateChangedListener onDateChangedListener) {
    365         init(year, monthOfYear, dayOfMonth, false, onDateChangedListener);
    366     }
    367 
    368     /**
    369      * Initialize the state.
    370      * @param year The initial year or {@link #NO_YEAR} if no year has been specified
    371      * @param monthOfYear The initial month.
    372      * @param dayOfMonth The initial day of the month.
    373      * @param yearOptional True if the user can toggle the year
    374      * @param onDateChangedListener How user is notified date is changed by user, can be null.
    375      */
    376     public void init(int year, int monthOfYear, int dayOfMonth, boolean yearOptional,
    377             OnDateChangedListener onDateChangedListener) {
    378         mYear = (yearOptional && year == NO_YEAR) ? getCurrentYear() : year;
    379         mMonth = monthOfYear;
    380         mDay = dayOfMonth;
    381         mYearOptional = yearOptional;
    382         mHasYear = yearOptional ? (year != NO_YEAR) : true;
    383         mOnDateChangedListener = onDateChangedListener;
    384         updateSpinners();
    385     }
    386 
    387     private void updateSpinners() {
    388         updateDaySpinner();
    389         mYearToggle.setChecked(mHasYear);
    390         mYearToggle.setVisibility(mYearOptional ? View.VISIBLE : View.GONE);
    391         mYearPicker.setValue(mYear);
    392         mYearPicker.setVisibility(mHasYear ? View.VISIBLE : View.GONE);
    393 
    394         /* The month display uses 1-12 but our internal state stores it
    395          * 0-11 so add one when setting the display.
    396          */
    397         mMonthPicker.setValue(mMonth + 1);
    398     }
    399 
    400     private void updateDaySpinner() {
    401         Calendar cal = Calendar.getInstance();
    402         // if year was not set, use 2000 as it was a leap year
    403         cal.set(mHasYear ? mYear : 2000, mMonth, 1);
    404         int max = cal.getActualMaximum(Calendar.DAY_OF_MONTH);
    405         mDayPicker.setMinValue(1);
    406         mDayPicker.setMaxValue(max);
    407         mDayPicker.setValue(mDay);
    408     }
    409 
    410     public int getYear() {
    411         return (mYearOptional && !mHasYear) ? NO_YEAR : mYear;
    412     }
    413 
    414     public boolean isYearOptional() {
    415         return mYearOptional;
    416     }
    417 
    418     public int getMonth() {
    419         return mMonth;
    420     }
    421 
    422     public int getDayOfMonth() {
    423         return mDay;
    424     }
    425 
    426     private void adjustMaxDay(){
    427         Calendar cal = Calendar.getInstance();
    428         // if year was not set, use 2000 as it was a leap year
    429         cal.set(Calendar.YEAR, mHasYear ? mYear : 2000);
    430         cal.set(Calendar.MONTH, mMonth);
    431         int max = cal.getActualMaximum(Calendar.DAY_OF_MONTH);
    432         if (mDay > max) {
    433             mDay = max;
    434         }
    435     }
    436 
    437     private void notifyDateChanged() {
    438         if (mOnDateChangedListener != null) {
    439             int year = (mYearOptional && !mHasYear) ? NO_YEAR : mYear;
    440             mOnDateChangedListener.onDateChanged(DatePicker.this, year, mMonth, mDay);
    441         }
    442     }
    443 }
    444