/* * Copyright (C) 2007 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.widget; import com.android.internal.R; import android.annotation.Widget; import android.content.Context; import android.content.res.TypedArray; import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; import android.text.format.DateFormat; import android.text.format.DateUtils; import android.util.AttributeSet; import android.util.Log; import android.util.SparseArray; import android.view.LayoutInflater; import android.view.accessibility.AccessibilityEvent; import android.widget.NumberPicker.OnValueChangeListener; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Calendar; import java.util.Locale; import java.util.TimeZone; /** * This class is a widget for selecting a date. The date can be selected by a * year, month, and day spinners or a {@link CalendarView}. The set of spinners * and the calendar view are automatically synchronized. The client can * customize whether only the spinners, or only the calendar view, or both to be * displayed. Also the minimal and maximal date from which dates to be selected * can be customized. *

* See the Date * Picker tutorial. *

*

* For a dialog using this view, see {@link android.app.DatePickerDialog}. *

* * @attr ref android.R.styleable#DatePicker_startYear * @attr ref android.R.styleable#DatePicker_endYear * @attr ref android.R.styleable#DatePicker_maxDate * @attr ref android.R.styleable#DatePicker_minDate * @attr ref android.R.styleable#DatePicker_spinnersShown * @attr ref android.R.styleable#DatePicker_calendarViewShown */ @Widget public class DatePicker extends FrameLayout { private static final String LOG_TAG = DatePicker.class.getSimpleName(); private static final String DATE_FORMAT = "MM/dd/yyyy"; private static final int DEFAULT_START_YEAR = 1900; private static final int DEFAULT_END_YEAR = 2100; private static final boolean DEFAULT_CALENDAR_VIEW_SHOWN = true; private static final boolean DEFAULT_SPINNERS_SHOWN = true; private static final boolean DEFAULT_ENABLED_STATE = true; private final NumberPicker mDaySpinner; private final LinearLayout mSpinners; private final NumberPicker mMonthSpinner; private final NumberPicker mYearSpinner; private final CalendarView mCalendarView; private OnDateChangedListener mOnDateChangedListener; private Locale mMonthLocale; private final Calendar mTempDate = Calendar.getInstance(); private final int mNumberOfMonths = mTempDate.getActualMaximum(Calendar.MONTH) + 1; private final String[] mShortMonths = new String[mNumberOfMonths]; private final java.text.DateFormat mDateFormat = new SimpleDateFormat(DATE_FORMAT); private final Calendar mMinDate = Calendar.getInstance(); private final Calendar mMaxDate = Calendar.getInstance(); private final Calendar mCurrentDate = Calendar.getInstance(); private boolean mIsEnabled = DEFAULT_ENABLED_STATE; /** * The callback used to indicate the user changes\d the date. */ public interface OnDateChangedListener { /** * Called upon a date change. * * @param view The view associated with this listener. * @param year The year that was set. * @param monthOfYear The month that was set (0-11) for compatibility * with {@link java.util.Calendar}. * @param dayOfMonth The day of the month that was set. */ void onDateChanged(DatePicker view, int year, int monthOfYear, int dayOfMonth); } public DatePicker(Context context) { this(context, null); } public DatePicker(Context context, AttributeSet attrs) { this(context, attrs, R.attr.datePickerStyle); } public DatePicker(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); TypedArray attributesArray = context.obtainStyledAttributes(attrs, R.styleable.DatePicker, defStyle, 0); boolean spinnersShown = attributesArray.getBoolean(R.styleable.DatePicker_spinnersShown, DEFAULT_SPINNERS_SHOWN); boolean calendarViewShown = attributesArray.getBoolean( R.styleable.DatePicker_calendarViewShown, DEFAULT_CALENDAR_VIEW_SHOWN); int startYear = attributesArray.getInt(R.styleable.DatePicker_startYear, DEFAULT_START_YEAR); int endYear = attributesArray.getInt(R.styleable.DatePicker_endYear, DEFAULT_END_YEAR); String minDate = attributesArray.getString(R.styleable.DatePicker_minDate); String maxDate = attributesArray.getString(R.styleable.DatePicker_maxDate); int layoutResourceId = attributesArray.getResourceId(R.styleable.DatePicker_layout, R.layout.date_picker); attributesArray.recycle(); LayoutInflater inflater = (LayoutInflater) context .getSystemService(Context.LAYOUT_INFLATER_SERVICE); inflater.inflate(layoutResourceId, this, true); OnValueChangeListener onChangeListener = new OnValueChangeListener() { public void onValueChange(NumberPicker picker, int oldVal, int newVal) { mTempDate.setTimeInMillis(mCurrentDate.getTimeInMillis()); // take care of wrapping of days and months to update greater fields if (picker == mDaySpinner) { int maxDayOfMonth = mTempDate.getActualMaximum(Calendar.DAY_OF_MONTH); if (oldVal == maxDayOfMonth && newVal == 1) { mTempDate.add(Calendar.DAY_OF_MONTH, 1); } else if (oldVal == 1 && newVal == maxDayOfMonth) { mTempDate.add(Calendar.DAY_OF_MONTH, -1); } else { mTempDate.add(Calendar.DAY_OF_MONTH, newVal - oldVal); } } else if (picker == mMonthSpinner) { if (oldVal == 11 && newVal == 0) { mTempDate.add(Calendar.MONTH, 1); } else if (oldVal == 0 && newVal == 11) { mTempDate.add(Calendar.MONTH, -1); } else { mTempDate.add(Calendar.MONTH, newVal - oldVal); } } else if (picker == mYearSpinner) { mTempDate.set(Calendar.YEAR, newVal); } else { throw new IllegalArgumentException(); } // now set the date to the adjusted one setDate(mTempDate.get(Calendar.YEAR), mTempDate.get(Calendar.MONTH), mTempDate.get(Calendar.DAY_OF_MONTH)); updateSpinners(); updateCalendarView(); notifyDateChanged(); } }; mSpinners = (LinearLayout) findViewById(R.id.pickers); // calendar view day-picker mCalendarView = (CalendarView) findViewById(R.id.calendar_view); mCalendarView.setOnDateChangeListener(new CalendarView.OnDateChangeListener() { public void onSelectedDayChange(CalendarView view, int year, int month, int monthDay) { setDate(year, month, monthDay); updateSpinners(); notifyDateChanged(); } }); // day mDaySpinner = (NumberPicker) findViewById(R.id.day); mDaySpinner.setFormatter(NumberPicker.TWO_DIGIT_FORMATTER); mDaySpinner.setOnLongPressUpdateInterval(100); mDaySpinner.setOnValueChangedListener(onChangeListener); // month mMonthSpinner = (NumberPicker) findViewById(R.id.month); mMonthSpinner.setMinValue(0); mMonthSpinner.setMaxValue(mNumberOfMonths - 1); mMonthSpinner.setDisplayedValues(getShortMonths()); mMonthSpinner.setOnLongPressUpdateInterval(200); mMonthSpinner.setOnValueChangedListener(onChangeListener); // year mYearSpinner = (NumberPicker) findViewById(R.id.year); mYearSpinner.setOnLongPressUpdateInterval(100); mYearSpinner.setOnValueChangedListener(onChangeListener); // show only what the user required but make sure we // show something and the spinners have higher priority if (!spinnersShown && !calendarViewShown) { setSpinnersShown(true); } else { setSpinnersShown(spinnersShown); setCalendarViewShown(calendarViewShown); } // set the min date giving priority of the minDate over startYear mTempDate.clear(); if (!TextUtils.isEmpty(minDate)) { if (!parseDate(minDate, mTempDate)) { mTempDate.set(startYear, 0, 1); } } else { mTempDate.set(startYear, 0, 1); } setMinDate(mTempDate.getTimeInMillis()); // set the max date giving priority of the maxDate over endYear mTempDate.clear(); if (!TextUtils.isEmpty(maxDate)) { if (!parseDate(maxDate, mTempDate)) { mTempDate.set(endYear, 11, 31); } } else { mTempDate.set(endYear, 11, 31); } setMaxDate(mTempDate.getTimeInMillis()); // initialize to current date mCurrentDate.setTimeInMillis(System.currentTimeMillis()); init(mCurrentDate.get(Calendar.YEAR), mCurrentDate.get(Calendar.MONTH), mCurrentDate .get(Calendar.DAY_OF_MONTH), null); // re-order the number spinners to match the current date format reorderSpinners(); } /** * Gets the minimal date supported by this {@link DatePicker} in * milliseconds since January 1, 1970 00:00:00 in * {@link TimeZone#getDefault()} time zone. *

* Note: The default minimal date is 01/01/1900. *

* * @return The minimal supported date. */ public long getMinDate() { return mCalendarView.getMinDate(); } /** * Sets the minimal date supported by this {@link NumberPicker} in * milliseconds since January 1, 1970 00:00:00 in * {@link TimeZone#getDefault()} time zone. * * @param minDate The minimal supported date. */ public void setMinDate(long minDate) { mTempDate.setTimeInMillis(minDate); if (mTempDate.get(Calendar.YEAR) == mMinDate.get(Calendar.YEAR) && mTempDate.get(Calendar.DAY_OF_YEAR) != mMinDate.get(Calendar.DAY_OF_YEAR)) { return; } mMinDate.setTimeInMillis(minDate); mCalendarView.setMinDate(minDate); if (mCurrentDate.before(mMinDate)) { mCurrentDate.setTimeInMillis(mMinDate.getTimeInMillis()); updateCalendarView(); } updateSpinners(); } /** * Gets the maximal date supported by this {@link DatePicker} in * milliseconds since January 1, 1970 00:00:00 in * {@link TimeZone#getDefault()} time zone. *

* Note: The default maximal date is 12/31/2100. *

* * @return The maximal supported date. */ public long getMaxDate() { return mCalendarView.getMaxDate(); } /** * Sets the maximal date supported by this {@link DatePicker} in * milliseconds since January 1, 1970 00:00:00 in * {@link TimeZone#getDefault()} time zone. * * @param maxDate The maximal supported date. */ public void setMaxDate(long maxDate) { mTempDate.setTimeInMillis(maxDate); if (mTempDate.get(Calendar.YEAR) == mMaxDate.get(Calendar.YEAR) && mTempDate.get(Calendar.DAY_OF_YEAR) != mMaxDate.get(Calendar.DAY_OF_YEAR)) { return; } mMaxDate.setTimeInMillis(maxDate); mCalendarView.setMaxDate(maxDate); if (mCurrentDate.after(mMaxDate)) { mCurrentDate.setTimeInMillis(mMaxDate.getTimeInMillis()); updateCalendarView(); } updateSpinners(); } @Override public void setEnabled(boolean enabled) { if (mIsEnabled == enabled) { return; } super.setEnabled(enabled); mDaySpinner.setEnabled(enabled); mMonthSpinner.setEnabled(enabled); mYearSpinner.setEnabled(enabled); mCalendarView.setEnabled(enabled); mIsEnabled = enabled; } @Override public boolean isEnabled() { return mIsEnabled; } @Override public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_SHOW_YEAR; String selectedDateUtterance = DateUtils.formatDateTime(mContext, mCurrentDate.getTimeInMillis(), flags); event.getText().add(selectedDateUtterance); return true; } /** * Gets whether the {@link CalendarView} is shown. * * @return True if the calendar view is shown. */ public boolean getCalendarViewShown() { return mCalendarView.isShown(); } /** * Sets whether the {@link CalendarView} is shown. * * @param shown True if the calendar view is to be shown. */ public void setCalendarViewShown(boolean shown) { mCalendarView.setVisibility(shown ? VISIBLE : GONE); } /** * Gets whether the spinners are shown. * * @return True if the spinners are shown. */ public boolean getSpinnersShown() { return mSpinners.isShown(); } /** * Sets whether the spinners are shown. * * @param shown True if the spinners are to be shown. */ public void setSpinnersShown(boolean shown) { mSpinners.setVisibility(shown ? VISIBLE : GONE); } /** * Reorders the spinners according to the date format in the current * {@link Locale}. */ private void reorderSpinners() { java.text.DateFormat format; String order; /* * If the user is in a locale where the medium date format is still * numeric (Japanese and Czech, for example), respect the date format * order setting. Otherwise, use the order that the locale says is * appropriate for a spelled-out date. */ if (getShortMonths()[0].startsWith("1")) { format = DateFormat.getDateFormat(getContext()); } else { format = DateFormat.getMediumDateFormat(getContext()); } if (format instanceof SimpleDateFormat) { order = ((SimpleDateFormat) format).toPattern(); } else { // Shouldn't happen, but just in case. order = new String(DateFormat.getDateFormatOrder(getContext())); } /* * Remove the 3 spinners from their parent and then add them back in the * required order. */ LinearLayout parent = mSpinners; parent.removeAllViews(); boolean quoted = false; boolean didDay = false, didMonth = false, didYear = false; for (int i = 0; i < order.length(); i++) { char c = order.charAt(i); if (c == '\'') { quoted = !quoted; } if (!quoted) { if (c == DateFormat.DATE && !didDay) { parent.addView(mDaySpinner); didDay = true; } else if ((c == DateFormat.MONTH || c == 'L') && !didMonth) { parent.addView(mMonthSpinner); didMonth = true; } else if (c == DateFormat.YEAR && !didYear) { parent.addView(mYearSpinner); didYear = true; } } } // Shouldn't happen, but just in case. if (!didMonth) { parent.addView(mMonthSpinner); } if (!didDay) { parent.addView(mDaySpinner); } if (!didYear) { parent.addView(mYearSpinner); } } /** * Updates the current date. * * @param year The year. * @param month The month which is starting from zero. * @param dayOfMonth The day of the month. */ public void updateDate(int year, int month, int dayOfMonth) { if (!isNewDate(year, month, dayOfMonth)) { return; } setDate(year, month, dayOfMonth); updateSpinners(); updateCalendarView(); notifyDateChanged(); } // Override so we are in complete control of save / restore for this widget. @Override protected void dispatchRestoreInstanceState(SparseArray container) { dispatchThawSelfOnly(container); } @Override protected Parcelable onSaveInstanceState() { Parcelable superState = super.onSaveInstanceState(); return new SavedState(superState, getYear(), getMonth(), getDayOfMonth()); } @Override protected void onRestoreInstanceState(Parcelable state) { SavedState ss = (SavedState) state; super.onRestoreInstanceState(ss.getSuperState()); setDate(ss.mYear, ss.mMonth, ss.mDay); updateSpinners(); updateCalendarView(); } /** * Initialize the state. If the provided values designate an inconsistent * date the values are normalized before updating the spinners. * * @param year The initial year. * @param monthOfYear The initial month starting from zero. * @param dayOfMonth The initial day of the month. * @param onDateChangedListener How user is notified date is changed by * user, can be null. */ public void init(int year, int monthOfYear, int dayOfMonth, OnDateChangedListener onDateChangedListener) { setDate(year, monthOfYear, dayOfMonth); updateSpinners(); updateCalendarView(); mOnDateChangedListener = onDateChangedListener; } /** * Parses the given date and in case of success sets the result * to the outDate. * * @return True if the date was parsed. */ private boolean parseDate(String date, Calendar outDate) { try { outDate.setTime(mDateFormat.parse(date)); return true; } catch (ParseException e) { Log.w(LOG_TAG, "Date: " + date + " not in format: " + DATE_FORMAT); return false; } } /** * @return The short month abbreviations. */ private String[] getShortMonths() { final Locale currentLocale = Locale.getDefault(); if (currentLocale.equals(mMonthLocale)) { return mShortMonths; } else { for (int i = 0; i < mNumberOfMonths; i++) { mShortMonths[i] = DateUtils.getMonthString(Calendar.JANUARY + i, DateUtils.LENGTH_MEDIUM); } mMonthLocale = currentLocale; return mShortMonths; } } private boolean isNewDate(int year, int month, int dayOfMonth) { return (mCurrentDate.get(Calendar.YEAR) != year || mCurrentDate.get(Calendar.MONTH) != dayOfMonth || mCurrentDate.get(Calendar.DAY_OF_MONTH) != month); } private void setDate(int year, int month, int dayOfMonth) { mCurrentDate.set(year, month, dayOfMonth); if (mCurrentDate.before(mMinDate)) { mCurrentDate.setTimeInMillis(mMinDate.getTimeInMillis()); } else if (mCurrentDate.after(mMaxDate)) { mCurrentDate.setTimeInMillis(mMaxDate.getTimeInMillis()); } } private void updateSpinners() { // set the spinner ranges respecting the min and max dates if (mCurrentDate.equals(mMinDate)) { mDaySpinner.setMinValue(mCurrentDate.get(Calendar.DAY_OF_MONTH)); mDaySpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.DAY_OF_MONTH)); mDaySpinner.setWrapSelectorWheel(false); mMonthSpinner.setDisplayedValues(null); mMonthSpinner.setMinValue(mCurrentDate.get(Calendar.MONTH)); mMonthSpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.MONTH)); mMonthSpinner.setWrapSelectorWheel(false); } else if (mCurrentDate.equals(mMaxDate)) { mDaySpinner.setMinValue(mCurrentDate.getActualMinimum(Calendar.DAY_OF_MONTH)); mDaySpinner.setMaxValue(mCurrentDate.get(Calendar.DAY_OF_MONTH)); mDaySpinner.setWrapSelectorWheel(false); mMonthSpinner.setDisplayedValues(null); mMonthSpinner.setMinValue(mCurrentDate.getActualMinimum(Calendar.MONTH)); mMonthSpinner.setMaxValue(mCurrentDate.get(Calendar.MONTH)); mMonthSpinner.setWrapSelectorWheel(false); } else { mDaySpinner.setMinValue(1); mDaySpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.DAY_OF_MONTH)); mDaySpinner.setWrapSelectorWheel(true); mMonthSpinner.setDisplayedValues(null); mMonthSpinner.setMinValue(0); mMonthSpinner.setMaxValue(11); mMonthSpinner.setWrapSelectorWheel(true); } // make sure the month names are a zero based array // with the months in the month spinner String[] displayedValues = Arrays.copyOfRange(getShortMonths(), mMonthSpinner.getMinValue(), mMonthSpinner.getMaxValue() + 1); mMonthSpinner.setDisplayedValues(displayedValues); // year spinner range does not change based on the current date mYearSpinner.setMinValue(mMinDate.get(Calendar.YEAR)); mYearSpinner.setMaxValue(mMaxDate.get(Calendar.YEAR)); mYearSpinner.setWrapSelectorWheel(false); // set the spinner values mYearSpinner.setValue(mCurrentDate.get(Calendar.YEAR)); mMonthSpinner.setValue(mCurrentDate.get(Calendar.MONTH)); mDaySpinner.setValue(mCurrentDate.get(Calendar.DAY_OF_MONTH)); } /** * Updates the calendar view with the current date. */ private void updateCalendarView() { mCalendarView.setDate(mCurrentDate.getTimeInMillis(), false, false); } /** * @return The selected year. */ public int getYear() { return mCurrentDate.get(Calendar.YEAR); } /** * @return The selected month. */ public int getMonth() { return mCurrentDate.get(Calendar.MONTH); } /** * @return The selected day of month. */ public int getDayOfMonth() { return mCurrentDate.get(Calendar.DAY_OF_MONTH); } /** * Notifies the listener, if such, for a change in the selected date. */ private void notifyDateChanged() { sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); if (mOnDateChangedListener != null) { mOnDateChangedListener.onDateChanged(this, getYear(), getMonth(), getDayOfMonth()); } } /** * Class for managing state storing/restoring. */ private static class SavedState extends BaseSavedState { private final int mYear; private final int mMonth; private final int mDay; /** * Constructor called from {@link DatePicker#onSaveInstanceState()} */ private SavedState(Parcelable superState, int year, int month, int day) { super(superState); mYear = year; mMonth = month; mDay = day; } /** * Constructor called from {@link #CREATOR} */ private SavedState(Parcel in) { super(in); mYear = in.readInt(); mMonth = in.readInt(); mDay = in.readInt(); } @Override public void writeToParcel(Parcel dest, int flags) { super.writeToParcel(dest, flags); dest.writeInt(mYear); dest.writeInt(mMonth); dest.writeInt(mDay); } @SuppressWarnings("all") // suppress unused and hiding public static final Parcelable.Creator CREATOR = new Creator() { public SavedState createFromParcel(Parcel in) { return new SavedState(in); } public SavedState[] newArray(int size) { return new SavedState[size]; } }; } }