blob: 5f5943fe7472cfb31e4e6962f3bcaf47ae558885 [file] [log] [blame]
/*
* 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 android.annotation.Nullable;
import android.annotation.Widget;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.TypedArray;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
import android.text.InputType;
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.View;
import android.view.accessibility.AccessibilityEvent;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.NumberPicker.OnValueChangeListener;
import com.android.internal.R;
import java.text.DateFormatSymbols;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Locale;
import java.util.TimeZone;
import libcore.icu.ICU;
/**
* Provides a widget for selecting a date.
* <p>
* When the {@link android.R.styleable#DatePicker_datePickerMode} attribute is
* set to {@code spinner}, the date can be selected using 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.
* </p>
* <p>
* When the {@link android.R.styleable#DatePicker_datePickerMode} attribute is
* set to {@code calendar}, the month and day can be selected using a
* calendar-style view while the year can be selected separately using a list.
* </p>
* <p>
* See the <a href="{@docRoot}guide/topics/ui/controls/pickers.html">Pickers</a>
* guide.
* </p>
* <p>
* For a dialog using this view, see {@link android.app.DatePickerDialog}.
* </p>
*
* @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
* @attr ref android.R.styleable#DatePicker_dayOfWeekBackground
* @attr ref android.R.styleable#DatePicker_dayOfWeekTextAppearance
* @attr ref android.R.styleable#DatePicker_headerBackground
* @attr ref android.R.styleable#DatePicker_headerMonthTextAppearance
* @attr ref android.R.styleable#DatePicker_headerDayOfMonthTextAppearance
* @attr ref android.R.styleable#DatePicker_headerYearTextAppearance
* @attr ref android.R.styleable#DatePicker_yearListItemTextAppearance
* @attr ref android.R.styleable#DatePicker_yearListSelectorColor
* @attr ref android.R.styleable#DatePicker_calendarTextColor
* @attr ref android.R.styleable#DatePicker_datePickerMode
*/
@Widget
public class DatePicker extends FrameLayout {
private static final String LOG_TAG = DatePicker.class.getSimpleName();
private static final int MODE_SPINNER = 1;
private static final int MODE_CALENDAR = 2;
private final DatePickerDelegate mDelegate;
/**
* The callback used to indicate the user changed 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 defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public DatePicker(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.DatePicker,
defStyleAttr, defStyleRes);
final int mode = a.getInt(R.styleable.DatePicker_datePickerMode, MODE_SPINNER);
final int firstDayOfWeek = a.getInt(R.styleable.DatePicker_firstDayOfWeek, 0);
a.recycle();
switch (mode) {
case MODE_CALENDAR:
mDelegate = createCalendarUIDelegate(context, attrs, defStyleAttr, defStyleRes);
break;
case MODE_SPINNER:
default:
mDelegate = createSpinnerUIDelegate(context, attrs, defStyleAttr, defStyleRes);
break;
}
if (firstDayOfWeek != 0) {
setFirstDayOfWeek(firstDayOfWeek);
}
}
private DatePickerDelegate createSpinnerUIDelegate(Context context, AttributeSet attrs,
int defStyleAttr, int defStyleRes) {
return new DatePickerSpinnerDelegate(this, context, attrs, defStyleAttr, defStyleRes);
}
private DatePickerDelegate createCalendarUIDelegate(Context context, AttributeSet attrs,
int defStyleAttr, int defStyleRes) {
return new DatePickerCalendarDelegate(this, context, attrs, defStyleAttr,
defStyleRes);
}
/**
* 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 <strong>starting from zero</strong>.
* @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) {
mDelegate.init(year, monthOfYear, dayOfMonth, onDateChangedListener);
}
/**
* Update the current date.
*
* @param year The year.
* @param month The month which is <strong>starting from zero</strong>.
* @param dayOfMonth The day of the month.
*/
public void updateDate(int year, int month, int dayOfMonth) {
mDelegate.updateDate(year, month, dayOfMonth);
}
/**
* @return The selected year.
*/
public int getYear() {
return mDelegate.getYear();
}
/**
* @return The selected month.
*/
public int getMonth() {
return mDelegate.getMonth();
}
/**
* @return The selected day of month.
*/
public int getDayOfMonth() {
return mDelegate.getDayOfMonth();
}
/**
* Gets the minimal date supported by this {@link DatePicker} in
* milliseconds since January 1, 1970 00:00:00 in
* {@link TimeZone#getDefault()} time zone.
* <p>
* Note: The default minimal date is 01/01/1900.
* <p>
*
* @return The minimal supported date.
*/
public long getMinDate() {
return mDelegate.getMinDate().getTimeInMillis();
}
/**
* 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) {
mDelegate.setMinDate(minDate);
}
/**
* Gets the maximal date supported by this {@link DatePicker} in
* milliseconds since January 1, 1970 00:00:00 in
* {@link TimeZone#getDefault()} time zone.
* <p>
* Note: The default maximal date is 12/31/2100.
* <p>
*
* @return The maximal supported date.
*/
public long getMaxDate() {
return mDelegate.getMaxDate().getTimeInMillis();
}
/**
* 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) {
mDelegate.setMaxDate(maxDate);
}
/**
* Sets the callback that indicates the current date is valid.
*
* @param callback the callback, may be null
* @hide
*/
public void setValidationCallback(@Nullable ValidationCallback callback) {
mDelegate.setValidationCallback(callback);
}
@Override
public void setEnabled(boolean enabled) {
if (mDelegate.isEnabled() == enabled) {
return;
}
super.setEnabled(enabled);
mDelegate.setEnabled(enabled);
}
@Override
public boolean isEnabled() {
return mDelegate.isEnabled();
}
/** @hide */
@Override
public boolean dispatchPopulateAccessibilityEventInternal(AccessibilityEvent event) {
return mDelegate.dispatchPopulateAccessibilityEvent(event);
}
/** @hide */
@Override
public void onPopulateAccessibilityEventInternal(AccessibilityEvent event) {
super.onPopulateAccessibilityEventInternal(event);
mDelegate.onPopulateAccessibilityEvent(event);
}
@Override
public CharSequence getAccessibilityClassName() {
return DatePicker.class.getName();
}
@Override
protected void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
mDelegate.onConfigurationChanged(newConfig);
}
/**
* Sets the first day of week.
*
* @param firstDayOfWeek The first day of the week conforming to the
* {@link CalendarView} APIs.
* @see Calendar#SUNDAY
* @see Calendar#MONDAY
* @see Calendar#TUESDAY
* @see Calendar#WEDNESDAY
* @see Calendar#THURSDAY
* @see Calendar#FRIDAY
* @see Calendar#SATURDAY
*
* @attr ref android.R.styleable#DatePicker_firstDayOfWeek
*/
public void setFirstDayOfWeek(int firstDayOfWeek) {
if (firstDayOfWeek < Calendar.SUNDAY || firstDayOfWeek > Calendar.SATURDAY) {
throw new IllegalArgumentException("firstDayOfWeek must be between 1 and 7");
}
mDelegate.setFirstDayOfWeek(firstDayOfWeek);
}
/**
* Gets the first day of week.
*
* @return The first day of the week conforming to the {@link CalendarView}
* APIs.
* @see Calendar#SUNDAY
* @see Calendar#MONDAY
* @see Calendar#TUESDAY
* @see Calendar#WEDNESDAY
* @see Calendar#THURSDAY
* @see Calendar#FRIDAY
* @see Calendar#SATURDAY
*
* @attr ref android.R.styleable#DatePicker_firstDayOfWeek
*/
public int getFirstDayOfWeek() {
return mDelegate.getFirstDayOfWeek();
}
/**
* Returns whether the {@link CalendarView} is shown.
* <p>
* <strong>Note:</strong> This method returns {@code false} when the
* {@link android.R.styleable#DatePicker_datePickerMode} attribute is set
* to {@code calendar}.
*
* @return {@code true} if the calendar view is shown
* @see #getCalendarView()
*/
public boolean getCalendarViewShown() {
return mDelegate.getCalendarViewShown();
}
/**
* Returns the {@link CalendarView} used by this picker.
* <p>
* <strong>Note:</strong> This method returns {@code null} when the
* {@link android.R.styleable#DatePicker_datePickerMode} attribute is set
* to {@code calendar}.
*
* @return the calendar view
* @see #getCalendarViewShown()
*/
public CalendarView getCalendarView() {
return mDelegate.getCalendarView();
}
/**
* Sets whether the {@link CalendarView} is shown.
* <p>
* <strong>Note:</strong> Calling this method has no effect when the
* {@link android.R.styleable#DatePicker_datePickerMode} attribute is set
* to {@code calendar}.
*
* @param shown {@code true} to show the calendar view, {@code false} to
* hide it
*/
public void setCalendarViewShown(boolean shown) {
mDelegate.setCalendarViewShown(shown);
}
/**
* Returns whether the spinners are shown.
* <p>
* <strong>Note:</strong> his method returns {@code false} when the
* {@link android.R.styleable#DatePicker_datePickerMode} attribute is set
* to {@code calendar}.
*
* @return {@code true} if the spinners are shown
*/
public boolean getSpinnersShown() {
return mDelegate.getSpinnersShown();
}
/**
* Sets whether the spinners are shown.
* <p>
* Calling this method has no effect when the
* {@link android.R.styleable#DatePicker_datePickerMode} attribute is set
* to {@code calendar}.
*
* @param shown {@code true} to show the spinners, {@code false} to hide
* them
*/
public void setSpinnersShown(boolean shown) {
mDelegate.setSpinnersShown(shown);
}
@Override
protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
dispatchThawSelfOnly(container);
}
@Override
protected Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
return mDelegate.onSaveInstanceState(superState);
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
BaseSavedState ss = (BaseSavedState) state;
super.onRestoreInstanceState(ss.getSuperState());
mDelegate.onRestoreInstanceState(ss);
}
/**
* A delegate interface that defined the public API of the DatePicker. Allows different
* DatePicker implementations. This would need to be implemented by the DatePicker delegates
* for the real behavior.
*
* @hide
*/
interface DatePickerDelegate {
void init(int year, int monthOfYear, int dayOfMonth,
OnDateChangedListener onDateChangedListener);
void updateDate(int year, int month, int dayOfMonth);
int getYear();
int getMonth();
int getDayOfMonth();
void setFirstDayOfWeek(int firstDayOfWeek);
int getFirstDayOfWeek();
void setMinDate(long minDate);
Calendar getMinDate();
void setMaxDate(long maxDate);
Calendar getMaxDate();
void setEnabled(boolean enabled);
boolean isEnabled();
CalendarView getCalendarView();
void setCalendarViewShown(boolean shown);
boolean getCalendarViewShown();
void setSpinnersShown(boolean shown);
boolean getSpinnersShown();
void setValidationCallback(ValidationCallback callback);
void onConfigurationChanged(Configuration newConfig);
Parcelable onSaveInstanceState(Parcelable superState);
void onRestoreInstanceState(Parcelable state);
boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event);
void onPopulateAccessibilityEvent(AccessibilityEvent event);
}
/**
* An abstract class which can be used as a start for DatePicker implementations
*/
abstract static class AbstractDatePickerDelegate implements DatePickerDelegate {
// The delegator
protected DatePicker mDelegator;
// The context
protected Context mContext;
// The current locale
protected Locale mCurrentLocale;
// Callbacks
protected OnDateChangedListener mOnDateChangedListener;
protected ValidationCallback mValidationCallback;
public AbstractDatePickerDelegate(DatePicker delegator, Context context) {
mDelegator = delegator;
mContext = context;
setCurrentLocale(Locale.getDefault());
}
protected void setCurrentLocale(Locale locale) {
if (!locale.equals(mCurrentLocale)) {
mCurrentLocale = locale;
onLocaleChanged(locale);
}
}
@Override
public void setValidationCallback(ValidationCallback callback) {
mValidationCallback = callback;
}
protected void onValidationChanged(boolean valid) {
if (mValidationCallback != null) {
mValidationCallback.onValidationChanged(valid);
}
}
protected void onLocaleChanged(Locale locale) {
// Stub.
}
}
/**
* A callback interface for updating input validity when the date picker
* when included into a dialog.
*
* @hide
*/
public static interface ValidationCallback {
void onValidationChanged(boolean valid);
}
/**
* A delegate implementing the basic DatePicker
*/
private static class DatePickerSpinnerDelegate extends AbstractDatePickerDelegate {
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 LinearLayout mSpinners;
private final NumberPicker mDaySpinner;
private final NumberPicker mMonthSpinner;
private final NumberPicker mYearSpinner;
private final EditText mDaySpinnerInput;
private final EditText mMonthSpinnerInput;
private final EditText mYearSpinnerInput;
private final CalendarView mCalendarView;
private String[] mShortMonths;
private final java.text.DateFormat mDateFormat = new SimpleDateFormat(DATE_FORMAT);
private int mNumberOfMonths;
private Calendar mTempDate;
private Calendar mMinDate;
private Calendar mMaxDate;
private Calendar mCurrentDate;
private boolean mIsEnabled = DEFAULT_ENABLED_STATE;
DatePickerSpinnerDelegate(DatePicker delegator, Context context, AttributeSet attrs,
int defStyleAttr, int defStyleRes) {
super(delegator, context);
mDelegator = delegator;
mContext = context;
// initialization based on locale
setCurrentLocale(Locale.getDefault());
final TypedArray attributesArray = context.obtainStyledAttributes(attrs,
R.styleable.DatePicker, defStyleAttr, defStyleRes);
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_legacyLayout, R.layout.date_picker_legacy);
attributesArray.recycle();
LayoutInflater inflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(layoutResourceId, mDelegator, true);
OnValueChangeListener onChangeListener = new OnValueChangeListener() {
public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
updateInputState();
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) mDelegator.findViewById(R.id.pickers);
// calendar view day-picker
mCalendarView = (CalendarView) mDelegator.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) mDelegator.findViewById(R.id.day);
mDaySpinner.setFormatter(NumberPicker.getTwoDigitFormatter());
mDaySpinner.setOnLongPressUpdateInterval(100);
mDaySpinner.setOnValueChangedListener(onChangeListener);
mDaySpinnerInput = (EditText) mDaySpinner.findViewById(R.id.numberpicker_input);
// month
mMonthSpinner = (NumberPicker) mDelegator.findViewById(R.id.month);
mMonthSpinner.setMinValue(0);
mMonthSpinner.setMaxValue(mNumberOfMonths - 1);
mMonthSpinner.setDisplayedValues(mShortMonths);
mMonthSpinner.setOnLongPressUpdateInterval(200);
mMonthSpinner.setOnValueChangedListener(onChangeListener);
mMonthSpinnerInput = (EditText) mMonthSpinner.findViewById(R.id.numberpicker_input);
// year
mYearSpinner = (NumberPicker) mDelegator.findViewById(R.id.year);
mYearSpinner.setOnLongPressUpdateInterval(100);
mYearSpinner.setOnValueChangedListener(onChangeListener);
mYearSpinnerInput = (EditText) mYearSpinner.findViewById(R.id.numberpicker_input);
// 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();
// accessibility
setContentDescriptions();
// If not explicitly specified this view is important for accessibility.
if (mDelegator.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
mDelegator.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
}
}
@Override
public void init(int year, int monthOfYear, int dayOfMonth,
OnDateChangedListener onDateChangedListener) {
setDate(year, monthOfYear, dayOfMonth);
updateSpinners();
updateCalendarView();
mOnDateChangedListener = onDateChangedListener;
}
@Override
public void updateDate(int year, int month, int dayOfMonth) {
if (!isNewDate(year, month, dayOfMonth)) {
return;
}
setDate(year, month, dayOfMonth);
updateSpinners();
updateCalendarView();
notifyDateChanged();
}
@Override
public int getYear() {
return mCurrentDate.get(Calendar.YEAR);
}
@Override
public int getMonth() {
return mCurrentDate.get(Calendar.MONTH);
}
@Override
public int getDayOfMonth() {
return mCurrentDate.get(Calendar.DAY_OF_MONTH);
}
@Override
public void setFirstDayOfWeek(int firstDayOfWeek) {
mCalendarView.setFirstDayOfWeek(firstDayOfWeek);
}
@Override
public int getFirstDayOfWeek() {
return mCalendarView.getFirstDayOfWeek();
}
@Override
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();
}
@Override
public Calendar getMinDate() {
final Calendar minDate = Calendar.getInstance();
minDate.setTimeInMillis(mCalendarView.getMinDate());
return minDate;
}
@Override
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 Calendar getMaxDate() {
final Calendar maxDate = Calendar.getInstance();
maxDate.setTimeInMillis(mCalendarView.getMaxDate());
return maxDate;
}
@Override
public void setEnabled(boolean enabled) {
mDaySpinner.setEnabled(enabled);
mMonthSpinner.setEnabled(enabled);
mYearSpinner.setEnabled(enabled);
mCalendarView.setEnabled(enabled);
mIsEnabled = enabled;
}
@Override
public boolean isEnabled() {
return mIsEnabled;
}
@Override
public CalendarView getCalendarView() {
return mCalendarView;
}
@Override
public void setCalendarViewShown(boolean shown) {
mCalendarView.setVisibility(shown ? VISIBLE : GONE);
}
@Override
public boolean getCalendarViewShown() {
return (mCalendarView.getVisibility() == View.VISIBLE);
}
@Override
public void setSpinnersShown(boolean shown) {
mSpinners.setVisibility(shown ? VISIBLE : GONE);
}
@Override
public boolean getSpinnersShown() {
return mSpinners.isShown();
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
setCurrentLocale(newConfig.locale);
}
@Override
public Parcelable onSaveInstanceState(Parcelable superState) {
return new SavedState(superState, getYear(), getMonth(), getDayOfMonth());
}
@Override
public void onRestoreInstanceState(Parcelable state) {
SavedState ss = (SavedState) state;
setDate(ss.mYear, ss.mMonth, ss.mDay);
updateSpinners();
updateCalendarView();
}
@Override
public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
onPopulateAccessibilityEvent(event);
return true;
}
@Override
public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
final int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR;
String selectedDateUtterance = DateUtils.formatDateTime(mContext,
mCurrentDate.getTimeInMillis(), flags);
event.getText().add(selectedDateUtterance);
}
/**
* Sets the current locale.
*
* @param locale The current locale.
*/
@Override
protected void setCurrentLocale(Locale locale) {
super.setCurrentLocale(locale);
mTempDate = getCalendarForLocale(mTempDate, locale);
mMinDate = getCalendarForLocale(mMinDate, locale);
mMaxDate = getCalendarForLocale(mMaxDate, locale);
mCurrentDate = getCalendarForLocale(mCurrentDate, locale);
mNumberOfMonths = mTempDate.getActualMaximum(Calendar.MONTH) + 1;
mShortMonths = new DateFormatSymbols().getShortMonths();
if (usingNumericMonths()) {
// We're in a locale where a date should either be all-numeric, or all-text.
// All-text would require custom NumberPicker formatters for day and year.
mShortMonths = new String[mNumberOfMonths];
for (int i = 0; i < mNumberOfMonths; ++i) {
mShortMonths[i] = String.format("%d", i + 1);
}
}
}
/**
* Tests whether the current locale is one where there are no real month names,
* such as Chinese, Japanese, or Korean locales.
*/
private boolean usingNumericMonths() {
return Character.isDigit(mShortMonths[Calendar.JANUARY].charAt(0));
}
/**
* Gets a calendar for locale bootstrapped with the value of a given calendar.
*
* @param oldCalendar The old calendar.
* @param locale The locale.
*/
private Calendar getCalendarForLocale(Calendar oldCalendar, Locale locale) {
if (oldCalendar == null) {
return Calendar.getInstance(locale);
} else {
final long currentTimeMillis = oldCalendar.getTimeInMillis();
Calendar newCalendar = Calendar.getInstance(locale);
newCalendar.setTimeInMillis(currentTimeMillis);
return newCalendar;
}
}
/**
* Reorders the spinners according to the date format that is
* explicitly set by the user and if no such is set fall back
* to the current locale's default format.
*/
private void reorderSpinners() {
mSpinners.removeAllViews();
// We use numeric spinners for year and day, but textual months. Ask icu4c what
// order the user's locale uses for that combination. http://b/7207103.
String pattern = DateFormat.getBestDateTimePattern(Locale.getDefault(), "yyyyMMMdd");
char[] order = ICU.getDateFormatOrder(pattern);
final int spinnerCount = order.length;
for (int i = 0; i < spinnerCount; i++) {
switch (order[i]) {
case 'd':
mSpinners.addView(mDaySpinner);
setImeOptions(mDaySpinner, spinnerCount, i);
break;
case 'M':
mSpinners.addView(mMonthSpinner);
setImeOptions(mMonthSpinner, spinnerCount, i);
break;
case 'y':
mSpinners.addView(mYearSpinner);
setImeOptions(mYearSpinner, spinnerCount, i);
break;
default:
throw new IllegalArgumentException(Arrays.toString(order));
}
}
}
/**
* Parses the given <code>date</code> and in case of success sets the result
* to the <code>outDate</code>.
*
* @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;
}
}
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(mShortMonths,
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));
if (usingNumericMonths()) {
mMonthSpinnerInput.setRawInputType(InputType.TYPE_CLASS_NUMBER);
}
}
/**
* Updates the calendar view with the current date.
*/
private void updateCalendarView() {
mCalendarView.setDate(mCurrentDate.getTimeInMillis(), false, false);
}
/**
* Notifies the listener, if such, for a change in the selected date.
*/
private void notifyDateChanged() {
mDelegator.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
if (mOnDateChangedListener != null) {
mOnDateChangedListener.onDateChanged(mDelegator, getYear(), getMonth(),
getDayOfMonth());
}
}
/**
* Sets the IME options for a spinner based on its ordering.
*
* @param spinner The spinner.
* @param spinnerCount The total spinner count.
* @param spinnerIndex The index of the given spinner.
*/
private void setImeOptions(NumberPicker spinner, int spinnerCount, int spinnerIndex) {
final int imeOptions;
if (spinnerIndex < spinnerCount - 1) {
imeOptions = EditorInfo.IME_ACTION_NEXT;
} else {
imeOptions = EditorInfo.IME_ACTION_DONE;
}
TextView input = (TextView) spinner.findViewById(R.id.numberpicker_input);
input.setImeOptions(imeOptions);
}
private void setContentDescriptions() {
// Day
trySetContentDescription(mDaySpinner, R.id.increment,
R.string.date_picker_increment_day_button);
trySetContentDescription(mDaySpinner, R.id.decrement,
R.string.date_picker_decrement_day_button);
// Month
trySetContentDescription(mMonthSpinner, R.id.increment,
R.string.date_picker_increment_month_button);
trySetContentDescription(mMonthSpinner, R.id.decrement,
R.string.date_picker_decrement_month_button);
// Year
trySetContentDescription(mYearSpinner, R.id.increment,
R.string.date_picker_increment_year_button);
trySetContentDescription(mYearSpinner, R.id.decrement,
R.string.date_picker_decrement_year_button);
}
private void trySetContentDescription(View root, int viewId, int contDescResId) {
View target = root.findViewById(viewId);
if (target != null) {
target.setContentDescription(mContext.getString(contDescResId));
}
}
private void updateInputState() {
// Make sure that if the user changes the value and the IME is active
// for one of the inputs if this widget, the IME is closed. If the user
// changed the value via the IME and there is a next input the IME will
// be shown, otherwise the user chose another means of changing the
// value and having the IME up makes no sense.
InputMethodManager inputMethodManager = InputMethodManager.peekInstance();
if (inputMethodManager != null) {
if (inputMethodManager.isActive(mYearSpinnerInput)) {
mYearSpinnerInput.clearFocus();
inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0);
} else if (inputMethodManager.isActive(mMonthSpinnerInput)) {
mMonthSpinnerInput.clearFocus();
inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0);
} else if (inputMethodManager.isActive(mDaySpinnerInput)) {
mDaySpinnerInput.clearFocus();
inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0);
}
}
}
}
/**
* 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<SavedState> CREATOR = new Creator<SavedState>() {
public SavedState createFromParcel(Parcel in) {
return new SavedState(in);
}
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}
}