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